diff options
19 files changed, 1294 insertions, 299 deletions
diff --git a/.github/nativefuncs.json b/.github/nativefuncs.json index 11042903..10ceebd5 100644 --- a/.github/nativefuncs.json +++ b/.github/nativefuncs.json @@ -7,10 +7,10 @@ "argTypes":"" }, { - "name":"NSIsModEnabled", + "name":"NSGetModsInformation", "helpText":"", - "returnTypeString":"bool", - "argTypes":"string modName" + "returnTypeString":"array<ModInfo>", + "argTypes":"" }, { "name":"NSSetModEnabled", @@ -19,48 +19,6 @@ "argTypes":"string modName, bool enabled" }, { - "name":"NSIsModRemote", - "helpText":"", - "returnTypeString":"bool", - "argTypes":"string modName" - }, - { - "name":"NSGetModDescriptionByModName", - "helpText":"", - "returnTypeString":"string", - "argTypes":"string modName" - }, - { - "name":"NSGetModVersionByModName", - "helpText":"", - "returnTypeString":"string", - "argTypes":"string modName" - }, - { - "name":"NSGetModDownloadLinkByModName", - "helpText":"", - "returnTypeString":"string", - "argTypes":"string modName" - }, - { - "name":"NSGetModLoadPriority", - "helpText":"", - "returnTypeString":"int", - "argTypes":"string modName" - }, - { - "name":"NSIsModRequiredOnClient", - "helpText":"", - "returnTypeString":"bool", - "argTypes":"string modName" - }, - { - "name":"NSGetModConvarsByModName", - "helpText":"", - "returnTypeString":"array<string>", - "argTypes":"string modName" - }, - { "name":"DecodeJSON", "helpText":"converts a json string to a squirrel table", "returnTypeString":"table", @@ -261,60 +219,12 @@ "argTypes":"" }, { - "name":"NSIsModEnabled", - "helpText":"", - "returnTypeString":"bool", - "argTypes":"string modName" - }, - { "name":"NSSetModEnabled", "helpText":"", "returnTypeString":"void", "argTypes":"string modName, bool enabled" }, { - "name":"NSIsModRemote", - "helpText":"", - "returnTypeString":"bool", - "argTypes":"string modName" - }, - { - "name":"NSGetModDescriptionByModName", - "helpText":"", - "returnTypeString":"string", - "argTypes":"string modName" - }, - { - "name":"NSGetModVersionByModName", - "helpText":"", - "returnTypeString":"string", - "argTypes":"string modName" - }, - { - "name":"NSGetModDownloadLinkByModName", - "helpText":"", - "returnTypeString":"string", - "argTypes":"string modName" - }, - { - "name":"NSGetModLoadPriority", - "helpText":"", - "returnTypeString":"int", - "argTypes":"string modName" - }, - { - "name":"NSIsModRequiredOnClient", - "helpText":"", - "returnTypeString":"bool", - "argTypes":"string modName" - }, - { - "name":"NSGetModConvarsByModName", - "helpText":"", - "returnTypeString":"array<string>", - "argTypes":"string modName" - }, - { "name":"DecodeJSON", "helpText":"converts a json string to a squirrel table", "returnTypeString":"table", @@ -467,58 +377,22 @@ "argTypes":"" }, { - "name":"NSIsModEnabled", - "helpText":"", - "returnTypeString":"bool", - "argTypes":"string modName" - }, - { - "name":"NSSetModEnabled", + "name":"NSGetModsInformation", "helpText":"", - "returnTypeString":"void", - "argTypes":"string modName, bool enabled" - }, - { - "name":"NSIsModRemote", - "helpText":"", - "returnTypeString":"bool", - "argTypes":"string modName" - }, - { - "name":"NSGetModDescriptionByModName", - "helpText":"", - "returnTypeString":"string", - "argTypes":"string modName" - }, - { - "name":"NSGetModVersionByModName", - "helpText":"", - "returnTypeString":"string", - "argTypes":"string modName" - }, - { - "name":"NSGetModDownloadLinkByModName", - "helpText":"", - "returnTypeString":"string", - "argTypes":"string modName" - }, - { - "name":"NSGetModLoadPriority", - "helpText":"", - "returnTypeString":"int", - "argTypes":"string modName" + "returnTypeString":"array<ModInfo>", + "argTypes":"" }, { - "name":"NSIsModRequiredOnClient", + "name":"NSGetModInformation", "helpText":"", - "returnTypeString":"bool", + "returnTypeString":"array<ModInfo>", "argTypes":"string modName" }, { - "name":"NSGetModConvarsByModName", + "name":"NSSetModEnabled", "helpText":"", - "returnTypeString":"array<string>", - "argTypes":"string modName" + "returnTypeString":"void", + "argTypes":"string modName, bool enabled" }, { "name": "NSFetchVerifiedModsManifesto", @@ -541,6 +415,12 @@ "argTypes": "string name, string version" }, { + "name": "NSCancelModDownload", + "helpText": "prevents installation of the mod currently being installed", + "returnTypeString": "void", + "argTypes": "" + }, + { "name": "NSGetModInstallState", "helpText": "get status of the mod currently being installed", "returnTypeString": "ModInstallState", diff --git a/Northstar.Client/mod/resource/northstar_client_localisation_english.txt b/Northstar.Client/mod/resource/northstar_client_localisation_english.txt index f7c5ee2d..76e4fce1 100644 --- a/Northstar.Client/mod/resource/northstar_client_localisation_english.txt +++ b/Northstar.Client/mod/resource/northstar_client_localisation_english.txt @@ -312,6 +312,12 @@ Press Yes if you agree to this. This choice can be changed in the mods menu at a // In-game chat "HUD_CHAT_WHISPER_PREFIX" "[WHISPER]" "HUD_CHAT_SERVER_PREFIX" "[SERVER]" + + // Team Switching + "TEAMSWITCH_GAMEMODE" "Gamemode does not allow Team Switching" + "TEAMSWITCH_BUFFER" "Team Switching is on Cooldown" + "TEAMSWITCH_GAMEPLAY" "Team change not allowed outside playing phase" + "TEAMSWITCH_DISABLED" "Current gamemode doesn't support team change" "NO_GAMESERVER_RESPONSE" "Couldn't reach game server" "BAD_GAMESERVER_RESPONSE" "Game server gave an invalid response" diff --git a/Northstar.Client/mod/resource/northstar_client_localisation_japanese.txt b/Northstar.Client/mod/resource/northstar_client_localisation_japanese.txt index 798d603e..47507c00 100644 --- a/Northstar.Client/mod/resource/northstar_client_localisation_japanese.txt +++ b/Northstar.Client/mod/resource/northstar_client_localisation_japanese.txt @@ -404,6 +404,7 @@ "PROGRESSION_ANNOUNCEMENT_BODY" "^CCCC0000進行システムが開放されました!^\n\nNorthstarはバニラの進行システム、つまり武器、スキン、タイタン等をレベルアップやチャレンジ達成で解除する機能をサポートしています。\n\nこの進行システムは、ロビー画面下のボタンから有効にできます。\n\nこの設定はいつでも変更可能です。" "MOD_NOT_VERIFIED" "(Modが検証されていないため、自動ダウンロードできませんでした)" "WRONG_MOD_VERSION" "サーバーはMod\"%s1\"v%s2 を要求していますが、あなたのModバージョンは%s3です" + "MODE_MENU_PVPVE" "PvPvE" // Translation done by Zetryox and CYakigasi } diff --git a/Northstar.Client/mod/resource/northstar_client_localisation_russian.txt b/Northstar.Client/mod/resource/northstar_client_localisation_russian.txt index a0f751ff..7742e768 100644 --- a/Northstar.Client/mod/resource/northstar_client_localisation_russian.txt +++ b/Northstar.Client/mod/resource/northstar_client_localisation_russian.txt @@ -32,7 +32,7 @@ "PRIVATE_MATCH_SINGLEPLAYER_LEVEL" "%s1 (Одиночная игра)" // fra hint for private match menu, because fra only has PL_fra_desc in vanilla - "PL_fra_hint" "Ты один. Уничтожь противников, чтобы победить. Собери 3 батареи для вызова титана." + "PL_fra_hint" "Все против всех. Уничтожай противников, чтобы победить. Собери 3 батареи для вызова титана." // mode settings "MODE_SETTING_CATEGORY_PILOT" "Пилот" @@ -41,7 +41,7 @@ "MODE_SETTING_CATEGORY_MATCH" "Матч" "classic_mp" "Классический мультиплеер" - "run_epilogue" "Показывать эпилог" + "run_epilogue" "Запускать эпилог" "scorelimit" "Лимит очков" "roundscorelimit" "Лимит очков (по раундам)" "timelimit" "Лимит времени" @@ -86,18 +86,18 @@ "PL_gg_abbr" "ГВ" "GAMEMODE_GG" "Гонка вооружений" - "PL_tt" "Разборки Титанов" - "PL_tt_lobby" "Лобби разборок титанов" - "PL_tt_desc" "Зарабатывайте очки, пока вы находитесь в своём титане. Уничтожьте титана чтобы получить своего." - "PL_tt_hint" "Зарабатывайте очки, пока вы находитесь в своём титане. Уничтожьте титана чтобы получить своего." - "PL_tt_abbr" "TT" - "GAMEMODE_TT" "Разборки титанов" + "PL_tt" "Салочки" + "PL_tt_lobby" "Лобби салочек" + "PL_tt_desc" "Зарабатывайте очки будучи в титане. Уничтожьте титана чтобы получить собственного." + "PL_tt_hint" "Зарабатывайте очки будучи в титане. Уничтожьте титана чтобы получить собственного." + "PL_tt_abbr" "СЛ" + "GAMEMODE_TT" "Салочки" "PL_inf" "Заражение" "PL_inf_lobby" "Лобби Заражения" "PL_inf_desc" "Переживите инфекцию. Выжившие становятся заражёнными при убийстве." "PL_inf_hint" "Переживите инфекцию. Выжившие становятся заражёнными при убийстве." - "PL_inf_abbr" "INF" + "PL_inf_abbr" "ЗРЖ" "GAMEMODE_INF" "Заражение" "INFECTION_YOU_ARE_INFECTED" "Вас заразили!" "INFECTION_KILL_SURVIVORS" "Заразите всех оставшихся выживших." @@ -111,7 +111,7 @@ "PL_hs_lobby" "Лобби пряток" "PL_hs_desc" "Игра со стандартными правилами пряток." "PL_hs_hint" "Игра со стандартными правилами пряток." - "PL_hs_abbr" "HS" + "PL_hs_abbr" "ПРЯТ" "GAMEMODE_hs" "Прятки" "HIDEANDSEEK_YOU_ARE_SEEKER" "ВЫ ИЩЕТЕ" "HIDEANDSEEK_SEEKER_DESC" "Найдите прячущихся и ударьте их.\nВы появитесь через %s1 секунд(у)" @@ -280,7 +280,7 @@ "NO_RESULTS" "Нет результатов." "NO_MODS" "Нечего настраивать! Загрузите моды с ^5588FF00northstar.thunderstore.io^0." "respawnprotection" "Длит. защиты после возрождения" - "SNS_BANKRUPT_SUB" "Вас обнулил %s1" + "SNS_BANKRUPT_SUB" "Ваши очки сбросил %s1" "PL_hidden" "Невидимка" "PL_hidden_lobby" "Невидимка — лобби" "GAMEMODE_HIDDEN" "Невидимка" @@ -300,7 +300,7 @@ "PL_hidden_abbr" "НЕВИД" "SCOREBOARD_BANKRUPTS" "Врагов обанкрочено" "SNS_LEADER_BANKRUPT" "Лидер обанкрочен!" - "SNS_LEADER_BANKRUPT_SUB" "%s1 был обнулён игроком %s2" + "SNS_LEADER_BANKRUPT_SUB" "%s1 был сброшен игроком %s2" "gg_kill_reward" "Множитель награды за убийство" "gg_execution_reward" "Множитель награды за казнь" "PL_tffa" "Все против всех на титанах" @@ -322,8 +322,8 @@ "GAMEMODE_TFFA" "Все против всех на титанах" "SHOW_ONLY_DISABLED" "Только выключенные" "HIDE_LOCKED" "Скрыть недоступные" - "PL_chamber_desc" "Один выстрел — один труп. Заработайте ещё один патрон, убив врага первым." - "PL_chamber_hint" "Один выстрел — один труп. Заработайте ещё один патрон, убив врага первым." + "PL_chamber_desc" "Один выстрел — один труп. Один труп — ещё один выстрел." + "PL_chamber_hint" "Один выстрел — один труп. Один труп — ещё один выстрел." "PL_hidden_desc" "Один из игроков невидим." "PL_tffa_hint" "Каждый пилот сам за себя. Уничтожьте всех вражеских титанов." "sns_softball_kill_value" "Очков за убийство Софтболом" @@ -374,5 +374,9 @@ "MODE_MENU_UNKNOWN" "Неизвестный" "MODE_MENU_SWITCH" "Фильтр" "MODE_MENU_TITAN_ONLY" "Только титаны" + "TEAMSWITCH_BUFFER" "Нельзя сменять команду так часто" + "TEAMSWITCH_GAMEMODE" "Невозможно сменить команду в текущем режиме игры" + "TEAMSWITCH_DISABLED" "Нельзя сменить команду в текущем режиме игры" + "TEAMSWITCH_GAMEPLAY" "Команду можно сменять только в игровой стадии" } } diff --git a/Northstar.Client/mod/scripts/vscripts/cl_northstar_client_init.nut b/Northstar.Client/mod/scripts/vscripts/cl_northstar_client_init.nut index 9e683a86..e4e44d51 100644 --- a/Northstar.Client/mod/scripts/vscripts/cl_northstar_client_init.nut +++ b/Northstar.Client/mod/scripts/vscripts/cl_northstar_client_init.nut @@ -29,6 +29,19 @@ global struct UIPresenceStruct { int gameState } +global struct ModInfo +{ + string name = "" + string description = "" + string version = "" + string downloadLink = "" + int loadPriority = 0 + bool enabled = false + bool requiredOnClient = false + bool isRemote + array<string> conVars = [] +} + global struct RequiredModInfo { string name diff --git a/Northstar.Client/mod/scripts/vscripts/client/cl_damage_indicator.gnut b/Northstar.Client/mod/scripts/vscripts/client/cl_damage_indicator.gnut new file mode 100644 index 00000000..4857c109 --- /dev/null +++ b/Northstar.Client/mod/scripts/vscripts/client/cl_damage_indicator.gnut @@ -0,0 +1,951 @@ +untyped + +global function ClDamageIndicator_Init +global function Create_DamageIndicatorHUD +global function DamageIndicators +global function GrenadeArrowThink +global function RumbleForTitanDamage + +global function ServerCallback_TitanTookDamage +global function ServerCallback_PilotTookDamage +//global function ClientCodeCallback_OnMissileCreation +global function ClientCodeCallback_CreateGrenadeIndicator + +global function DamageIndicatorRui + +global function ShowGrenadeArrow + +global function SCB_AddGrenadeIndicatorForEntity + +const DAMAGEARROW_FADEANIM = "damage_fade" +const DAMAGEARROW_DURATION = 2.5 +const DAMAGEARROW_SMALL = 0 +const DAMAGEARROW_MEDIUM = 1 +const DAMAGEARROW_LARGE = 2 + +const float DAMAGEHUD_GRENADE_DEBOUNCE_TIME = 0.4 +const float DAMAGEHUD_GRENADE_DEBOUNCE_TIME_LOWSPEED = 0.15 +const float DAMAGEHUD_GRENADE_DEBOUNCE_TIME_LOWSPEED_VELOCITYCUTOFF = 500.0 + + +struct { + array<table> damageArrows + int currentDamageArrow = 0 + int numDamageArrows = 16 + float damageArrowFadeDuration = 1.0 + float damageArrowTime = 0.0 + vector damageArrowAngles = < 0.0, 0.0, 0.0 > + vector damageArrowPointCenter = < 0.0, 0.0, 0.0 > + + table whizByFX = { + small = null, + large = null, + titan = null, + } + + array<table> arrowIncomingAnims = [ + { anim = "damage_incoming_small", duration = 1.5 }, + { anim = "damage_incoming", duration = 1.75 }, + { anim = "damage_incoming_large", duration = 2.00 }, + ] + + int damageIndicatorCount = 0 +} file + +function ClDamageIndicator_Init() +{ + RegisterSignal( "CriticalHitReceived" ) + + AddCreateCallback( "titan_cockpit", DamageArrow_CockpitInit ) + + PrecacheParticleSystem( $"P_wpn_grenade_frag_icon" ) + PrecacheParticleSystem( $"P_wpn_grenade_frag_blue_icon" ) + PrecacheParticleSystem( $"P_wpn_grenade_smoke_icon" ) + + if ( !IsLobby() ) + AddCallback_EntitiesDidLoad( InitDamageArrows ) +} + +function ServerCallback_TitanTookDamage( damage, x, y, z, damageType, damageSourceId, attackerEHandle, eModId, doomedNow, doomedDamage ) +{ + expect float( damage ) + expect int( damageType ) + expect int( damageSourceId ) + expect bool( doomedNow ) + expect int( doomedDamage ) + + if ( IsWatchingThirdPersonKillReplay() ) + return + + if ( DebugVictimClientDamageFeedbackIsEnabled() && (damage > 0.0) ) + { + entity attacker = attackerEHandle ? GetHeavyWeightEntityFromEncodedEHandle( attackerEHandle ) : null + entity localViewPlayer = GetLocalViewPlayer() + bool isHeadShot = (damageType & DF_HEADSHOT) ? true : false + bool isKillShot = (damageType & DF_KILLSHOT) ? true : false + bool isCritical = (damageType & DF_CRITICAL) ? true : false + bool isDoomProtected = (damageType & DF_DOOM_PROTECTED) ? true : false + bool isDoomFatality = (damageType & DF_DOOM_FATALITY) ? true : false + + local weaponMods = [] + if ( eModId != null && eModId in modNameStrings ) + weaponMods.append( modNameStrings[eModId] ) + + string modDesc = ((eModId != null && eModId in modNameStrings) ? (expect string( modNameStrings[eModId] )) : "") + DebugTookDamagePrint( localViewPlayer, attacker, damage, damageSourceId, modDesc, isHeadShot, isKillShot, isCritical, doomedNow, doomedDamage, isDoomProtected, isDoomFatality ) + } + + // It appears to be faster here to create a new thread so other functions called can wait until the frame ends before running. + thread TitanTookDamageThread( damage, x, y, z, damageType, damageSourceId, attackerEHandle, eModId, doomedNow, doomedDamage ) + + vector damageOrigin = < x, y, z > + entity attacker = attackerEHandle ? GetHeavyWeightEntityFromEncodedEHandle( attackerEHandle ) : null + + if ( damageSourceId in clGlobal.onLocalPlayerTookDamageCallback ) + { + foreach ( callback in clGlobal.onLocalPlayerTookDamageCallback[ damageSourceId ] ) + callback( damage, damageOrigin, damageType, damageSourceId, attacker ) + } +} + +function TitanTookDamageThread( float damage, x, y, z, int damageType, int damageSourceId, attackerEHandle, eModId, bool doomedNow, int doomedDamage ) +{ + WaitEndFrame() + + entity attacker = attackerEHandle ? GetHeavyWeightEntityFromEncodedEHandle( attackerEHandle ) : null + entity localViewPlayer = GetLocalViewPlayer() + entity cockpit = localViewPlayer.GetCockpit() + + if ( cockpit && IsTitanCockpitModelName( cockpit.GetModelName() ) ) + TitanCockpit_DamageFeedback( localViewPlayer, cockpit, damage, damageType, < x, y, z >, damageSourceId, doomedNow, doomedDamage ) + + if ( damage >= DAMAGE_BREAK_MELEE_ASSIST ) + localViewPlayer.Lunge_ClearTarget() + + if ( damageSourceId != eDamageSourceId.bubble_shield ) //Don't play Betty OS dialogue if we took damage by bubble shield. We don't have appropriate dialogue for it. + Tracker_PlayerAttackedByTarget( localViewPlayer, attacker ) + + array<string> weaponMods + if ( eModId != null && eModId in modNameStrings ) + weaponMods.append( expect string( modNameStrings[ eModId ] ) ) + + if ( (damage > 0.0) || doomedDamage ) + { + vector damageOrigin = < x, y, z > + DamageHistoryStruct damageHistory = StoreDamageHistoryAndUpdate( localViewPlayer, MAX_DAMAGE_HISTORY_TIME, damage, damageOrigin, damageType, damageSourceId, attacker, weaponMods ) + DamageIndicators( damageHistory, true ) + } + + entity soul = localViewPlayer.GetTitanSoul() + if ( PlayerHasPassive( localViewPlayer, ePassives.PAS_AUTO_EJECT ) ) //TODO: Handle nuclear eject if we ever allow nuclear + auto eject combo again + { + if ( ShouldPlayAutoEjectAnim( localViewPlayer, soul, doomedNow ) ) + thread PlayerEjects( localViewPlayer, cockpit ) + + } + + if ( damageType & DF_CRITICAL ) + { + localViewPlayer.Signal( "CriticalHitReceived" ) + EmitSoundOnEntity( localViewPlayer, "titan_damage_crit_3p_vs_1p" ) + } +} + +bool function ShouldPlayAutoEjectAnim( entity player, entity titanSoul, bool doomedNow ) +{ + if ( !titanSoul.IsDoomed() ) + return false + + if ( player.ContextAction_IsActive() && !player.ContextAction_IsBusy() ) //Some other context action, e.g. melee instead of eject. Then again + return false + + return true +} + +string function DevBuildAttackerDesc( entity localViewPlayer, entity ent ) +{ + if ( ent == null ) + return "<null>" + + if ( localViewPlayer == ent ) + return ("<self>") + + if ( ent.IsPlayer() ) + return ("'" + ent.GetPlayerName() + "' " + ent.GetPlayerSettings()) + + entity bossPlayer = ent.GetBossPlayer() + string ownerString = ((bossPlayer != null) ? (bossPlayer.GetPlayerName() + "'s ") : "") + + var sigName = ent.GetSignifierName() + string debugName = (sigName != null) ? expect string( sigName ) : ent.GetClassName() + return (ownerString + debugName) +} + +void function DebugTookDamagePrint( entity ornull localViewPlayer, entity attacker, float damage, int damageSourceId, string modDesc, bool isHeadShot, bool isKillShot, bool isCritical, bool isDoomShot, int doomShotDamage, bool isDoomProtected, bool isDoomFatality ) +{ + Assert( localViewPlayer ) + string attackerDesc = DevBuildAttackerDesc( expect entity( localViewPlayer ), attacker ) + string timePrint = format( "%d:%.2f", FrameCount(), PlatformTime() ) + printt( + "{"+timePrint+"} TOOK DAMAGE: " + damage + + (isHeadShot ? " (headshot)" : "") + + (isCritical ? " (critical)" : "") + + (isKillShot ? " (KILLED)" : "") + + (isDoomShot ? " (DOOMED dmg:" + doomShotDamage + ")" : "") + + (isDoomProtected ? " (DOOM PROTECTION)" : "") + + (isDoomFatality ? " (DOOM FATALITY)" : "") + + " " + attackerDesc + + " w/ " + GetObitFromDamageSourceID( damageSourceId ) + modDesc + ) +} + +void function PlayVictimHeadshotConfirmation( bool isKillShot ) +{ + entity localViewPlayer = GetLocalViewPlayer() + if ( localViewPlayer == null ) + return + + if ( isKillShot ) + EmitSoundOnEntity( localViewPlayer, "Player.Hitbeep_headshot.Kill.Human_3P_vs_1P" ) + else + EmitSoundOnEntity( localViewPlayer, "Player.Hitbeep_headshot.Human_3P_vs_1P" ) +} + +void function RumbleForPilotDamage( float damageAmount ) +{ + Rumble_Play( "rumble_pilot_hurt", {} ) +} + +void function RumbleForTitanDamage( float damageAmount ) +{ + string rumbleName; + if ( damageAmount < 500 ) + rumbleName = "titan_damaged_small" + else if ( damageAmount < 1000 ) + rumbleName = "titan_damaged_med" + else + rumbleName = "titan_damaged_big" + + Rumble_Play( rumbleName, {} ) +} + +function ServerCallback_PilotTookDamage( damage, x, y, z, damageType, damageSourceId, attackerEHandle, eModId ) +{ + expect float( damage ) + expect int( damageType ) + expect int( damageSourceId ) + + if ( IsWatchingThirdPersonKillReplay() ) + return + + entity attacker = attackerEHandle ? GetHeavyWeightEntityFromEncodedEHandle( attackerEHandle ) : null + entity localViewPlayer = GetLocalViewPlayer() + vector damageOrigin = < x, y, z > + + bool isHeadShot = (damageType & DF_HEADSHOT) ? true : false + bool isKillShot = (damageType & DF_KILLSHOT) ? true : false + + if ( isHeadShot ) + PlayVictimHeadshotConfirmation( isKillShot ); + + //Jolt view if player is getting meleed + if ( damageSourceId == eDamageSourceId.human_melee ) + { + vector joltDir = Normalize( localViewPlayer.CameraPosition() - damageOrigin ) + //clear melee assist when you get meleed + localViewPlayer.Lunge_ClearTarget() + } + + array<string> weaponMods + if ( eModId != null && eModId in modNameStrings ) + weaponMods.append( expect string( modNameStrings[ eModId ] ) ) + + if ( DebugVictimClientDamageFeedbackIsEnabled() && !IsWatchingReplay() ) + { + string modDesc = (weaponMods.len() > 0 ? (" +" + weaponMods[0]) : "") + bool isCritical = (damageType & DF_CRITICAL) ? true : false + + DebugTookDamagePrint( localViewPlayer, attacker, damage, damageSourceId, modDesc, isHeadShot, isKillShot, isCritical, false, 0, false, false ) + } + + RumbleForPilotDamage( damage ) + + DamageHistoryStruct damageTable = StoreDamageHistoryAndUpdate( localViewPlayer, MAX_DAMAGE_HISTORY_TIME, damage, damageOrigin, damageType, damageSourceId, attacker, weaponMods ) + + DamageIndicators( damageTable, false ) + + if ( damageSourceId in clGlobal.onLocalPlayerTookDamageCallback ) + { + foreach ( callback in clGlobal.onLocalPlayerTookDamageCallback[ damageSourceId ] ) + callback( damage, damageOrigin, damageType, damageSourceId, attacker ) + } +} + +/* +void function ClientCodeCallback_OnMissileCreation( entity missileEnt, string weaponName, bool firstTime ) +{ + +} +*/ + +void function ClientCodeCallback_CreateGrenadeIndicator( entity missileEnt, string weaponName ) +{ + if ( !IsValid( missileEnt ) ) + return + + //Called for all projectiles, not just missiles. + TryAddGrenadeIndicator( missileEnt, weaponName ) +} + + +void function DamageIndicators( DamageHistoryStruct damageHistory, bool playerIsTitan ) +{ + if ( damageHistory.damageType & DF_NO_INDICATOR ) + return + if ( !level.clientScriptInitialized ) + return + if ( IsWatchingThirdPersonKillReplay() ) + return + + entity localViewPlayer = GetLocalViewPlayer() + + int arrowType = DAMAGEARROW_MEDIUM + + if ( IsValid( damageHistory.attacker ) ) + { + if ( damageHistory.attacker == localViewPlayer ) + return + + if ( damageHistory.attacker.IsTitan() ) + arrowType = DAMAGEARROW_MEDIUM + else if ( damageHistory.attacker.IsPlayer() ) + arrowType = DAMAGEARROW_SMALL + else + arrowType = DAMAGEARROW_SMALL + + //if ( damageHistory.attacker.IsTitan() ) + // arrowType = DAMAGEARROW_LARGE + //else if ( damageHistory.attacker.IsPlayer() ) + // arrowType = DAMAGEARROW_MEDIUM + //else + // arrowType = DAMAGEARROW_SMALL + } + + if ( playerIsTitan ) + { + entity cockpit = localViewPlayer.GetCockpit() + + if ( !cockpit ) + return + + vector dirToDamage = damageHistory.origin - localViewPlayer.GetOrigin() + dirToDamage.z = 0 + dirToDamage = Normalize( dirToDamage ) + + vector playerViewForward = localViewPlayer.GetViewVector() + playerViewForward.z = 0.0 + playerViewForward = Normalize( playerViewForward ) + + float damageFrontDot = DotProduct( dirToDamage, playerViewForward ) + + if ( damageFrontDot >= 0.707107 ) + cockpit.AddToTitanHudDamageHistory( COCKPIT_PANEL_TOP, damageHistory.damage ) + else if ( damageFrontDot <= -0.707107 ) + cockpit.AddToTitanHudDamageHistory( COCKPIT_PANEL_BOTTOM, damageHistory.damage ) + else + { + vector playerViewRight = localViewPlayer.GetViewRight() + playerViewRight.z = 0.0 + playerViewRight = Normalize( playerViewRight ) + + float damageRightDot = DotProduct( dirToDamage, playerViewRight ) + + if ( damageRightDot >= 0.707107 ) + cockpit.AddToTitanHudDamageHistory( COCKPIT_PANEL_RIGHT, damageHistory.damage ) + else + cockpit.AddToTitanHudDamageHistory( COCKPIT_PANEL_LEFT, damageHistory.damage ) + } + + if ( damageHistory.attacker && damageHistory.attacker.GetParent() == localViewPlayer ) + { + damageHistory.rodeoDamage = true + return + } + } + + #if SP + if ( IsValid( damageHistory.attacker ) && damageHistory.attacker.IsTitan() ) + arrowType = DAMAGEARROW_LARGE + else if ( playerIsTitan && damageHistory.damage < 50 ) + return + else if ( !playerIsTitan && damageHistory.damage < 15 ) + arrowType = DAMAGEARROW_SMALL + else + arrowType = DAMAGEARROW_MEDIUM + + thread DamageIndicatorRui( damageHistory.origin, arrowType, playerIsTitan ) + #else + bool show2DIndicator = true + bool show3DIndicator = false + + const int DAMAGE_INDICATOR_STYLE_2D_ONLY = 0 + const int DAMAGE_INDICATOR_STYLE_BOTH = 1 + const int DAMAGE_INDICATOR_STYLE_3D_ONLY = 2 + + if ( playerIsTitan ) + { + show2DIndicator = GetConVarInt( "damage_indicator_style_titan" ) != DAMAGE_INDICATOR_STYLE_3D_ONLY + show3DIndicator = GetConVarInt( "damage_indicator_style_titan" ) != DAMAGE_INDICATOR_STYLE_2D_ONLY + } + else + { + show2DIndicator = GetConVarInt( "damage_indicator_style_pilot" ) != DAMAGE_INDICATOR_STYLE_3D_ONLY + show3DIndicator = GetConVarInt( "damage_indicator_style_pilot" ) != DAMAGE_INDICATOR_STYLE_2D_ONLY + } + + if ( show2DIndicator ) + thread DamageIndicatorRui( damageHistory.origin, arrowType, playerIsTitan ) + + if ( show3DIndicator ) + ShowDamageArrow( localViewPlayer, damageHistory.origin, arrowType, playerIsTitan, damageHistory.attacker ) + #endif +} + +const float DAMAGE_INDICATOR_DURATION = 4.0 + +void function DamageIndicatorRui( vector damageOrigin, int arrowType, bool playerIsTitan ) +{ + clGlobal.levelEnt.EndSignal( "KillReplayStarted" ) + clGlobal.levelEnt.EndSignal( "KillReplayEnded" ) + + // slop + float distance = Length( damageOrigin - GetLocalViewPlayer().CameraPosition() ) + float randomRange = GraphCapped( distance, 0.0, 2048, 0.0, 256.0 ) + damageOrigin = <damageOrigin.x + RandomFloatRange( randomRange, -randomRange ), damageOrigin.y + RandomFloatRange( randomRange, -randomRange ), damageOrigin.z> + + float startTime = Time() + + var rui = RuiCreate( $"ui/damage_indicator.rpak", clGlobal.topoFullScreen, RUI_DRAW_HUD, 0 ) + RuiSetResolutionToScreenSize( rui ) + RuiSetGameTime( rui, "startTime", startTime ) + RuiSetFloat( rui, "duration", DAMAGE_INDICATOR_DURATION ) + RuiSetInt( rui, "attackerType", arrowType ) + + file.damageIndicatorCount++ + int damageIndicatorThreshold = file.damageIndicatorCount + 8 + + OnThreadEnd( + function() : ( rui ) + { + RuiDestroy( rui ) + } + ) + + while ( Time() - startTime < DAMAGE_INDICATOR_DURATION && file.damageIndicatorCount < damageIndicatorThreshold ) + { + vector vecToDamage = damageOrigin - GetLocalViewPlayer().CameraPosition() + vecToDamage.z = 0 + vecToDamage = Normalize( vecToDamage ) + RuiSetFloat3( rui, "vecToDamage2D", vecToDamage ) + RuiSetFloat3( rui, "camVec2D", Normalize( AnglesToForward( < 0, GetLocalViewPlayer().CameraAngles().y, 0 > ) ) ) + RuiSetFloat( rui, "sideDot", vecToDamage.Dot( CrossProduct( <0, 0, 1>, Normalize( AnglesToForward( < 0, GetLocalViewPlayer().CameraAngles().y, 0 > ) ) ) ) ) + WaitFrame() + } +} + +void function ShowGrenadeArrow( entity player, entity grenade, float damageRadius, float startDelay, bool requireLos = true ) +{ + thread GrenadeArrowThink( player, grenade, damageRadius, startDelay, requireLos ) +} + +vector function GetRandomOriginWithinBounds( entity ent ) +{ + vector boundingMins = ent.GetBoundingMins() + vector boundingMaxs = ent.GetBoundingMaxs() + + vector randomOffset = < RandomFloatRange( boundingMins.x, boundingMaxs.x ), RandomFloatRange( boundingMins.y, boundingMaxs.y ), RandomFloatRange( boundingMins.z, boundingMaxs.z ) > + + return ent.GetOrigin() + randomOffset +} + +void function GrenadeArrowThink( entity player, entity grenade, float damageRadius, float startDelay, bool requireLos, string requiredPlayerState = "any" ) +{ + EndSignal( grenade, "OnDeath" ) //On death added primarily for frag_drones + EndSignal( grenade, "OnDestroy" ) + EndSignal( player, "OnDeath" ) + + wait startDelay + + asset grenadeModel = GRENADE_INDICATOR_FRAG_MODEL + vector grenadeOffset = < -5, 0, 0 > + if ( grenade instanceof C_Projectile ) + { + if ( grenade.ProjectileGetWeaponClassName() == "mp_weapon_grenade_sonar" ) + { + grenadeModel = GRENADE_INDICATOR_SONAR_MODEL + grenadeOffset = < -5, 0, 0 > + requireLos = false + } + } + else if ( grenade.IsNPC() ) + { + switch ( grenade.GetSignifierName() ) + { + #if MP + case "npc_stalker": + grenadeModel = GRENADE_INDICATOR_STALKER_MODEL + break + #endif + + case "npc_frag_drone": + grenadeModel = GRENADE_INDICATOR_TICK_MODEL + break + } + } + + entity arrow = CreateClientSidePropDynamic( < 0, 0, 0 >, < 0, 0, 0 >, GRENADE_INDICATOR_ARROW_MODEL ) + entity mdl = CreateClientSidePropDynamic( < 0, 0, 0 >, < 0, 0, 0 >, grenadeModel ) + + OnThreadEnd( + function() : ( arrow, mdl ) + { + if ( IsValid( arrow ) ) + arrow.Destroy() + if ( IsValid( mdl ) ) + mdl.Destroy() + } + ) + + entity cockpit = player.GetCockpit() + if ( !cockpit ) + return + + EndSignal( cockpit, "OnDestroy" ) + + arrow.SetParent( cockpit, "CAMERA_BASE" ) + arrow.SetAttachOffsetOrigin( < 25.0, 0.0, -4.0 > ) + + mdl.SetParent( arrow, "BACK" ) + mdl.SetAttachOffsetOrigin( grenadeOffset ) + + float lastVisibleTime = 0 + bool shouldBeVisible = true + + while ( true ) + { + cockpit = player.GetCockpit() + + switch ( requiredPlayerState ) + { + case "any": + shouldBeVisible = true + break + case "pilot": + shouldBeVisible = !player.IsTitan() + break + case "titan": + shouldBeVisible = player.IsTitan() + break + default: + Assert( false, "Invalid player state! Allower states: 'any' 'pilot' 'titan'" ) + + } + + if ( shouldBeVisible ) + { + if ( Distance( player.GetOrigin(), grenade.GetOrigin() ) > damageRadius || !cockpit ) + { + shouldBeVisible = false + } + else + { + bool tracePassed = false + + if ( requireLos ) + { + TraceResults result = TraceLine( grenade.GetOrigin(), GetRandomOriginWithinBounds( player ), grenade, TRACE_MASK_SHOT, TRACE_COLLISION_GROUP_NONE ) + + if ( result.fraction == 1.0 ) + tracePassed = true + } + + if ( requireLos && !tracePassed ) + { + shouldBeVisible = false + } + else + { + shouldBeVisible = true + lastVisibleTime = Time() + } + } + } + + if ( shouldBeVisible || Time() - lastVisibleTime < 0.25 ) + { + arrow.EnableDraw() + mdl.EnableDraw() + + arrow.DisableRenderWithViewModelsNoZoom() + arrow.EnableRenderWithCockpit() + arrow.EnableRenderWithHud() + mdl.DisableRenderWithViewModelsNoZoom() + mdl.EnableRenderWithCockpit() + mdl.EnableRenderWithHud() + + vector damageArrowAngles = AnglesInverse( player.EyeAngles() ) + vector vecToDamage = grenade.GetOrigin() - (player.EyePosition() + (player.GetViewVector() * 20.0)) + + // reparent for embark/disembark + if ( arrow.GetParent() == null ) + arrow.SetParent( cockpit, "CAMERA_BASE", true ) + + arrow.SetAttachOffsetAngles( damageArrowAngles.AnglesCompose( vecToDamage.VectorToAngles() ) ) + } + else + { + mdl.DisableDraw() + arrow.DisableDraw() + } + + WaitFrame() + } + +} + + +function Create_DamageIndicatorHUD() +{ +} + + +void function SCB_AddGrenadeIndicatorForEntity( int team, int ownerHandle, int eHandle, float damageRadius ) +{ + if ( !level.grenadeIndicatorEnabled ) + return + + #if DEV + if ( !level.clientScriptInitialized ) + return + #endif + + entity player = GetLocalViewPlayer() + entity owner = GetEntityFromEncodedEHandle( ownerHandle ) + + entity ent = GetEntityFromEncodedEHandle( eHandle ) + if ( !IsValid( ent ) ) + return + + if ( team == player.GetTeam() && owner != player ) + return + + //TryAddGrenadeIndicator( ent, "" ) // TODO: make function handle non-grenade ents +} + + +function TryAddGrenadeIndicator( grenade, weaponName ) +{ + #if DEV + if ( !level.clientScriptInitialized ) + return + #endif + + if ( !level.grenadeIndicatorEnabled ) + return + + expect entity( grenade ) + entity player = GetLocalViewPlayer() + + // view player may be null when dead + if ( !IsValid( player ) ) + return + + var className = grenade.GetClassName() + float damageRadius = 0.0 + + if ( className == "grenade" ) + { + damageRadius = grenade.GetDamageRadius() + } + else if ( grenade.ProjectileGetWeaponClassName() == "mp_titanweapon_arc_ball" ) + { + // arc ball doesn't arc to pilots so no need to show the warning + if ( !player.IsTitan() ) + return + + damageRadius = BALL_LIGHTNING_ZAP_RADIUS + } + else + { + return + } + + float radius = grenade.GetExplosionRadius() + + if ( player.IsPhaseShifted() ) + return + + + float startDelay = 0.0 + if ( grenade.GetOwner() == player ) + { + if ( !grenade.GetProjectileWeaponSettingBool( eWeaponVar.projectile_damages_owner ) && !grenade.GetProjectileWeaponSettingBool( eWeaponVar.explosion_damages_owner ) ) + return + + float relVelocity = Length( grenade.GetVelocity() - player.GetVelocity() ) + if ( relVelocity < DAMAGEHUD_GRENADE_DEBOUNCE_TIME_LOWSPEED_VELOCITYCUTOFF ) + startDelay = DAMAGEHUD_GRENADE_DEBOUNCE_TIME_LOWSPEED + else + startDelay = DAMAGEHUD_GRENADE_DEBOUNCE_TIME + } + else if ( grenade.GetTeam() == player.GetTeam() ) + { + return + } + + float padding = player.IsTitan() ? 204.0 : 65.0 + + //AddGrenadeIndicator( grenade, radius + padding, startDelay, false ) + ShowGrenadeArrow( player, grenade, radius + padding, startDelay ) + + //thread ShowRuiGrenadeThreatIndicator( grenade, float( radius ) + padding ) +} + +void function ShowRuiGrenadeThreatIndicator( entity grenade, float radius ) +{ + var rui = RuiCreate( $"ui/grenade_threat_indicator.rpak", clGlobal.topoCockpitHudPermanent, RUI_DRAW_COCKPIT, 0 ) + //var rui = CreateCockpitRui( $"ui/grenade_threat_indicator.rpak", 0 ) + RuiSetGameTime( rui, "startTime", Time() ) + RuiSetFloat( rui, "damageRadius", radius ) + //RuiTrackFloat3( rui, "pos", grenade, RUI_TRACK_ABSORIGIN_FOLLOW )` + RuiTrackFloat3( rui, "pos", grenade, RUI_TRACK_POINT_FOLLOW, grenade.LookupAttachment( "BACK" ) ) + + OnThreadEnd( + function() : ( rui ) + { + RuiDestroy( rui ) + } + ) + + grenade.WaitSignal( "OnDestroy" ) +} + + + +void function InitDamageArrows() +{ + for ( int i = 0; i < file.numDamageArrows; i++ ) + { + table arrowData = { + grenade = null + grenadeRadius = 0.0 + damageOrigin = < 0.0, 0.0, 0.0 >, + damageDirection = < 0.0, 0.0, 0.0 >, + endTime = -99.0 + DAMAGEARROW_DURATION, + startTime = -99.0, + isDying = false, + isVisible = false, + whizby = false, // hack until we get a new model/shader for the whizby indicator - Roger + attacker = null, + randomAngle = 0 // Repeated shots from the same attacker randomize the angle of the arrow. + } + + entity arrow = CreateClientSidePropDynamic( < 0, 0, 0 >, < 0, 0, 0 >, DAMAGEARROW_MODEL ) + arrow.SetCanCloak( false ) + arrow.SetVisibleForLocalPlayer( 0 ) + arrow.DisableDraw() + + arrowData.arrow <- arrow + arrow.s.arrowData <- arrowData + + file.damageArrows.append( arrowData ) + } + + entity arrow = CreateClientSidePropDynamic( < 0, 0, 0 >, < 0, 0, 0 >, DAMAGEARROW_MODEL ) + file.damageArrowFadeDuration = arrow.GetSequenceDuration( DAMAGEARROW_FADEANIM ) // 0.266 + arrow.Destroy() +} + + +void function DamageArrow_CockpitInit( entity cockpit ) +{ + entity localViewPlayer = GetLocalViewPlayer() + thread UpdateDamageArrows( localViewPlayer, cockpit ) +} + +function RefreshExistingDamageArrow( entity player, arrowData, int arrowType, damageOrigin ) +{ + //Hack - 10 tick rate is making damage feedback bunch up. If we improve that then shouldn't be threaded. + player.EndSignal( "OnDestroy" ) + entity cockpit = player.GetCockpit() + if ( IsValid( cockpit ) ) + cockpit.EndSignal( "OnDestroy" ) + + float time = Time() + + if ( arrowData.startTime == time ) + wait 0.05 + + if ( !arrowData.isVisible || arrowData.isDying ) + return + + time = Time() + arrowData.endTime = time + file.arrowIncomingAnims[ arrowType ].duration + arrowData.startTime = time + arrowData.damageOrigin = damageOrigin + arrowData.randomAngle = RandomIntRange( -3, 3 ) + PulseDamageArrow( expect entity( arrowData.arrow ), arrowType ) + UpdateDamageArrowVars( player ) + UpdateDamageArrowAngle( arrowData ) +} + +function ShowDamageArrow( entity player, damageOrigin, int arrowType, playerIsTitan, attacker ) +{ + if ( file.damageArrows.len() == 0 ) // not yet initialized + return + + table arrowData = file.damageArrows[file.currentDamageArrow] + entity arrow = expect entity( arrowData.arrow ) + + file.currentDamageArrow++ + if ( file.currentDamageArrow >= file.numDamageArrows ) + file.currentDamageArrow = 0 + + float time = Time() + + arrow.s.arrowData.damageOrigin = damageOrigin + arrow.s.arrowData.grenade = null + arrow.s.arrowData.grenadeRadius = 0.0 + arrow.s.arrowData.endTime = time + file.arrowIncomingAnims[ arrowType ].duration + arrow.s.arrowData.startTime = time + arrow.s.arrowData.isDying = false + arrow.s.arrowData.whizby = false // hack until we get a new model/shader for the whizby indicator + arrow.s.arrowData.attacker = attacker + + if ( !arrow.s.arrowData.isVisible ) + { + entity cockpit = player.GetCockpit() + + if ( !cockpit ) + return + + arrow.s.arrowData.isVisible = true + arrow.EnableDraw() + + arrow.DisableRenderWithViewModelsNoZoom() + arrow.EnableRenderWithCockpit() + + arrow.EnableRenderWithHud() + + arrow.SetParent( cockpit, "CAMERA_BASE" ) + arrow.SetAttachOffsetOrigin( < 20.0, 0.0, -2.0 > ) + } + + + PulseDamageArrow( arrow, arrowType ) + UpdateDamageArrowVars( player ) + UpdateDamageArrowAngle( arrowData ) +} + + +function PulseDamageArrow( entity arrow, int arrowType ) +{ + arrow.Anim_NonScriptedPlay( file.arrowIncomingAnims[ arrowType ].anim ) +} + +function UpdateDamageArrowVars( entity localViewPlayer ) +{ + file.damageArrowTime = Time() + file.damageArrowAngles = AnglesInverse( localViewPlayer.EyeAngles() ) + file.damageArrowPointCenter = localViewPlayer.EyePosition() + ( localViewPlayer.GetViewVector() * 20.0 ) +} + +function UpdateDamageArrowAngle( arrowData ) +{ + if ( IsValid( arrowData.grenade ) ) + arrowData.damageOrigin = arrowData.grenade.GetOrigin() + + vector vecToDamage = expect vector( arrowData.damageOrigin ) - file.damageArrowPointCenter + vector anglesToDamage = VectorToAngles( vecToDamage ) + vector eyeAngles = GetLocalViewPlayer().EyeAngles() + + float roll = sin( DegToRad( eyeAngles.y - anglesToDamage.y ) ) + + arrowData.arrow.SetAttachOffsetAngles( AnglesCompose( file.damageArrowAngles, anglesToDamage ) + < arrowData.randomAngle, 0, roll * 90.0 > ) + arrowData.damageDirection = Normalize( vecToDamage ) +} + +function UpdateDamageArrows( entity localViewPlayer, entity cockpit ) +{ + localViewPlayer.EndSignal( "OnDestroy" ) + cockpit.EndSignal( "OnDestroy" ) + + OnThreadEnd( + function() : ( localViewPlayer ) + { + foreach ( arrowData in file.damageArrows ) + { + if ( IsValid( arrowData.arrow ) ) + { + arrowData.arrow.DisableDraw() + arrowData.arrow.ClearParent() + arrowData.attacker = null + arrowData.isVisible = false + arrowData.randomAngle = 0 + } + } + } + ) + + bool varsUpdated = false + + while ( true ) + { + WaitEndFrame() + + vector playerOrigin = localViewPlayer.GetOrigin() + + varsUpdated = false + bool inPhaseShift = localViewPlayer.IsPhaseShifted() + + foreach ( arrowData in file.damageArrows ) + { + if ( !arrowData.isVisible ) + { + continue + } + + if ( arrowData.grenade != null ) + { + if ( !IsValid( arrowData.grenade ) ) + arrowData.endTime = 0.0 + } + + if ( (file.damageArrowTime >= arrowData.endTime) || inPhaseShift ) + { + arrowData.arrow.DisableDraw() + arrowData.arrow.ClearParent() + arrowData.attacker = null + arrowData.isVisible = false + arrowData.randomAngle = 0 + continue + } + + if ( !varsUpdated ) // only call UpdateDamageArrowVars if one or more of the file.damageArrows is visible + { + varsUpdated = true + UpdateDamageArrowVars( localViewPlayer ) + } + + UpdateDamageArrowAngle( arrowData ) + + if ( !arrowData.isDying && ( ( arrowData.endTime - file.damageArrowTime ) <= file.damageArrowFadeDuration ) ) + { + arrowData.isDying = true + arrowData.arrow.Anim_NonScriptedPlay( DAMAGEARROW_FADEANIM ) + } + } + + wait( 0.0 ) + } +} diff --git a/Northstar.Client/mod/scripts/vscripts/client/cl_screenfade.gnut b/Northstar.Client/mod/scripts/vscripts/client/cl_screenfade.gnut index 9844b65b..deccbac2 100644 --- a/Northstar.Client/mod/scripts/vscripts/client/cl_screenfade.gnut +++ b/Northstar.Client/mod/scripts/vscripts/client/cl_screenfade.gnut @@ -247,8 +247,10 @@ void function PlayerPainSoundThread() ourPlayer = localViewPlayer; soundLayer2Start = GetPainSound( ourPlayer, "sound_pain_layer2_start" ) soundLayer2Loop = GetPainSound( ourPlayer, "sound_pain_layer2_loop" ) - EmitSoundOnEntity( ourPlayer, soundLayer2Start ) - EmitSoundOnEntity( ourPlayer, soundLayer2Loop ) + if ( soundLayer2Start != "" ) + EmitSoundOnEntity( ourPlayer, soundLayer2Start ) + if ( soundLayer2Loop != "" ) + EmitSoundOnEntity( ourPlayer, soundLayer2Loop ) } else { @@ -274,7 +276,8 @@ void function PlayerPainSoundThread() ourPlayer = localViewPlayer soundLayer3Loop = GetPainSound( ourPlayer, "sound_pain_layer3_loop" ) soundLayer3End = GetPainSound( ourPlayer, "sound_pain_layer3_end" ) - EmitSoundOnEntity( ourPlayer, soundLayer3Loop ) + if ( soundLayer3Loop != "" ) + EmitSoundOnEntity( ourPlayer, soundLayer3Loop ) } else { diff --git a/Northstar.Client/mod/scripts/vscripts/ui/menu_ingame.nut b/Northstar.Client/mod/scripts/vscripts/ui/menu_ingame.nut index 35c9e9ba..ac617a9c 100644 --- a/Northstar.Client/mod/scripts/vscripts/ui/menu_ingame.nut +++ b/Northstar.Client/mod/scripts/vscripts/ui/menu_ingame.nut @@ -85,6 +85,9 @@ void function InitInGameMPMenu() var gameHeader = AddComboButtonHeader( comboStruct, headerIndex, "#MENU_HEADER_GAME" ) var leaveButton = AddComboButton( comboStruct, headerIndex, buttonIndex++, "#LEAVE_MATCH" ) Hud_AddEventHandler( leaveButton, UIE_CLICK, OnLeaveButton_Activate ) + var teamChangeButton = AddComboButton( comboStruct, headerIndex, buttonIndex++, "#SWITCH_TEAMS" ) + Hud_AddEventHandler( teamChangeButton, UIE_CLICK, OnRequestTeamSwitch ) + thread UpdateTeamSwitchButton_Threaded( teamChangeButton ) #if DEV var devButton = AddComboButton( comboStruct, headerIndex, buttonIndex++, "Dev" ) Hud_AddEventHandler( devButton, UIE_CLICK, AdvanceMenuEventHandler( GetMenu( "DevMenu" ) ) ) @@ -700,3 +703,21 @@ void function SetTitanSelectButtonVisibleState( bool state ) Hud_Hide( file.titanSelectButton ) } } + +void function UpdateTeamSwitchButton_Threaded( var button ) +{ + while ( true ) + { + Hud_SetLocked( button, !GetConVarBool( "ns_allow_team_change" ) ) + wait 0.5 + } +} + +void function OnRequestTeamSwitch( var button ) +{ + if ( !Hud_IsLocked( button ) ) + { + ClientCommand( "changeteam" ) + CloseAllMenus() + } +} diff --git a/Northstar.Client/mod/scripts/vscripts/ui/menu_ns_moddownload.nut b/Northstar.Client/mod/scripts/vscripts/ui/menu_ns_moddownload.nut index 4968714c..09001f57 100644 --- a/Northstar.Client/mod/scripts/vscripts/ui/menu_ns_moddownload.nut +++ b/Northstar.Client/mod/scripts/vscripts/ui/menu_ns_moddownload.nut @@ -9,6 +9,7 @@ global enum eModInstallStatus CHECKSUMING, EXTRACTING, DONE, + ABORTED, FAILED, FAILED_READING_ARCHIVE, FAILED_WRITING_TO_DISK, @@ -57,6 +58,11 @@ bool function DownloadMod( RequiredModInfo mod ) dialogData.message = Localize( "#DOWNLOADING_MOD_TEXT", mod.name, mod.version ) dialogData.showSpinner = true; + // Prevent download button + AddDialogButton( dialogData, "#CANCEL", void function() { + NSCancelModDownload() + }) + // Prevent user from closing dialog dialogData.forceChoice = true; OpenDialog( dialogData ) @@ -77,6 +83,13 @@ bool function DownloadMod( RequiredModInfo mod ) WaitFrame() } + // If download was aborted, don't close UI since it was closed by clicking cancel button + if ( state.status == eModInstallStatus.ABORTED ) + { + print("Mod download was cancelled by the user.") + return false; + } + printt( "Mod status:", state.status ) // Close loading dialog @@ -114,6 +127,12 @@ void function DisplayModDownloadErrorDialog( string modName ) { ModInstallState state = NSGetModInstallState() + // If user cancelled download, no need to display an error message + if ( state.status == eModInstallStatus.ABORTED ) + { + return + } + DialogData dialogData dialogData.header = Localize( "#FAILED_DOWNLOADING", modName ) dialogData.image = $"ui/menu/common/dialog_error" diff --git a/Northstar.Client/mod/scripts/vscripts/ui/menu_ns_modmenu.nut b/Northstar.Client/mod/scripts/vscripts/ui/menu_ns_modmenu.nut index f08d69a7..67a18431 100644 --- a/Northstar.Client/mod/scripts/vscripts/ui/menu_ns_modmenu.nut +++ b/Northstar.Client/mod/scripts/vscripts/ui/menu_ns_modmenu.nut @@ -5,17 +5,8 @@ global function AddNorthstarModMenu_MainMenuFooter global function ReloadMods -struct modData { - string name = "" - string version = "" - string link = "" - int loadPriority = 0 - bool enabled = false - array<string> conVars = [] -} - struct panelContent { - modData& mod + ModInfo& mod bool isHeader = false } @@ -37,10 +28,10 @@ struct { var menu array<var> panels int scrollOffset = 0 - array<string> enabledMods + array<ModInfo> enabledMods var currentButton string searchTerm - modData& lastMod + ModInfo& lastMod } file const int PANELS_LEN = 15 @@ -150,11 +141,22 @@ void function OnModMenuClosed() } catch ( ex ) {} - array<string> current = GetEnabledModsArray() + array<ModInfo> current = GetEnabledModsArray() bool reload - foreach ( string mod in current ) + foreach ( ModInfo mod in current ) { - if ( file.enabledMods.find(mod) == -1 ) + bool notFound = true + + foreach ( ModInfo enMod in file.enabledMods ) + { + if ( mod.name == enMod.name ) + { + notFound = false + break + } + } + + if ( notFound ) { reload = true break @@ -176,10 +178,10 @@ void function OnModButtonFocused( var button ) RuiSetGameTime( rui, "startTime", -99999.99 ) // make sure it skips the whole animation for showing this RuiSetString( rui, "headerText", modName ) - RuiSetString( rui, "messageText", FormatModDescription( modName ) ) + RuiSetString( rui, "messageText", FormatModDescription() ) // Add a button to open the link with if required - string link = NSGetModDownloadLinkByModName( modName ) + string link = file.lastMod.downloadLink var linkButton = Hud_GetChild( file.menu, "ModPageButton" ) if ( link.len() ) { @@ -193,28 +195,44 @@ void function OnModButtonFocused( var button ) Hud_SetVisible( linkButton, false ) } - SetControlBarColor( modName ) + SetControlBarColor( file.lastMod ) - bool required = NSIsModRequiredOnClient( modName ) + bool required = file.lastMod.requiredOnClient Hud_SetVisible( Hud_GetChild( file.menu, "WarningLegendLabel" ), required ) Hud_SetVisible( Hud_GetChild( file.menu, "WarningLegendImage" ), required ) } void function OnModButtonPressed( var button ) { - string modName = file.mods[ int ( Hud_GetScriptID( Hud_GetParent( button ) ) ) + file.scrollOffset - 1 ].mod.name - if ( StaticFind( modName ) && NSIsModEnabled( modName ) ) + ModInfo mod = file.mods[ int ( Hud_GetScriptID( Hud_GetParent( button ) ) ) + file.scrollOffset - 1 ].mod + string modName = mod.name + if ( StaticFind( modName ) && mod.enabled ) CoreModToggleDialog( modName ) else { - NSSetModEnabled( modName, !NSIsModEnabled( modName ) ) - var panel = file.panels[ int ( Hud_GetScriptID( Hud_GetParent( button ) ) ) - 1 ] - SetControlBoxColor( Hud_GetChild( panel, "ControlBox" ), modName ) - SetControlBarColor( modName ) - SetModEnabledHelperImageAsset( Hud_GetChild( panel, "EnabledImage" ), modName ) - // RefreshMods() - UpdateListSliderPosition() - UpdateListSliderHeight() + NSSetModEnabled( modName, !mod.enabled ) + + // retrieve state of the mod that just got toggled + array<ModInfo> infos = NSGetModInformation( mod.name ) + foreach ( modInfo in infos ) + { + if ( modInfo.name != modName || modInfo.version != mod.version ) + { + continue + } + + // Update UI mod state + file.mods[ int ( Hud_GetScriptID( Hud_GetParent( button ) ) ) + file.scrollOffset - 1 ].mod = modInfo + + var panel = file.panels[ int ( Hud_GetScriptID( Hud_GetParent( button ) ) ) - 1 ] + SetControlBoxColor( Hud_GetChild( panel, "ControlBox" ), modInfo ) + SetControlBarColor( modInfo ) + SetModEnabledHelperImageAsset( Hud_GetChild( panel, "EnabledImage" ), modInfo ) + // RefreshMods() + UpdateListSliderPosition() + UpdateListSliderHeight() + break + } } } @@ -230,8 +248,8 @@ void function OnAuthenticationAgreementButtonPressed( var button ) void function OnModLinkButtonPressed( var button ) { - string modName = file.mods[ int ( Hud_GetScriptID( Hud_GetParent( file.currentButton ) ) ) + file.scrollOffset - 1 ].mod.name - string link = NSGetModDownloadLinkByModName( modName ) + ModInfo mod = file.mods[ int ( Hud_GetScriptID( Hud_GetParent( file.currentButton ) ) ) + file.scrollOffset - 1 ].mod + string link = mod.downloadLink if ( link.find("http://") != 0 && link.find("https://") != 0 ) link = "http://" + link // links without the http or https protocol get opened in the internal browser LaunchExternalWebBrowser( link, WEBBROWSER_FLAG_FORCEEXTERNAL ) @@ -261,7 +279,7 @@ void function OnHideConVarsChange( var n ) if ( modName == "" ) return var rui = Hud_GetRui( Hud_GetChild( file.menu, "LabelDetails" ) ) - RuiSetString( rui, "messageText", FormatModDescription( modName ) ) + RuiSetString( rui, "messageText", FormatModDescription() ) } // LIST LOGIC @@ -284,23 +302,35 @@ void function CoreModToggleDialog( string mod ) void function DisableMod() { - string modName = file.mods[ int ( Hud_GetScriptID( Hud_GetParent( file.currentButton ) ) ) + file.scrollOffset - 1 ].mod.name + ModInfo mod = file.mods[ int ( Hud_GetScriptID( Hud_GetParent( file.currentButton ) ) ) + file.scrollOffset - 1 ].mod + string modName = mod.name NSSetModEnabled( modName, false ) - var panel = file.panels[ int ( Hud_GetScriptID( Hud_GetParent( file.currentButton ) ) ) - 1] - SetControlBoxColor( Hud_GetChild( panel, "ControlBox" ), modName ) - SetControlBarColor( modName ) - SetModEnabledHelperImageAsset( Hud_GetChild( panel, "EnabledImage" ), modName ) + // retrieve state of the mod that just got toggled + array<ModInfo> infos = NSGetModInformation( mod.name ) + foreach ( modInfo in infos ) + { + if ( modInfo.name != modName || modInfo.version != mod.version ) + { + continue + } + + var panel = file.panels[ int ( Hud_GetScriptID( Hud_GetParent( file.currentButton ) ) ) - 1] + SetControlBoxColor( Hud_GetChild( panel, "ControlBox" ), modInfo ) + SetControlBarColor( modInfo ) + SetModEnabledHelperImageAsset( Hud_GetChild( panel, "EnabledImage" ), modInfo ) - RefreshMods() + RefreshMods() + break + } } -array<string> function GetEnabledModsArray() +array<ModInfo> function GetEnabledModsArray() { - array<string> enabledMods - foreach ( string mod in NSGetModNames() ) + array<ModInfo> enabledMods + foreach ( ModInfo mod in NSGetModsInformation() ) { - if ( NSIsModEnabled( mod ) ) + if ( mod.enabled ) enabledMods.append( mod ) } return enabledMods @@ -324,29 +354,30 @@ void function UpdateList() void function RefreshMods() { - array<string> modNames = NSGetModNames() + array<ModInfo> mods = NSGetModsInformation() file.mods.clear() bool reverse = GetConVarBool( "modlist_reverse" ) - int lastLoadPriority = reverse ? NSGetModLoadPriority( modNames[ modNames.len() - 1 ] ) + 1 : -1 + int lastLoadPriority = reverse ? mods.top().loadPriority + 1 : -1 string searchTerm = Hud_GetUTF8Text( Hud_GetChild( file.menu, "BtnModsSearch" ) ).tolower() - for ( int i = reverse ? modNames.len() - 1 : 0; - reverse ? ( i >= 0 ) : ( i < modNames.len() ); + for ( int i = reverse ? mods.len() - 1 : 0; + reverse ? ( i >= 0 ) : ( i < mods.len() ); i += ( reverse ? -1 : 1) ) { - string mod = modNames[i] + ModInfo mod = mods[i] + string modName = mod.name // Do not display remote mods - if ( NSIsModRemote( mod ) ) + if ( mod.isRemote ) continue - if ( searchTerm.len() && mod.tolower().find( searchTerm ) == null ) + if ( searchTerm.len() && modName.tolower().find( searchTerm ) == null ) continue - bool enabled = NSIsModEnabled( mod ) - bool required = NSIsModRequiredOnClient( mod ) + bool enabled = mod.enabled + bool required = mod.requiredOnClient switch ( GetConVarInt( "filter_mods" ) ) { case filterShow.ONLY_ENABLED: @@ -367,11 +398,11 @@ void function RefreshMods() break } - int pr = NSGetModLoadPriority( mod ) + int pr = mod.loadPriority if ( reverse ? pr < lastLoadPriority : pr > lastLoadPriority ) { - modData m + ModInfo m m.name = pr.tostring() panelContent c @@ -381,16 +412,8 @@ void function RefreshMods() lastLoadPriority = pr } - modData m - m.name = mod - m.version = NSGetModVersionByModName( mod ) - m.link = NSGetModDownloadLinkByModName( mod ) - m.loadPriority = NSGetModLoadPriority( mod ) - m.enabled = enabled - m.conVars = NSGetModConvarsByModName( mod ) - panelContent c - c.mod = m + c.mod = mod file.mods.append( c ) } @@ -404,7 +427,7 @@ void function DisplayModPanels() break panelContent c = file.mods[ file.scrollOffset + i ] - modData mod = c.mod + ModInfo mod = c.mod var btn = Hud_GetChild( panel, "BtnMod" ) var headerLabel = Hud_GetChild( panel, "Header" ) var box = Hud_GetChild( panel, "ControlBox" ) @@ -434,53 +457,45 @@ void function DisplayModPanels() Hud_SetVisible( headerLabel, false ) - SetControlBoxColor( box, mod.name ) + SetControlBoxColor( box, mod ) Hud_SetVisible( box, true ) Hud_SetVisible( line, false ) - Hud_SetVisible( warning, NSIsModRequiredOnClient( c.mod.name ) ) + Hud_SetVisible( warning, mod.requiredOnClient ) - SetModEnabledHelperImageAsset( enabledImage, c.mod.name ) + SetModEnabledHelperImageAsset( enabledImage, c.mod ) } Hud_SetVisible( panel, true ) } } -void function SetModEnabledHelperImageAsset( var panel, string modName ) +void function SetModEnabledHelperImageAsset( var panel, ModInfo mod ) { - if( NSIsModEnabled( modName ) ) + if( mod.enabled ) RuiSetImage( Hud_GetRui( panel ), "basicImage", $"rui/menu/common/merit_state_success" ) else RuiSetImage( Hud_GetRui( panel ), "basicImage", $"rui/menu/common/merit_state_failure" ) - RuiSetFloat3(Hud_GetRui( panel ), "basicImageColor", GetControlColorForMod( modName ) ) + RuiSetFloat3(Hud_GetRui( panel ), "basicImageColor", GetControlColorForMod( mod ) ) Hud_SetVisible( panel, true ) } -void function SetControlBoxColor( var box, string modName ) +void function SetControlBoxColor( var box, ModInfo mod ) { var rui = Hud_GetRui( box ) - // if ( NSIsModEnabled( modName ) ) - // RuiSetFloat3(rui, "basicImageColor", <0,1,0>) - // else - // RuiSetFloat3(rui, "basicImageColor", <1,0,0>) - RuiSetFloat3(rui, "basicImageColor", GetControlColorForMod( modName ) ) + RuiSetFloat3(rui, "basicImageColor", GetControlColorForMod( mod ) ) } -void function SetControlBarColor( string modName ) +void function SetControlBarColor( ModInfo mod ) { var bar_element = Hud_GetChild( file.menu, "ModEnabledBar" ) var bar = Hud_GetRui( bar_element ) - // if ( NSIsModEnabled( modName ) ) - // RuiSetFloat3(bar, "basicImageColor", <0,1,0>) - // else - // RuiSetFloat3(bar, "basicImageColor", <1,0,0>) - RuiSetFloat3(bar, "basicImageColor", GetControlColorForMod( modName ) ) + RuiSetFloat3(bar, "basicImageColor", GetControlColorForMod( mod ) ) Hud_SetVisible( bar_element, true ) } -vector function GetControlColorForMod( string modName ) +vector function GetControlColorForMod( ModInfo mod ) { - if ( NSIsModEnabled( modName ) ) + if ( mod.enabled ) switch ( GetConVarInt( "colorblind_mode" ) ) { case 1: @@ -502,17 +517,20 @@ vector function GetControlColorForMod( string modName ) unreachable } -string function FormatModDescription( string modName ) +string function FormatModDescription() { + ModInfo mod = file.lastMod + string modName = mod.name + string ret // version - ret += format( "Version %s\n", NSGetModVersionByModName( modName ) ) + ret += format( "Version %s\n", mod.version ) // load priority - ret += format( "Load Priority: %i\n", NSGetModLoadPriority( modName ) ) + ret += format( "Load Priority: %i\n", mod.loadPriority ) // convars - array<string> modCvars = NSGetModConvarsByModName( modName ) + array<string> modCvars = mod.conVars if ( modCvars.len() != 0 && GetConVarBool( "modlist_show_convars" ) ) { ret += "ConVars: " @@ -529,7 +547,7 @@ string function FormatModDescription( string modName ) } // description - ret += format( "\n%s\n", NSGetModDescriptionByModName( modName ) ) + ret += format( "\n%s\n", mod.description ) return ret } diff --git a/Northstar.Client/mod/scripts/vscripts/ui/menu_ns_serverbrowser.nut b/Northstar.Client/mod/scripts/vscripts/ui/menu_ns_serverbrowser.nut index cdeb8b3e..f2effd12 100644 --- a/Northstar.Client/mod/scripts/vscripts/ui/menu_ns_serverbrowser.nut +++ b/Northstar.Client/mod/scripts/vscripts/ui/menu_ns_serverbrowser.nut @@ -973,18 +973,27 @@ void function OnServerSelected_Threaded( var button ) foreach ( requiredModInfo in server.requiredMods ) { // Tolerate core mods having different versions - if ( requiredModInfo.name.len() > 10 && requiredModInfo.name.slice(0, 10) == "Northstar." ) + if ( IsCoreMod( requiredModInfo.name ) ) continue if ( !modNames.contains( requiredModInfo.name ) ) { - print( format ( "\"%s\" was not found locally, triggering manifesto fetching.", requiredModInfo.name ) ) - uninstalledModFound = true - break - } else if ( NSGetModVersionByModName( requiredModInfo.name ) != requiredModInfo.version ) { - print( format ( "\"%s\" was found locally but has version \"%s\" while server requires \"%s\", triggering manifesto fetching.", requiredModInfo.name, NSGetModVersionByModName( requiredModInfo.name ), requiredModInfo.version ) ) + print( format ( "\"%s\" was not found locally" + ( autoDownloadAllowed ? ", triggering manifesto fetching." : "." ), requiredModInfo.name ) ) uninstalledModFound = true break + } else { + array<string> modVersions = GetModVersions( requiredModInfo.name ) + + if ( !modVersions.contains( requiredModInfo.version ) ) { + print( format ( "\"%s\" was found locally but has versions:", requiredModInfo.name ) ) + foreach ( string version in modVersions ) + { + print(" - " + version) + } + print( format ( "while server requires \"%s\"" + ( autoDownloadAllowed ? ", triggering manifesto fetching." : "." ), requiredModInfo.version ) ) + uninstalledModFound = true + break + } } } @@ -992,17 +1001,16 @@ void function OnServerSelected_Threaded( var button ) // mods can be installed through auto-download if ( uninstalledModFound && autoDownloadAllowed ) { - print("Auto-download is allowed, checking if missing mods can be installed automatically.") FetchVerifiedModsManifesto() } foreach ( RequiredModInfo mod in server.requiredMods ) { // Tolerate core mods having different versions - if ( mod.name.len() > 10 && mod.name.slice(0, 10) == "Northstar." ) + if ( IsCoreMod( mod.name ) ) continue - if ( !NSGetModNames().contains( mod.name ) || NSGetModVersionByModName( mod.name ) != mod.version ) + if ( !NSGetModNames().contains( mod.name ) || !GetModVersions( mod.name ).contains( mod.version ) ) { // Auto-download mod if ( autoDownloadAllowed ) @@ -1055,43 +1063,8 @@ void function OnServerSelected_Threaded( var button ) return } } - else - { - // this uses semver https://semver.org - array<string> serverModVersion = split( mod.name, "." ) - array<string> clientModVersion = split( NSGetModVersionByModName( mod.name ), "." ) - - bool semverFail = false - // if server has invalid semver don't bother checking - if ( serverModVersion.len() == 3 ) - { - // bad client semver - if ( clientModVersion.len() != serverModVersion.len() ) - semverFail = true - // major version, don't think we should need to check other versions - else if ( clientModVersion[ 0 ] != serverModVersion[ 0 ] ) - semverFail = true - } - - if ( semverFail ) - { - DialogData dialogData - dialogData.header = "#ERROR" - dialogData.message = Localize( "#WRONG_MOD_VERSION", mod.name, mod.version, NSGetModVersionByModName( mod.name ) ) - dialogData.image = $"ui/menu/common/dialog_error" - - #if PC_PROG - AddDialogButton( dialogData, "#DISMISS" ) - AddDialogFooter( dialogData, "#A_BUTTON_SELECT" ) - #endif // PC_PROG - AddDialogFooter( dialogData, "#B_BUTTON_DISMISS_RUI" ) - - OpenDialog( dialogData ) - - return - } - } + // If we get here, means that mod version exists locally => we good } if ( server.requiresPassword ) @@ -1136,25 +1109,31 @@ void function ThreadedAuthAndConnectToServer( string password = "", bool modsCha if ( NSWasAuthSuccessful() ) { // disable all RequiredOnClient mods that are not required by the server and are currently enabled - foreach ( string modName in NSGetModNames() ) + foreach ( ModInfo mod in NSGetModsInformation() ) { - if ( NSIsModRequiredOnClient( modName ) && NSIsModEnabled( modName ) ) + string modName = mod.name + string modVersion = mod.version + + if ( mod.requiredOnClient && mod.enabled ) { // find the mod name in the list of server required mods bool found = false foreach ( RequiredModInfo mod in file.lastSelectedServer.requiredMods ) { - if (mod.name == modName) + // this tolerates a version difference for requiredOnClient core mods (only Northstar.Custom for now) + if (mod.name == modName && ( IsCoreMod( modName ) || mod.version == modVersion )) { found = true + print(format("\"%s\" (v%s) is required and already enabled.", modName, modVersion)) break } } - // if we didnt find the mod name, disable the mod + // if we didn't find the mod name, disable the mod if (!found) { modsChanged = true NSSetModEnabled( modName, false ) + print(format("Disabled \"%s\" (v%s) since it's not required on server.", modName, modVersion)) } } } @@ -1162,10 +1141,33 @@ void function ThreadedAuthAndConnectToServer( string password = "", bool modsCha // enable all RequiredOnClient mods that are required by the server and are currently disabled foreach ( RequiredModInfo mod in file.lastSelectedServer.requiredMods ) { - if ( NSIsModRequiredOnClient( mod.name ) && !NSIsModEnabled( mod.name )) + string modName = mod.name + string modVersion = mod.version + array<ModInfo> localModInfos = NSGetModInformation( modName ) + + // Tolerate core mods (only Northstar.Custom for now) having a different version than server + if ( IsCoreMod(modName) ) + { + if ( !localModInfos[0].enabled ) + { + modsChanged = true + NSSetModEnabled( modName, true ) + print(format("Enabled \"%s\" (v%s) to join server.", modName, localModInfos[0].version)) + } + } + + else { - modsChanged = true - NSSetModEnabled( mod.name, true ) + foreach( localMod in localModInfos ) + { + if ( localMod.version == mod.version ) + { + modsChanged = true + NSSetModEnabled( mod.name, true ) + print(format("Enabled \"%s\" (v%s) to join server.", modName, modVersion)) + break + } + } } } @@ -1361,3 +1363,19 @@ void function TriggerConnectToServerCallbacks( ServerInfo ornull targetServer = callback( expect ServerInfo( targetServer ) ) } } + +const array<string> CORE_MODS = ["Northstar.Client", "Northstar.Coop", "Northstar.CustomServers", "Northstar.Custom"] +bool function IsCoreMod( string modName ) +{ + return CORE_MODS.find( modName ) != -1 +} + +array<string> function GetModVersions( string modName ) +{ + array<string> versions = [] + foreach ( ModInfo mod in NSGetModInformation( modName ) ) + { + versions.append( mod.version ) + } + return versions +} diff --git a/Northstar.Client/mod/scripts/vscripts/ui/menu_ns_setversionlabel.nut b/Northstar.Client/mod/scripts/vscripts/ui/menu_ns_setversionlabel.nut index 6dbafde9..afba8a70 100644 --- a/Northstar.Client/mod/scripts/vscripts/ui/menu_ns_setversionlabel.nut +++ b/Northstar.Client/mod/scripts/vscripts/ui/menu_ns_setversionlabel.nut @@ -5,6 +5,6 @@ void function NS_SetVersionLabel() { var mainMenu = GetMenu( "MainMenu" ) //Gets main menu element var versionLabel = GetElementsByClassname( mainMenu, "nsVersionClass" )[0] //Gets the label from the mainMenu element. - Hud_SetText( versionLabel, "v" + NSGetModVersionByModName("Northstar.Client")) //Sets the label text (Getting Northstar version from Northstar.Client) + Hud_SetText( versionLabel, "v" + NSGetModInformation( "Northstar.Client" )[0].version ) //Sets the label text (Getting Northstar version from Northstar.Client) } diff --git a/Northstar.Client/mod/scripts/vscripts/ui/panel_mainmenu.nut b/Northstar.Client/mod/scripts/vscripts/ui/panel_mainmenu.nut index 2f1bcf02..330cd0d6 100644 --- a/Northstar.Client/mod/scripts/vscripts/ui/panel_mainmenu.nut +++ b/Northstar.Client/mod/scripts/vscripts/ui/panel_mainmenu.nut @@ -431,9 +431,9 @@ void function UpdatePlayButton( var button ) { // restrict non-vanilla players from accessing official servers bool hasNonVanillaMods = false - foreach ( string modName in NSGetModNames() ) + foreach ( ModInfo mod in NSGetModsInformation() ) { - if ( NSIsModEnabled( modName ) && NSIsModRequiredOnClient( modName ) ) + if ( mod.enabled && mod.requiredOnClient ) { hasNonVanillaMods = true break diff --git a/Northstar.Custom/mod/scripts/vscripts/gamemodes/_gamemode_hidden.nut b/Northstar.Custom/mod/scripts/vscripts/gamemodes/_gamemode_hidden.nut index 6729ff97..c3bdd484 100644 --- a/Northstar.Custom/mod/scripts/vscripts/gamemodes/_gamemode_hidden.nut +++ b/Northstar.Custom/mod/scripts/vscripts/gamemodes/_gamemode_hidden.nut @@ -11,6 +11,7 @@ void function GamemodeHidden_Init() SetLoadoutGracePeriodEnabled( false ) // prevent modifying loadouts with grace period
SetWeaponDropsEnabled( false )
SetRespawnsEnabled( false )
+ SetGamemodeAllowsTeamSwitch( false )
Riff_ForceTitanAvailability( eTitanAvailability.Never )
Riff_ForceBoostAvailability( eBoostAvailability.Disabled )
Riff_ForceSetEliminationMode( eEliminationMode.Pilots )
diff --git a/Northstar.Custom/mod/scripts/vscripts/gamemodes/_gamemode_inf.gnut b/Northstar.Custom/mod/scripts/vscripts/gamemodes/_gamemode_inf.gnut index 02f0799a..e03f01ad 100644 --- a/Northstar.Custom/mod/scripts/vscripts/gamemodes/_gamemode_inf.gnut +++ b/Northstar.Custom/mod/scripts/vscripts/gamemodes/_gamemode_inf.gnut @@ -11,6 +11,7 @@ void function GamemodeInfection_Init() SetSpawnpointGamemodeOverride( FFA )
SetLoadoutGracePeriodEnabled( false ) // prevent modifying loadouts with grace period
SetWeaponDropsEnabled( false )
+ SetGamemodeAllowsTeamSwitch( false )
SetShouldUseRoundWinningKillReplay( true )
Riff_ForceTitanAvailability( eTitanAvailability.Never )
Riff_ForceBoostAvailability( eBoostAvailability.Disabled )
diff --git a/Northstar.Custom/mod/scripts/vscripts/sh_message_utils.gnut b/Northstar.Custom/mod/scripts/vscripts/sh_message_utils.gnut index 4cfdc6fb..6bc1718a 100644 --- a/Northstar.Custom/mod/scripts/vscripts/sh_message_utils.gnut +++ b/Northstar.Custom/mod/scripts/vscripts/sh_message_utils.gnut @@ -185,13 +185,14 @@ void function NSSendPopUpMessageToPlayer( entity player, string text ) ServerToClientStringCommand( player, "ServerHUDMessageShow " + eMessageType.POPUP ) } -void function NSSendAnnouncementMessageToPlayer( entity player, string title, string description, vector color, int priority, int style ) +void function NSSendAnnouncementMessageToPlayer( entity player, string title, string description, vector color, int priority, int style, string image = "" ) { ServerToClientStringCommand( player, "ServerHUDMessagePut " + eDataType.TITLE + " " + title ) ServerToClientStringCommand( player, "ServerHUDMessagePut " + eDataType.DESC + " " + description ) ServerToClientStringCommand( player, "ServerHUDMessagePut " + eDataType.COLOR + " " + color.x + " " + color.y + " " + color.z ) ServerToClientStringCommand( player, "ServerHUDMessagePut " + eDataType.PRIORITY + " " + priority ) ServerToClientStringCommand( player, "ServerHUDMessagePut " + eDataType.STYLE + " " + style ) + ServerToClientStringCommand( player, "ServerHUDMessagePut " + eDataType.ASSET + " " + image ) ServerToClientStringCommand( player, "ServerHUDMessageShow " + eMessageType.ANNOUNCEMENT ) } @@ -453,6 +454,7 @@ void function AnnouncementMessageHandler_Threaded() Announcement_SetPriority( announcement, client.announcementQueue[0].priority ) Announcement_SetSoundAlias( announcement, SFX_HUD_ANNOUNCE_QUICK ) Announcement_SetStyle( announcement, client.announcementQueue[0].style ) + Announcement_SetIcon( announcement, StringToAsset( strip( client.announcementQueue[0].image ) ) ) AnnouncementFromClass( GetLocalViewPlayer(), announcement ) wait 5 diff --git a/Northstar.Custom/mod/scripts/weapons/mp_titanweapon_arc_cannon.txt b/Northstar.Custom/mod/scripts/weapons/mp_titanweapon_arc_cannon.txt index 2672dca9..0ef3ed33 100644 --- a/Northstar.Custom/mod/scripts/weapons/mp_titanweapon_arc_cannon.txt +++ b/Northstar.Custom/mod/scripts/weapons/mp_titanweapon_arc_cannon.txt @@ -103,8 +103,11 @@ WeaponData "charge_effect_1p" "wpn_arc_cannon_charge_fp" "charge_effect_3p" "wpn_arc_cannon_charge" "charge_effect_attachment" "muzzle_flash" + "charge_effect2_1p" "wpn_ARC_knob_FP" + "charge_effect2_3p" "wpn_ARC_knob" + "charge_effect2_attachment" "SPINNING_KNOB" - + "charge_effect_show_during_drain" "0" // Spread "spread_stand_hip" "10" diff --git a/Northstar.CustomServers/mod.json b/Northstar.CustomServers/mod.json index fa51f4d4..6735306e 100644 --- a/Northstar.CustomServers/mod.json +++ b/Northstar.CustomServers/mod.json @@ -49,6 +49,11 @@ "Name": "ns_progression_enabled", "DefaultValue": "0", "Flags": "ARCHIVE_PLAYERPROFILE" + }, + { + "Name": "ns_allow_team_change", + "DefaultValue": "1", + "Flags": "REPLICATED" } ], "Scripts": [ diff --git a/Northstar.CustomServers/mod/scripts/vscripts/mp/_base_gametype_mp.gnut b/Northstar.CustomServers/mod/scripts/vscripts/mp/_base_gametype_mp.gnut index b77a37b2..6b4e82d6 100644 --- a/Northstar.CustomServers/mod/scripts/vscripts/mp/_base_gametype_mp.gnut +++ b/Northstar.CustomServers/mod/scripts/vscripts/mp/_base_gametype_mp.gnut @@ -18,6 +18,7 @@ global function TrackTitanDamageInPlayerGameStat global function ShouldEntTakeDamage_SPMP global function GetTitanBuildTime global function TitanPlayerHotDropsIntoLevel +global function SetGamemodeAllowsTeamSwitch global function SetRecalculateRespawnAsTitanStartPointCallback @@ -30,10 +31,13 @@ struct { array<entity> specCams entity functionref( entity player, entity basePoint ) recalculateRespawnAsTitanStartPointCallback + table<entity, float> playerChangeTeamTimeBuffer + bool gamemodeTeamSwitchEnabled = true } file void function BaseGametype_Init_MPSP() { + AddClientCommandCallback( "changeteam", ClientCommandCallbackChangeTeam ) AddSpawnCallback( "info_intermission", SetIntermissionCamera ) AddPostDamageCallback( "player", AddToTitanDamageStat ) @@ -630,6 +634,51 @@ void function SetRecalculateRespawnAsTitanStartPointCallback( entity functionref file.recalculateRespawnAsTitanStartPointCallback = callbackFunc } +void function SetGamemodeAllowsTeamSwitch( bool enabled ) +{ + file.gamemodeTeamSwitchEnabled = enabled +} + +bool function ClientCommandCallbackChangeTeam( entity player, array<string> args ) +{ + if ( !GetConVarBool( "ns_allow_team_change" ) || IsPrivateMatchSpectator( player ) ) + return true + + if ( !file.gamemodeTeamSwitchEnabled ) + { + SendHudMessage( player, "#TEAMSWITCH_GAMEMODE", -1, 0.4, 255, 255, 255, 255, 0.15, 3.0, 0.5 ) + return true + } + + if ( !( player in file.playerChangeTeamTimeBuffer ) ) + { + file.playerChangeTeamTimeBuffer[ player ] <- Time() + 5.0 + } + else + { + if ( file.playerChangeTeamTimeBuffer[ player ] > Time() ) + { + SendHudMessage( player, "#TEAMSWITCH_BUFFER", -1, 0.4, 255, 255, 255, 255, 0.15, 3.0, 0.5 ) + return true + } + } + + if ( player in file.playerChangeTeamTimeBuffer && file.playerChangeTeamTimeBuffer[ player ] < Time() ) + file.playerChangeTeamTimeBuffer[ player ] = Time() + 5.0 + + if ( !GamePlaying() ) + { + SendHudMessage( player, "#TEAMSWITCH_GAMEPLAY", -1, 0.4, 255, 255, 255, 255, 0.15, 3.0, 0.5 ) + return true + } + if ( GetCurrentPlaylistVarInt( "max_teams", 0 ) > 1 && !IsFFAGame() ) + SetTeam( player, GetOtherTeam( player.GetTeam() ) ) + else + SendHudMessage( player, "#TEAMSWITCH_DISABLED", -1, 0.4, 255, 255, 255, 255, 0.15, 3.0, 0.5 ) + + return true +} + // stuff to change later bool function ShouldEntTakeDamage_SPMP( entity ent, var damageInfo ) |