diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c8d876a7..629fd787 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -106,13 +106,13 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Remove old data files - run: ssh ${{ secrets.H1_MOD_MASTER_SSH_USER }}@${{ secrets.H1_MOD_MASTER_SSH_ADDRESS }} rm -rf ${{ env.H1_MOD_MASTER_PATH }}/h1-mod/data/* + run: ssh ${{ secrets.H1_MOD_MASTER_SSH_USER }}@${{ secrets.H1_MOD_MASTER_SSH_ADDRESS }} rm -rf ${{ env.H1_MOD_MASTER_PATH }}/h1-mod/* - name: Upload h1-mod binary run: rsync -avz h1-mod.exe ${{ secrets.H1_MOD_MASTER_SSH_USER }}@${{ secrets.H1_MOD_MASTER_SSH_ADDRESS }}:${{ env.H1_MOD_MASTER_PATH }}/h1-mod/ - name: Upload data files - run: rsync -avz ./data/ ${{ secrets.H1_MOD_MASTER_SSH_USER }}@${{ secrets.H1_MOD_MASTER_SSH_ADDRESS }}:${{ env.H1_MOD_MASTER_PATH }}/h1-mod/data/ + run: rsync -avz ./data/ ${{ secrets.H1_MOD_MASTER_SSH_USER }}@${{ secrets.H1_MOD_MASTER_SSH_ADDRESS }}:${{ env.H1_MOD_MASTER_PATH }}/h1-mod/ - name: Publish changes run: ssh ${{ secrets.H1_MOD_MASTER_SSH_USER }}@${{ secrets.H1_MOD_MASTER_SSH_ADDRESS }} ${{ secrets.H1_MOD_MASTER_SSH_CHANGE_PUBLISH_COMMAND }} diff --git a/.gitmodules b/.gitmodules index 2f46cb5f..a6a9bd10 100644 --- a/.gitmodules +++ b/.gitmodules @@ -25,10 +25,6 @@ [submodule "deps/lua"] path = deps/lua url = https://github.com/lua/lua.git -[submodule "deps/stb"] - path = deps/stb - url = https://github.com/nothings/stb.git - branch = develop [submodule "deps/libtomcrypt"] path = deps/libtomcrypt url = https://github.com/libtom/libtomcrypt.git @@ -48,3 +44,13 @@ [submodule "deps/curl"] path = deps/curl url = https://github.com/curl/curl.git +[submodule "deps/json"] + path = deps/json + url = https://github.com/nlohmann/json.git +[submodule "deps/gsc-tool"] + path = deps/gsc-tool + url = https://github.com/xensik/gsc-tool.git + branch = xlabs +[submodule "deps/stb"] + path = deps/stb + url = https://github.com/nothings/stb.git diff --git a/data/cdata/fallback/fonts/defaultBold.otf b/data/cdata/fallback/fonts/defaultBold.otf new file mode 100644 index 00000000..404a4f26 Binary files /dev/null and b/data/cdata/fallback/fonts/defaultBold.otf differ diff --git a/data/cdata/scripts/mp/team_balance.gsc b/data/cdata/scripts/mp/team_balance.gsc new file mode 100644 index 00000000..759aceb2 --- /dev/null +++ b/data/cdata/scripts/mp/team_balance.gsc @@ -0,0 +1,27 @@ +init() +{ + // define the auto balance string in the game array (referenced in gsc dump, but not defined past IW6?) + precachestring(&"MP_AUTOBALANCE_NOW"); + game["strings"]["autobalance"] = &"MP_AUTOBALANCE_NOW"; + + // define onteamselection callback function used in balanceteams() + level.onteamselection = ::set_team; +} + +set_team(team) +{ + if (team != self.pers["team"]) + { + self.switching_teams = true; + self.joining_team = team; + self.leaving_team = self.pers["team"]; + } + + if (self.sessionstate == "playing") + { + self suicide(); + } + + maps\mp\gametypes\_menus::addtoteam(team); + maps\mp\gametypes\_menus::endrespawnnotify(); +} diff --git a/data/ui_scripts/custom_depot/__init__.lua b/data/cdata/ui_scripts/custom_depot/__init__.lua similarity index 100% rename from data/ui_scripts/custom_depot/__init__.lua rename to data/cdata/ui_scripts/custom_depot/__init__.lua diff --git a/data/ui_scripts/custom_depot/depot_override.lua b/data/cdata/ui_scripts/custom_depot/depot_override.lua similarity index 100% rename from data/ui_scripts/custom_depot/depot_override.lua rename to data/cdata/ui_scripts/custom_depot/depot_override.lua diff --git a/data/cdata/ui_scripts/custom_depot/mod_eula.lua b/data/cdata/ui_scripts/custom_depot/mod_eula.lua new file mode 100644 index 00000000..6d5b597c --- /dev/null +++ b/data/cdata/ui_scripts/custom_depot/mod_eula.lua @@ -0,0 +1,13 @@ +local mod_eula = function(unk1, unk2) + return LUI.EULABase.new(CoD.CreateState(0, 0, 0, 0, CoD.AnchorTypes.All), { + textStrings = LUI.EULABase.CreateTextStrings("@CUSTOM_DEPOT_EULA_", 6), + declineCallback = function(unk3) + unk2.declineCallback(unk3) + end, + acceptCallback = function(unk4) + unk2.acceptCallback(unk4) + end + }) +end + +LUI.MenuBuilder.registerPopupType("mod_eula", mod_eula) diff --git a/data/ui_scripts/custom_depot/scoreboard_override.lua b/data/cdata/ui_scripts/custom_depot/scoreboard_override.lua similarity index 100% rename from data/ui_scripts/custom_depot/scoreboard_override.lua rename to data/cdata/ui_scripts/custom_depot/scoreboard_override.lua diff --git a/data/cdata/ui_scripts/discord/__init__.lua b/data/cdata/ui_scripts/discord/__init__.lua new file mode 100644 index 00000000..fe988a68 --- /dev/null +++ b/data/cdata/ui_scripts/discord/__init__.lua @@ -0,0 +1,272 @@ +if (game:issingleplayer() or Engine.InFrontend()) then + return +end + +local container = LUI.UIVerticalList.new({ + topAnchor = true, + rightAnchor = true, + top = 20, + right = 200, + width = 200, + spacing = 5 +}) + +function canasktojoin(userid) + history = history or {} + if (history[userid] ~= nil) then + return false + end + + history[userid] = true + game:ontimeout(function() + history[userid] = nil + end, 15000) + + return true +end + +function truncatename(name, length) + if (#name <= length - 3) then + return name + end + + return name:sub(1, length - 3) .. "..." +end + +function addrequest(request) + if (not canasktojoin(request.userid)) then + return + end + + if (container.temp) then + container:removeElement(container.temp) + container.temp = nil + end + + local invite = LUI.UIElement.new({ + leftAnchor = true, + rightAnchor = true, + height = 75 + }) + + invite:registerAnimationState("move_in", { + leftAnchor = true, + height = 75, + width = 200, + left = -220 + }) + + invite:animateToState("move_in", 100) + + local background = LUI.UIImage.new({ + topAnchor = true, + leftAnchor = true, + rightAnchor = true, + bottomAnchor = true, + top = 1, + left = 1, + bottom = -1, + right = -1, + material = RegisterMaterial("white"), + color = { + r = 0, + b = 0, + g = 0 + }, + alpha = 0.6 + }) + + local border = LUI.UIImage.new({ + topAnchor = true, + leftAnchor = true, + rightAnchor = true, + bottomAnchor = true, + material = RegisterMaterial("btn_focused_rect_innerglow") + }) + + border:setup9SliceImage(10, 5, 0.25, 0.12) + + local paddingvalue = 10 + local padding = LUI.UIElement.new({ + topAnchor = true, + leftAnchor = true, + rightAnchor = true, + bottomAnchor = true, + top = paddingvalue, + left = paddingvalue, + right = -paddingvalue, + bottom = -paddingvalue + }) + + local avatarmaterial = discord.getavatarmaterial(request.userid) + local avatar = LUI.UIImage.new({ + leftAnchor = true, + topAnchor = true, + width = 32, + height = 32, + left = 1, + material = RegisterMaterial(avatarmaterial) + }) + + local username = LUI.UIText.new({ + leftAnchor = true, + topAnchor = true, + height = 12, + left = 32 + paddingvalue, + color = Colors.white, + alignment = LUI.Alignment.Left, + rightAnchor = true, + font = CoD.TextSettings.BodyFontBold.Font + }) + + username:setText(string.format("%s^7#%s requested to join your game!", truncatename(request.username, 18), + request.discriminator)) + + local buttons = LUI.UIElement.new({ + leftAnchor = true, + rightAnchor = true, + topAnchor = true, + top = 37, + height = 18 + }) + + local createbutton = function(text, left) + local button = LUI.UIElement.new({ + leftAnchor = left, + rightAnchor = not left, + topAnchor = true, + height = 18, + width = 85, + material = RegisterMaterial("btn_focused_rect_innerglow") + }) + + local center = LUI.UIText.new({ + rightAnchor = true, + height = 12, + width = 85, + top = -6.5, + alignment = LUI.Alignment.Center, + font = CoD.TextSettings.BodyFontBold.Font + }) + + button:setup9SliceImage(10, 5, 0.25, 0.12) + center:setText(text) + button:addElement(center) + + return button + end + + buttons:addElement(createbutton("[F1] Accept", true)) + buttons:addElement(createbutton("[F2] Deny")) + + local fadeouttime = 50 + local timeout = 10 * 1000 - fadeouttime + + local function close() + container:processEvent({ + name = "update_navigation", + dispatchToChildren = true + }) + invite:animateToState("fade_out", fadeouttime) + invite:addElement(LUI.UITimer.new(fadeouttime + 50, "remove")) + + invite:registerEventHandler("remove", function() + container:removeElement(invite) + if (container.temp) then + container:removeElement(container.temp) + container.temp = nil + end + local temp = LUI.UIElement.new({}) + container.temp = temp + container:addElement(temp) + end) + end + + buttons:registerEventHandler("keydown_", function(element, event) + if (event.key == "F1") then + close() + discord.respond(request.userid, discord.reply.yes) + end + + if (event.key == "F2") then + close() + discord.respond(request.userid, discord.reply.no) + end + end) + + invite:registerAnimationState("fade_out", { + leftAnchor = true, + rightAnchor = true, + height = 75, + alpha = 0, + left = 0 + }) + + invite:addElement(LUI.UITimer.new(timeout, "end_invite")) + invite:registerEventHandler("end_invite", function() + close() + discord.respond(request.userid, discord.reply.ignore) + end) + + local bar = LUI.UIImage.new({ + bottomAnchor = true, + leftAnchor = true, + bottom = -3, + left = 3, + width = 200 - 6, + material = RegisterMaterial("white"), + height = 2, + color = { + r = 92 / 255, + g = 206 / 255, + b = 113 / 255 + } + }) + + bar:registerAnimationState("closing", { + bottomAnchor = true, + leftAnchor = true, + bottom = -3, + left = 3, + width = 0, + height = 2 + }) + + bar:animateToState("closing", timeout) + + avatar:registerEventHandler("update", function() + local avatarmaterial = discord.getavatarmaterial(request.userid) + avatar:setImage(RegisterMaterial(avatarmaterial)) + end) + + avatar:addElement(LUI.UITimer.new(100, "update")) + + invite:addElement(background) + invite:addElement(bar) + invite:addElement(border) + invite:addElement(padding) + padding:addElement(username) + padding:addElement(avatar) + padding:addElement(buttons) + + container:addElement(invite) +end + +container:registerEventHandler("keydown", function(element, event) + local first = container:getFirstChild() + + if (not first) then + return + end + + first:processEvent({ + name = "keydown_", + key = event.key + }) +end) + +LUI.roots.UIRoot0:registerEventHandler("discord_join_request", function(element, event) + addrequest(event.request) +end) + +LUI.roots.UIRoot0:addElement(container) diff --git a/data/ui_scripts/hud_info/__init__.lua b/data/cdata/ui_scripts/hud_info/__init__.lua similarity index 100% rename from data/ui_scripts/hud_info/__init__.lua rename to data/cdata/ui_scripts/hud_info/__init__.lua diff --git a/data/cdata/ui_scripts/hud_info/hud.lua b/data/cdata/ui_scripts/hud_info/hud.lua new file mode 100644 index 00000000..c8c2c0e5 --- /dev/null +++ b/data/cdata/ui_scripts/hud_info/hud.lua @@ -0,0 +1,193 @@ +local mphud = luiglobals.require("LUI.mp_hud.MPHud") +local barheight = 16 +local textheight = 13 +local textoffsety = barheight / 2 - textheight / 2 + +function createinfobar() + local infobar = LUI.UIElement.new({ + left = 213, + top = -6, + height = barheight, + width = 70, + leftAnchor = true, + topAnchor = true + }) + + infobar:registerAnimationState("minimap_on", { + left = 213, + top = -6, + height = barheight, + width = 70, + leftAnchor = true, + topAnchor = true + }) + + infobar:registerAnimationState("minimap_off", { + left = 0, + top = 0, + height = barheight, + width = 70, + leftAnchor = true, + topAnchor = true + }) + + infobar:registerAnimationState("hud_on", { + alpha = 1 + }) + + infobar:registerAnimationState("hud_off", { + alpha = 0 + }) + + return infobar +end + +function updateinfobarvisibility() + local root = Engine.GetLuiRoot() + local menus = root:AnyActiveMenusInStack() + local infobar = root.infobar + + if (not infobar) then + return + end + + if (menus or Game.InKillCam()) then + infobar:animateToState("hud_off") + else + infobar:animateToState("hud_on") + end + + local validstates = {"hud_on", "active", "nosignal", "scrambled"} + + infobar:animateToState("minimap_off") + for i = 1, #validstates do + if (validstates[i] == root.hud.minimap.current_state) then + infobar:animateToState("minimap_on") + break + end + end +end + +function populateinfobar(infobar) + elementoffset = 0 + + if (Engine.GetDvarBool("cg_infobar_fps")) then + infobar:addElement(infoelement({ + label = Engine.Localize("@MPHUD_FPS"), + getvalue = function() + return game:getfps() + end, + width = 70, + interval = 100 + })) + end + + if (Engine.GetDvarBool("cg_infobar_ping")) then + infobar:addElement(infoelement({ + label = Engine.Localize("@MPHUD_LATENCY"), + getvalue = function() + return game:getping() .. Engine.Localize("@MPHUD_LATENCY_MS") + end, + width = 115, + interval = 100 + })) + end + + updateinfobarvisibility() +end + +function infoelement(data) + local container = LUI.UIElement.new({ + bottomAnchor = true, + leftAnchor = true, + topAnchor = true, + width = data.width, + left = elementoffset + }) + + elementoffset = elementoffset + data.width + 10 + + local background = LUI.UIImage.new({ + bottomAnchor = true, + leftAnchor = true, + topAnchor = true, + rightAnchor = true, + material = luiglobals.RegisterMaterial("white"), + color = luiglobals.Colors.black, + alpha = 0.5 + }) + + local labelfont = CoD.TextSettings.FontBold110 + + local label = LUI.UIText.new({ + left = 5, + top = textoffsety, + font = labelfont.Font, + height = textheight, + leftAnchor = true, + topAnchor = true, + color = { + r = 0.8, + g = 0.8, + b = 0.8 + } + }) + + label:setText(data.label) + + local _, _, left = luiglobals.GetTextDimensions(data.label, labelfont.Font, textheight) + local value = LUI.UIText.new({ + left = left + 5, + top = textoffsety, + font = labelfont.Font, + height = textheight, + leftAnchor = true, + topAnchor = true, + color = { + r = 0.6, + g = 0.6, + b = 0.6 + } + }) + + value:addElement(LUI.UITimer.new(data.interval, "update")) + value:setText(data.getvalue()) + value:addEventHandler("update", function() + value:setText(data.getvalue()) + end) + + container:addElement(background) + container:addElement(label) + container:addElement(value) + + return container +end + +local updatehudvisibility = mphud.updateHudVisibility +mphud.updateHudVisibility = function(a1, a2) + updatehudvisibility(a1, a2) + updateinfobarvisibility() +end + +LUI.onmenuopen("mp_hud", function(hud) + if (Engine.InFrontend()) then + return + end + + local infobar = createinfobar() + local root = Engine.GetLuiRoot() + root.infobar = infobar + root.hud = hud + populateinfobar(infobar) + + root:registerEventHandler("update_hud_infobar_settings", function() + infobar:removeAllChildren() + populateinfobar(infobar) + end) + + root:processEvent({ + name = "update_hud_infobar_settings" + }) + + hud.static.scalable:addElement(infobar) +end) diff --git a/data/cdata/ui_scripts/hud_info/settings.lua b/data/cdata/ui_scripts/hud_info/settings.lua new file mode 100644 index 00000000..1a8bae08 --- /dev/null +++ b/data/cdata/ui_scripts/hud_info/settings.lua @@ -0,0 +1,121 @@ +local pcdisplay = luiglobals.require("LUI.PCDisplay") + +function createdivider(menu, text) + local element = LUI.UIElement.new({ + leftAnchor = true, + rightAnchor = true, + left = 0, + right = 0, + topAnchor = true, + bottomAnchor = false, + top = 0, + bottom = 33.33 + }) + + element.scrollingToNext = true + element:addElement(LUI.MenuBuilder.BuildRegisteredType("h1_option_menu_titlebar", { + title_bar_text = text + })) + + menu.list:addElement(element) +end + +pcdisplay.CreateOptions = function(menu) + LUI.Options.AddButtonOptionVariant(menu, luiglobals.GenericButtonSettings.Variants.Select, + "@LUA_MENU_COLORBLIND_FILTER", "@LUA_MENU_COLOR_BLIND_DESC", LUI.Options.GetRenderColorBlindText, + LUI.Options.RenderColorBlindToggle, LUI.Options.RenderColorBlindToggle) + + if Engine.IsMultiplayer() and Engine.GetDvarType("cg_paintballFx") == luiglobals.DvarTypeTable.DvarBool then + LUI.Options.AddButtonOptionVariant(menu, luiglobals.GenericButtonSettings.Variants.Select, + "@LUA_MENU_PAINTBALL", "@LUA_MENU_PAINTBALL_DESC", + LUI.Options.GetDvarEnableTextFunc("cg_paintballFx", false), LUI.Options.ToggleDvarFunc("cg_paintballFx"), + LUI.Options.ToggleDvarFunc("cg_paintballFx")) + end + + LUI.Options.AddButtonOptionVariant(menu, luiglobals.GenericButtonSettings.Variants.Select, "@LUA_MENU_BLOOD", + "@LUA_MENU_BLOOD_DESC", LUI.Options.GetDvarEnableTextFunc("cg_blood", false), LUI.Options + .ToggleProfiledataFunc("showblood", Engine.GetControllerForLocalClient(0)), LUI.Options + .ToggleProfiledataFunc("showblood", Engine.GetControllerForLocalClient(0))) + + if not Engine.IsMultiplayer() then + LUI.Options.AddButtonOptionVariant(menu, luiglobals.GenericButtonSettings.Variants.Select, + "@LUA_MENU_CROSSHAIR", "@LUA_MENU_CROSSHAIR_DESC", + LUI.Options.GetDvarEnableTextFunc("cg_drawCrosshairOption", false), + LUI.Options.ToggleDvarFunc("cg_drawCrosshairOption"), LUI.Options.ToggleDvarFunc("cg_drawCrosshairOption")) + + LUI.Options.CreateOptionButton(menu, "cg_drawDamageFeedbackOption", "@LUA_MENU_HIT_MARKER", + "@LUA_MENU_HIT_MARKER_DESC", {{ + text = "@LUA_MENU_ENABLED", + value = true + }, { + text = "@LUA_MENU_DISABLED", + value = false + }}) + end + + if Engine.IsMultiplayer() then + LUI.Options.AddButtonOptionVariant(menu, luiglobals.GenericButtonSettings.Variants.Select, + "@MENU_DISPLAY_KILLSTREAK_COUNTER", "@MENU_DISPLAY_KILLSTREAK_COUNTER_DESC", + pcdisplay.GetDisplayKillstreakCounterText, pcdisplay.DisplayKillstreakCounterToggle, + pcdisplay.DisplayKillstreakCounterToggle) + + LUI.Options.AddButtonOptionVariant(menu, luiglobals.GenericButtonSettings.Variants.Select, + "@MENU_DISPLAY_MEDAL_SPLASHES", "@MENU_DISPLAY_MEDAL_SPLASHES_DESC", pcdisplay.GetDisplayMedalSplashesText, + pcdisplay.DisplayMedalSplashesToggle, pcdisplay.DisplayMedalSplashesToggle) + + LUI.Options.AddButtonOptionVariant(menu, luiglobals.GenericButtonSettings.Variants.Select, + "@MENU_DISPLAY_WEAPON_EMBLEMS", "@MENU_DISPLAY_WEAPON_EMBLEMS_DESC", pcdisplay.GetDisplayWeaponEmblemsText, + pcdisplay.DisplayWeaponEmblemsToggle, pcdisplay.DisplayWeaponEmblemsToggle) + end + + LUI.Options.AddButtonOptionVariant(menu, luiglobals.GenericButtonSettings.Variants.Common, "@MENU_BRIGHTNESS", + "@MENU_BRIGHTNESS_DESC1", nil, nil, nil, pcdisplay.OpenBrightnessMenu, nil, nil, nil) + + local reddotbounds = { + step = 0.2, + max = 4, + min = 0.2 + } + + LUI.Options.AddButtonOptionVariant(menu, GenericButtonSettings.Variants.Slider, "@LUA_MENU_RED_DOT_BRIGHTNESS", + "@LUA_MENU_RED_DOT_BRIGHTNESS_DESC", function() + return (Engine.GetDvarFloat("r_redDotBrightnessScale") - reddotbounds.min) / + (reddotbounds.max - reddotbounds.min) + end, function() + Engine.SetDvarFloat("r_redDotBrightnessScale", math.min(reddotbounds.max, math.max(reddotbounds.min, + Engine.GetDvarFloat("r_redDotBrightnessScale") - reddotbounds.step))) + end, function() + Engine.SetDvarFloat("r_redDotBrightnessScale", math.min(reddotbounds.max, math.max(reddotbounds.min, + Engine.GetDvarFloat("r_redDotBrightnessScale") + reddotbounds.step))) + end) + + createdivider(menu, Engine.Localize("@LUA_MENU_TELEMETRY")) + + LUI.Options.CreateOptionButton(menu, "cg_infobar_ping", "@LUA_MENU_LATENCY", "@LUA_MENU_LATENCY_DESC", {{ + text = "@LUA_MENU_ENABLED", + value = true + }, { + text = "@LUA_MENU_DISABLED", + value = false + }}, nil, nil, function(value) + Engine.SetDvarBool("cg_infobar_ping", value) + Engine.GetLuiRoot():processEvent({ + name = "update_hud_infobar_settings" + }) + end) + + LUI.Options.CreateOptionButton(menu, "cg_infobar_fps", "@LUA_MENU_FPS", "@LUA_MENU_FPS_DESC", {{ + text = "@LUA_MENU_ENABLED", + value = true + }, { + text = "@LUA_MENU_DISABLED", + value = false + }}, nil, nil, function(value) + Engine.SetDvarBool("cg_infobar_fps", value) + Engine.GetLuiRoot():processEvent({ + name = "update_hud_infobar_settings" + }) + end) + + LUI.Options.InitScrollingList(menu.list, nil) +end diff --git a/data/cdata/ui_scripts/mods/__init__.lua b/data/cdata/ui_scripts/mods/__init__.lua new file mode 100644 index 00000000..5c75897c --- /dev/null +++ b/data/cdata/ui_scripts/mods/__init__.lua @@ -0,0 +1,5 @@ +require("loading") + +if (Engine.InFrontend()) then + require("download") +end diff --git a/data/cdata/ui_scripts/mods/download.lua b/data/cdata/ui_scripts/mods/download.lua new file mode 100644 index 00000000..b530fc42 --- /dev/null +++ b/data/cdata/ui_scripts/mods/download.lua @@ -0,0 +1,24 @@ +Engine.GetLuiRoot():registerEventHandler("mod_download_start", function(element, event) + local popup = LUI.openpopupmenu("generic_waiting_popup_", { + oncancel = function() + download.abort() + end, + withcancel = true, + text = "Downloading files..." + }) + + local file = "" + + popup:registerEventHandler("mod_download_set_file", function(element, event) + file = event.request.name + popup.text:setText(string.format("Downloading %s...", file)) + end) + + popup:registerEventHandler("mod_download_progress", function(element, event) + popup.text:setText(string.format("Downloading %s (%i%%)...", file, math.floor(event.fraction * 100))) + end) + + popup:registerEventHandler("mod_download_done", function() + LUI.FlowManager.RequestLeaveMenu(popup) + end) +end) diff --git a/data/cdata/ui_scripts/mods/loading.lua b/data/cdata/ui_scripts/mods/loading.lua new file mode 100644 index 00000000..ebf24748 --- /dev/null +++ b/data/cdata/ui_scripts/mods/loading.lua @@ -0,0 +1,115 @@ +function createdivider(menu, text) + local element = LUI.UIElement.new({ + leftAnchor = true, + rightAnchor = true, + left = 0, + right = 0, + topAnchor = true, + bottomAnchor = false, + top = 0, + bottom = 33.33 + }) + + element.scrollingToNext = true + element:addElement(LUI.MenuBuilder.BuildRegisteredType("h1_option_menu_titlebar", { + title_bar_text = Engine.ToUpperCase(text) + })) + + element.text = element:getFirstChild():getFirstChild():getNextSibling() + + menu.list:addElement(element) + return element +end + +function string:truncate(length) + if (#self <= length) then + return self + end + + return self:sub(1, length - 3) .. "..." +end + +if (game:issingleplayer()) then + LUI.addmenubutton("main_campaign", { + index = 6, + text = "@MENU_MODS", + description = Engine.Localize("@MENU_MODS_DESC"), + callback = function() + LUI.FlowManager.RequestAddMenu(nil, "mods_menu") + end + }) +end + +function getmodname(path) + local name = path + game:addlocalizedstring(name, name) + local desc = Engine.Localize("LUA_MENU_MOD_DESC_DEFAULT", name) + local infofile = path .. "/info.json" + + if (io.fileexists(infofile)) then + pcall(function() + local data = json.decode(io.readfile(infofile)) + game:addlocalizedstring(data.description, data.description) + game:addlocalizedstring(data.author, data.author) + game:addlocalizedstring(data.version, data.version) + desc = Engine.Localize("@LUA_MENU_MOD_DESC", data.description, data.author, data.version) + name = data.name + end) + end + + return name, desc +end + +LUI.MenuBuilder.registerType("mods_menu", function(a1) + local menu = LUI.MenuTemplate.new(a1, { + menu_title = "@MENU_MODS", + exclusiveController = 0, + menu_width = 400, + menu_top_indent = LUI.MenuTemplate.spMenuOffset, + showTopRightSmallBar = true, + uppercase_title = true + }) + + local modfolder = game:getloadedmod() + if (modfolder ~= "") then + local name = getmodname(modfolder) + createdivider(menu, Engine.Localize("@LUA_MENU_LOADED_MOD", name:truncate(24))) + + menu:AddButton("@LUA_MENU_UNLOAD", function() + Engine.Exec("unloadmod") + end, nil, true, nil, { + desc_text = Engine.Localize("@LUA_MENU_UNLOAD_DESC") + }) + end + + createdivider(menu, Engine.Localize("@LUA_MENU_AVAILABLE_MODS")) + + if (io.directoryexists("mods")) then + local mods = io.listfiles("mods/") + for i = 1, #mods do + if (io.directoryexists(mods[i]) and not io.directoryisempty(mods[i])) then + local name, desc = getmodname(mods[i]) + + if (mods[i] ~= modfolder) then + game:addlocalizedstring(name, name) + menu:AddButton(name, function() + Engine.Exec("loadmod " .. mods[i]) + end, nil, true, nil, { + desc_text = desc + }) + end + end + end + end + + menu:AddBackButton(function(a1) + Engine.PlaySound(CoD.SFX.MenuBack) + LUI.FlowManager.RequestLeaveMenu(a1) + end) + + LUI.Options.InitScrollingList(menu.list, nil) + menu:CreateBottomDivider() + menu.optionTextInfo = LUI.Options.AddOptionTextInfo(menu) + + return menu +end) diff --git a/data/cdata/ui_scripts/patches/__init__.lua b/data/cdata/ui_scripts/patches/__init__.lua new file mode 100644 index 00000000..6e0cc090 --- /dev/null +++ b/data/cdata/ui_scripts/patches/__init__.lua @@ -0,0 +1,27 @@ +require("language") +require("background_effects") + +if game:issingleplayer() then + require("sp_unlockall") + return +end + +require("disable_useless_things") + +if Engine.InFrontend() then + require("shader_dialog") + require("gamemodes") + require("no_mode_switch") +else + require("scoreboard") +end + +-- defined in mp_hud/hudutils.lua +function GetGameModeName() + return Engine.Localize(Engine.TableLookup(GameTypesTable.File, GameTypesTable.Cols.Ref, GameX.GetGameMode(), + GameTypesTable.Cols.Name)) +end + +function NeverAllowChangeTeams() + return false +end diff --git a/data/cdata/ui_scripts/patches/background_effects.lua b/data/cdata/ui_scripts/patches/background_effects.lua new file mode 100644 index 00000000..7f41b738 --- /dev/null +++ b/data/cdata/ui_scripts/patches/background_effects.lua @@ -0,0 +1,11 @@ +if (Engine.InFrontend()) then + return +end + +-- less background blur in SP, no blur in MP +LUI.MenuTemplate.InitInGameBkg = function(f39_arg0, f39_arg1) + LUI.MenuTemplate.AddDarken(f39_arg0, f39_arg1) + if game:issingleplayer() and not LUI.FlowManager.IsMenuTopmost(Engine.GetLuiRoot(), "advanced_video") then + LUI.MenuTemplate.AddWorldBlur(f39_arg0, f39_arg1) + end +end diff --git a/data/cdata/ui_scripts/patches/disable_useless_things.lua b/data/cdata/ui_scripts/patches/disable_useless_things.lua new file mode 100644 index 00000000..69fa968a --- /dev/null +++ b/data/cdata/ui_scripts/patches/disable_useless_things.lua @@ -0,0 +1,19 @@ +if Engine.InFrontend() then + -- Disable CP + Engine.SetDvarInt("ui_enable_cp", 0) + + -- Disable CP store + Engine.SetDvarInt("ui_show_store", 0) + + -- Remove CoD account button + if Engine.IsMultiplayer() and CoD.IsCoDAccountRegistrationAvailableInMyRegion() then + LUI.removemenubutton("pc_controls", 4) + end +end + +-- Remove social button +LUI.MenuBuilder.m_definitions["online_friends_widget"] = function() + return { + type = "UIElement" + } +end diff --git a/data/ui_scripts/patches/gamemodes.lua b/data/cdata/ui_scripts/patches/gamemodes.lua similarity index 100% rename from data/ui_scripts/patches/gamemodes.lua rename to data/cdata/ui_scripts/patches/gamemodes.lua diff --git a/data/cdata/ui_scripts/patches/language.lua b/data/cdata/ui_scripts/patches/language.lua new file mode 100644 index 00000000..2a62d98d --- /dev/null +++ b/data/cdata/ui_scripts/patches/language.lua @@ -0,0 +1,105 @@ +local available_languages = {"english", "english_safe", "french", "german", "italian", "polish", "portuguese", + "russian", "spanish", "simplified_chinese", "traditional_chinese", "japanese_partial", + "korean"} +local current_language = "LANGUAGE" + +LUI.UIButtonText.IsOffsetedLanguage = function() + return false +end + +function get_user_language() + user_language = game:getcurrentgamelanguage() +end + +function set_language(value) + local file_path = "players2/default/language" + local file = io.open(file_path, "w") + file:write(value) + file:close() +end + +function does_zone_folder_exists(language) + return io.directoryexists("zone/" .. language) +end + +get_user_language() + +if user_language ~= "" and does_zone_folder_exists(user_language) then + current_language = user_language +end + +LUI.addmenubutton("pc_controls", { + index = 4, + text = "LUA_MENU_CHOOSE_LANGUAGE", + description = Engine.Localize("LUA_MENU_CHOOSE_LANGUAGE_DESC"), + callback = function() + LUI.FlowManager.RequestAddMenu(nil, "choose_language_menu") + end +}) + +LUI.MenuBuilder.registerType("choose_language_menu", function(a1) + local menu = LUI.MenuTemplate.new(a1, { + menu_title = "LUA_MENU_CHOOSE_LANGUAGE", + menu_list_divider_top_offset = -(LUI.H1MenuTab.tabChangeHoldingElementHeight + H1MenuDims.spacing), + uppercase_title = true + }) + + for i = 1, #available_languages do + if does_zone_folder_exists(available_languages[i]) then + menu:AddButton(Engine.Localize(string.format("MENU_%s", available_languages[i])), function() + LUI.yesnopopup({ + title = Engine.Localize("@MENU_NOTICE"), + text = Engine.Localize("MENU_" .. current_language) .. " → " .. + Engine.Localize("MENU_" .. available_languages[i]) .. "\n\n" .. + Engine.Localize("@LUA_MENU_CONFIRM_LANGUAGE") .. " " .. + Engine.Localize("@MENU_APPLY_LANGUAGE_SETTINGS"), + callback = function(result) + if (result) then + set_language(available_languages[i]) + updater.relaunch() + else + LUI.FlowManager.RequestLeaveMenu(popup) + end + end + }) + end, available_languages[i] == current_language, true, nil, { + desc_text = Engine.Localize("LOCALE_" .. (available_languages[i])) + }) + end + end + + LUI.Options.InitScrollingList(menu.list, nil, { + rows = 12 + }) + + LUI.Options.AddOptionTextInfo(menu) + + menu:AddHelp({ + name = "add_button_helper_text", + button_ref = "", + helper_text = "^2" .. Engine.Localize("@LUA_MENU_DOWNLOAD") .. ": ^7https://docs.h1.gg/languages", + side = "left", + priority = -9001, + clickable = false + }) + + menu:AddBackButton() + + return menu +end) + +-- fix for ammo zeros +if not Engine.InFrontend() then + local weaponinfodef = LUI.MenuBuilder.m_definitions["WeaponInfoHudDef"] + LUI.MenuBuilder.m_definitions["WeaponInfoHudDef"] = function(...) + Engine.GetCurrentLanguage = function() + return 0 + end + local res = weaponinfodef(...) + Engine.GetCurrentLanguage = function() + lang = Engine.GetDvarString("loc_language") + return lang + end + return res + end +end diff --git a/data/ui_scripts/patches/no_mode_switch.lua b/data/cdata/ui_scripts/patches/no_mode_switch.lua similarity index 100% rename from data/ui_scripts/patches/no_mode_switch.lua rename to data/cdata/ui_scripts/patches/no_mode_switch.lua diff --git a/data/cdata/ui_scripts/patches/scoreboard.lua b/data/cdata/ui_scripts/patches/scoreboard.lua new file mode 100644 index 00000000..0dfc6d0c --- /dev/null +++ b/data/cdata/ui_scripts/patches/scoreboard.lua @@ -0,0 +1,35 @@ +local gametypes = { + ["dm"] = true, + ["dom"] = true, + ["sd"] = true, + ["war"] = true, + ["conf"] = true, + ["vlobby"] = true, + ["koth"] = true, + ["sab"] = true, + ["ctf"] = true, + ["dd"] = true, + ["hp"] = true, + ["gun"] = true +} + +local func = LUI.mp_hud.Scoreboard.DetermineIfSingleTeamGameType +LUI.mp_hud.Scoreboard.DetermineIfSingleTeamGameType = function() + local gametype = Engine.GetDvarString("ui_gametype") + if (gametypes[gametype]) then + return func() + end + + return Game.GetPlayerTeam() == Teams.free +end + +local updateicon = LUI.FactionIcon.Update +LUI.FactionIcon.Update = function(element, icon, a3) + local scale = LUI.FactionIcon.BackgroundScale + if (game:isdefaultmaterial(icon .. "_soft")) then + LUI.FactionIcon.BackgroundScale = 0 + end + + updateicon(element, icon, a3) + LUI.FactionIcon.BackgroundScale = scale +end diff --git a/data/cdata/ui_scripts/patches/shader_dialog.lua b/data/cdata/ui_scripts/patches/shader_dialog.lua new file mode 100644 index 00000000..d4723fe0 --- /dev/null +++ b/data/cdata/ui_scripts/patches/shader_dialog.lua @@ -0,0 +1,25 @@ +LUI.MenuBuilder.registerPopupType("ShaderCacheDialog_original", LUI.ShaderCacheDialog.new) + +local function dialog(...) + if (game:sharedget("has_accepted_shader_caching") == "1") then + return LUI.ShaderCacheDialog.new(...) + end + + return LUI.MenuBuilder.BuildRegisteredType("generic_yesno_popup", { + popup_title = Engine.Localize("@MENU_WARNING"), + message_text = Engine.Localize("@PLATFORM_SHADER_PRECACHE_ASK"), + yes_action = function() + game:sharedset("has_accepted_shader_caching", "1") + LUI.FlowManager.RequestAddMenu(nil, "ShaderCacheDialog_original") + end, + yes_text = Engine.Localize("@MENU_YES"), + no_text = Engine.Localize("@MENU_NO_DONT_ASK"), + no_action = function() + Engine.SetDvarInt("r_preloadShadersFrontendAllow", 0) + end, + default_focus_index = 2, + cancel_will_close = false + }) +end + +LUI.MenuBuilder.m_types_build["ShaderCacheDialog"] = dialog diff --git a/data/cdata/ui_scripts/patches/sp_unlockall.lua b/data/cdata/ui_scripts/patches/sp_unlockall.lua new file mode 100644 index 00000000..7db05582 --- /dev/null +++ b/data/cdata/ui_scripts/patches/sp_unlockall.lua @@ -0,0 +1,53 @@ +if Engine.InFrontend() then + local levelselectmenu = LUI.sp_menus.LevelSelectMenu + levelselectmenu.SetupInfoBoxRightForArcadeMode = function(f44_arg0, f44_arg1) + return false + end + LUI.LevelSelect.AddLevelListButtons = function(f50_arg0, f50_arg1) + for f50_local0 = 1, #f50_arg1, 1 do + if not Engine.GetDvarBool("arcademode") or not f50_arg1[f50_local0].narativeLevel then + f50_arg0:AddLevelListEntry(f50_local0, f50_arg1[f50_local0]) + end + end + if not Engine.GetDvarBool("arcademode") then + if Engine.GetDvarBool("profileMenuOption_hasUnlockedAll_SP") then + f50_arg0:AddHelp({ + name = "add_button_helper_text", + button_ref = "button_alt2", + helper_text = Engine.Localize("@LUA_MENU_CANCEL_UNLOCK_CAPS"), + side = "right", + clickable = true + }, levelselectmenu.SetUnlockAll) + else + f50_arg0:AddHelp({ + name = "add_button_helper_text", + button_ref = "button_alt2", + helper_text = Engine.Localize("@LUA_MENU_CAMPAIGN_UNLOCKED_ALL_TITLE"), + side = "right", + clickable = true + }, levelselectmenu.UnlockAllPopup) + end + end + end + levelselectmenu.UnlockAllPopup = function(f56_arg0, f56_arg1) + LUI.FlowManager.RequestAddMenu(nil, "request_yesno_popup_generic", true, f56_arg1.controller, nil, { + popup_title = Engine.Localize("@LUA_MENU_CAMPAIGN_UNLOCKED_ALL_TITLE"), + yes_action = levelselectmenu.SetUnlockAll, + message_text = Engine.Localize("@MENU_COMPLETED_CHEAT") .. ". " .. + Engine.Localize("@LUA_MENU_CAMPAIGN_UNLOCKED_ALL_DESC") + }) + end + levelselectmenu.SetUnlockAll = function(f57_arg0, f57_arg1) + Engine.SetDvarBool("profileMenuOption_hasUnlockedAll_SP", + not Engine.GetDvarBool("profileMenuOption_hasUnlockedAll_SP")) + Engine.SetDvarBool("mis_cheat", not Engine.GetDvarBool("mis_cheat")) + Engine.ExecNow("profile_menuDvarsFinish") + Engine.Exec("updategamerprofile") + local f57_local0 = LUI.FlowManager.GetTopOpenAndVisibleMenuName() + LUI.FlowManager.RequestAddMenu(nil, f57_local0, true, f57_arg1.controller, true, + LUI.LevelSelect.FindActData(f57_local0), { + reload = true + }) + end +end + diff --git a/data/ui_scripts/server_list/__init__.lua b/data/cdata/ui_scripts/server_list/__init__.lua similarity index 81% rename from data/ui_scripts/server_list/__init__.lua rename to data/cdata/ui_scripts/server_list/__init__.lua index 94df0df8..b17a57f4 100644 --- a/data/ui_scripts/server_list/__init__.lua +++ b/data/cdata/ui_scripts/server_list/__init__.lua @@ -4,3 +4,4 @@ end require("lobby") require("serverlist") +require("confirm") diff --git a/data/cdata/ui_scripts/server_list/confirm.lua b/data/cdata/ui_scripts/server_list/confirm.lua new file mode 100644 index 00000000..da79dadc --- /dev/null +++ b/data/cdata/ui_scripts/server_list/confirm.lua @@ -0,0 +1,12 @@ +LUI.MenuBuilder.m_types_build["popup_confirmdownload"] = function() + return LUI.MenuBuilder.BuildRegisteredType("generic_yesno_popup", { + popup_title = Engine.Localize("@MENU_NOTICE"), + message_text = Engine.Localize("@LUA_MENU_3RD_PARTY_CONTENT_DESC", download.getwwwurl()), + yes_action = function() + download.userdownloadresponse(true) + end, + no_action = function() + download.userdownloadresponse(false) + end + }) +end diff --git a/data/ui_scripts/server_list/lobby.lua b/data/cdata/ui_scripts/server_list/lobby.lua similarity index 91% rename from data/ui_scripts/server_list/lobby.lua rename to data/cdata/ui_scripts/server_list/lobby.lua index 20027026..8d71585c 100644 --- a/data/ui_scripts/server_list/lobby.lua +++ b/data/cdata/ui_scripts/server_list/lobby.lua @@ -1,8 +1,6 @@ local Lobby = luiglobals.Lobby local MPLobbyOnline = LUI.mp_menus.MPLobbyOnline -game:addlocalizedstring("LUA_MENU_SERVERLIST", "SERVER LIST") - function LeaveLobby(f5_arg0) LeaveXboxLive() if Lobby.IsInPrivateParty() == false or Lobby.IsPrivatePartyHost() then @@ -28,6 +26,12 @@ function menu_xboxlive(f16_arg0, f16_arg1) menu:AddBarracksButton() menu:AddPersonalizationButton() menu:AddDepotButton() + + -- kinda a weird place to do this, but it's whatever + -- add "MODS" button below depot button + local modsButton = menu:AddButton("@MENU_MODS", function(a1, a2) + LUI.FlowManager.RequestAddMenu(a1, "mods_menu", true, nil) + end) end local privateMatchButton = menu:AddButton("@MENU_PRIVATE_MATCH", MPLobbyOnline.OnPrivateMatch, diff --git a/data/cdata/ui_scripts/server_list/serverlist.lua b/data/cdata/ui_scripts/server_list/serverlist.lua new file mode 100644 index 00000000..e6c80757 --- /dev/null +++ b/data/cdata/ui_scripts/server_list/serverlist.lua @@ -0,0 +1,252 @@ +local Lobby = luiglobals.Lobby +local SystemLinkJoinMenu = LUI.mp_menus.SystemLinkJoinMenu + +if (not SystemLinkJoinMenu) then + return +end + +local columns = {{ + offset = 40, + text = "@MENU_HOST_NAME", + dataindex = 0 +}, { + offset = 500, + text = "@MENU_MAP", + dataindex = 1 +}, { + offset = 725, + text = "@MENU_TYPE1", + dataindex = 3 +}, { + offset = 920, + text = "@MENU_NUMPLAYERS", + dataindex = 2 +}, { + offset = 1070, + text = "@MENU_PING", + dataindex = 4 +}, { + offset = 10, + image = "s1_icon_locked", + customelement = function(value, offset) + return LUI.UIImage.new({ + leftAnchor = true, + topAnchor = true, + height = 20, + width = 20, + left = offset, + top = 2, + material = RegisterMaterial(CoD.Material.RestrictedIcon), + alpha = value == "1" and 1 or 0, + color = { + r = 1, + b = 1, + g = 1 + } + }) + end, + dataindex = 5 +}} + +function textlength(text, font, height) + local _, _, width = luiglobals.GetTextDimensions(text, font, height) + return width +end + +function trimtext(text, font, height, maxwidth) + if (maxwidth < 0) then + return text + end + + while (textlength(text, font, height) > maxwidth) do + text = text:sub(1, #text - 1) + end + + return text +end + +SystemLinkJoinMenu.AddHeaderButton = function(menu, f12_arg1, width) + local state = CoD.CreateState(0, f12_arg1, nil, nil, CoD.AnchorTypes.TopLeft) + state.width = width + local element = LUI.UIElement.new(state) + local button = SystemLinkJoinMenu.CreateButton("header", 24) + + button:addElement(LUI.Divider.new(CoD.CreateState(nil, 0, nil, nil, CoD.AnchorTypes.TopLeftRight), 40, + LUI.Divider.Grey)) + button:makeNotFocusable() + button:addElement(LUI.Divider.new(CoD.CreateState(nil, 0, nil, nil, CoD.AnchorTypes.BottomLeftRight), 40, + LUI.Divider.Grey)) + + button.m_eventHandlers = {} + + for i = 1, #columns do + if (columns[i].text) then + SystemLinkJoinMenu.MakeText(button.textHolder, columns[i].offset, Engine.Localize(columns[i].text), nil) + elseif (columns[i].image) then + local image = LUI.UIImage.new({ + leftAnchor = true, + topAnchor = true, + height = 20, + width = 20, + top = 2, + left = columns[i].offset, + material = RegisterMaterial(columns[i].image) + }) + button.textHolder:addElement(image) + end + end + + element:addElement(button) + menu:addElement(element) +end + +SystemLinkJoinMenu.AddServerButton = function(menu, controller, index) + local button = SystemLinkJoinMenu.CreateButton(index or "header", 24) + button:makeFocusable() + button.index = index + button:addEventHandler("button_action", SystemLinkJoinMenu.OnJoinGame) + + local gettext = function(i) + local text = Lobby.GetServerData(controller, index, columns[i].dataindex) + if (columns[i].customelement) then + text = columns[i].customelement(text) + end + + local islast = not columns[i + 1] + local end_ = islast and 1130 or columns[i + 1].offset + local maxlength = end_ - columns[i].offset + + if (maxlength < 0) then + maxlength = columns[i].offset - end_ + end + + if (not islast) then + maxlength = maxlength - 50 + end + + return trimtext(text, CoD.TextSettings.TitleFontSmall.Font, 14, maxlength) + end + + for i = 1, #columns do + if (columns[i].customelement) then + local value = Lobby.GetServerData(controller, index, columns[i].dataindex) + local element = columns[i].customelement(value, columns[i].offset) + button.textHolder:addElement(element) + else + SystemLinkJoinMenu.MakeText(button.textHolder, columns[i].offset, gettext(i), + luiglobals.Colors.h1.medium_grey) + end + end + + menu.list:addElement(button) + return button +end + +SystemLinkJoinMenu.MakeText = function(menu, f5_arg1, text, color) + local state = CoD.CreateState(f5_arg1, nil, f5_arg1 + 200, nil, CoD.AnchorTypes.Left) + state.font = CoD.TextSettings.TitleFontSmall.Font + state.top = -6 + state.height = 14 + state.alignment = nil + state.glow = LUI.GlowState.None + state.color = color + + local el = LUI.UIText.new(state) + el:registerAnimationState("focused", { + color = luiglobals.Colors.white + }) + + el:registerEventHandler("focused", function(element, event) + element:animateToState("focused", 0) + end) + + el:registerEventHandler("unfocused", function(element, event) + element:animateToState("default", 0) + end) + + el:setText(text) + menu:addElement(el) + + return el +end + +function menu_systemlink_join(f19_arg0, f19_arg1) + local width = 1145 + + local menu = LUI.MenuTemplate.new(f19_arg0, { + menu_title = "@PLATFORM_SYSTEM_LINK_TITLE", + menu_width = width, + menu_top_indent = 20, + disableDeco = true, + spacing = 1 + }) + + SystemLinkJoinMenu.AddHeaderButton(menu, 80, width) + SystemLinkJoinMenu.AddLowerCounter(menu, width) + SystemLinkJoinMenu.UpdateCounterText(menu, nil) + Lobby.BuildServerList(Engine.GetFirstActiveController()) + + local playercount = LUI.UIText.new({ + rightAnchor = true, + topAnchor = true, + height = 18, + bottom = 58, + font = CoD.TextSettings.BodyFont.Font, + width = 300, + alignment = LUI.Alignment.Right + }) + menu:addElement(playercount) + + local servercount = LUI.UIText.new({ + rightAnchor = true, + topAnchor = true, + height = 18, + bottom = 58 - 25, + font = CoD.TextSettings.BodyFont.Font, + width = 300, + alignment = LUI.Alignment.Right + }) + menu:addElement(servercount) + + menu.list:registerEventHandler(LUI.UIScrollIndicator.UpdateEvent, function(element, event) + SystemLinkJoinMenu.UpdateCounterText(menu, event) + + playercount:setText(Engine.Localize("@SERVERLIST_PLAYER_COUNT", serverlist:getplayercount())) + servercount:setText(Engine.Localize("@SERVERLIST_SERVER_COUNT", serverlist:getservercount())) + end) + + SystemLinkJoinMenu.UpdateGameList(menu) + menu:registerEventHandler("updateGameList", SystemLinkJoinMenu.UpdateGameList) + + LUI.ButtonHelperText.ClearHelperTextObjects(menu.help, { + side = "all" + }) + + menu:AddHelp({ + name = "add_button_helper_text", + button_ref = "button_alt1", + helper_text = Engine.Localize("@MENU_SB_TOOLTIP_BTN_REFRESH"), + side = "right", + clickable = true, + priority = -1000 + }, function(f21_arg0, f21_arg1) + SystemLinkJoinMenu.RefreshServers(f21_arg0, f21_arg1, menu) + end) + + menu:AddHelp({ + name = "add_button_helper_text", + button_ref = "button_action", + helper_text = Engine.Localize("@MENU_JOIN_GAME1"), + side = "left", + clickable = false, + priority = -1000 + }, nil, nil, true) + + menu:AddBackButton() + + Lobby.RefreshServerList(Engine.GetFirstActiveController()) + + return menu +end + +LUI.MenuBuilder.m_types_build["menu_systemlink_join"] = menu_systemlink_join diff --git a/data/cdata/ui_scripts/stats/__init__.lua b/data/cdata/ui_scripts/stats/__init__.lua new file mode 100644 index 00000000..224bad3e --- /dev/null +++ b/data/cdata/ui_scripts/stats/__init__.lua @@ -0,0 +1,145 @@ +if (game:issingleplayer() or not Engine.InFrontend()) then + return +end + +function createdivider(menu, text) + local element = LUI.UIElement.new({ + leftAnchor = true, + rightAnchor = true, + left = 0, + right = 0, + topAnchor = true, + bottomAnchor = false, + top = 0, + bottom = 33.33 + }) + + element.scrollingToNext = true + element:addElement(LUI.MenuBuilder.BuildRegisteredType("h1_option_menu_titlebar", { + title_bar_text = Engine.ToUpperCase(Engine.Localize(text)) + })) + + menu.list:addElement(element) +end + +local personalizationbutton = LUI.MPLobbyBase.AddPersonalizationButton +LUI.MPLobbyBase.AddPersonalizationButton = function(menu) + personalizationbutton(menu) + menu:AddButton("@LUA_MENU_STATS", function() + LUI.FlowManager.RequestAddMenu(nil, "stats_menu") + end) +end + +LUI.MenuBuilder.registerType("stats_menu", function(a1) + local menu = LUI.MenuTemplate.new(a1, { + menu_title = Engine.ToUpperCase(Engine.Localize("@LUA_MENU_STATS")), + menu_width = luiglobals.GenericMenuDims.OptionMenuWidth + }) + + createdivider(menu, "LUA_MENU_SETTINGS") + + LUI.Options.CreateOptionButton(menu, "cg_unlockall_items", "@LUA_MENU_UNLOCKALL_ITEMS", + "@LUA_MENU_UNLOCKALL_ITEMS_DESC", {{ + text = "@LUA_MENU_ENABLED", + value = true + }, { + text = "@LUA_MENU_DISABLED", + value = false + }}, nil, nil) + + LUI.Options.CreateOptionButton(menu, "cg_unlockall_loot", "@LUA_MENU_UNLOCKALL_LOOT", + "@LUA_MENU_UNLOCKALL_LOOT_DESC", {{ + text = "@LUA_MENU_ENABLED", + value = true + }, { + text = "@LUA_MENU_DISABLED", + value = false + }}, nil, nil) + + LUI.Options.CreateOptionButton(menu, "cg_unlockall_classes", "@LUA_MENU_UNLOCKALL_CLASSES", + "@LUA_MENU_UNLOCKALL_CLASSES_DESC", {{ + text = "@LUA_MENU_ENABLED", + value = true + }, { + text = "@LUA_MENU_DISABLED", + value = false + }}, nil, nil) + + createdivider(menu, "LUA_MENU_EDIT_STATS") + + local prestige = Engine.GetPlayerData(0, CoD.StatsGroup.Ranked, "prestige") or 0 + local experience = Engine.GetPlayerData(0, CoD.StatsGroup.Ranked, "experience") or 0 + local rank = Lobby.GetRankForXP(experience, prestige) + + prestigeeditbutton(menu, function(value) + Engine.SetPlayerData(0, CoD.StatsGroup.Ranked, "prestige", tonumber(value)) + end) + + rankeditbutton(menu, function(value) + local rank = tonumber(value) + local prestige = Engine.GetPlayerData(0, CoD.StatsGroup.Ranked, "prestige") or 0 + local experience = rank == 0 and 0 or Rank.GetRankMaxXP(tonumber(value) - 1, prestige) + + Engine.SetPlayerData(0, CoD.StatsGroup.Ranked, "experience", experience) + end) + + LUI.Options.InitScrollingList(menu.list, nil) + LUI.Options.AddOptionTextInfo(menu) + + menu:AddBackButton() + + return menu +end) + +function prestigeeditbutton(menu, callback) + local options = {} + local max = Lobby.GetMaxPrestigeLevel() + local prestige = Engine.GetPlayerData(0, CoD.StatsGroup.Ranked, "prestige") or 0 + + for i = 0, max do + game:addlocalizedstring("LUA_MENU_" .. i, i .. "") + + table.insert(options, { + text = "@" .. i, + value = i .. "" + }) + end + + Engine.SetDvarFromString("ui_prestige_level", prestige .. "") + + LUI.Options.CreateOptionButton(menu, "ui_prestige_level", "@LUA_MENU_PRESTIGE", "@LUA_MENU_PRESTIGE_DESC", options, + nil, nil, callback) +end + +function rankeditbutton(menu, callback) + local options = {} + local prestige = Engine.GetPlayerData(0, CoD.StatsGroup.Ranked, "prestige") or 0 + local experience = Engine.GetPlayerData(0, CoD.StatsGroup.Ranked, "experience") or 0 + + local rank = Lobby.GetRankForXP(experience, prestige) + local max = Rank.GetMaxRank(prestige) + local maxprestige = Lobby.GetMaxPrestigeLevel() + + for i = 0, max do + game:addlocalizedstring("LUA_MENU_" .. i, i .. "") + + table.insert(options, { + text = "@" .. (i + 1), + value = i .. "" + }) + end + + Engine.SetDvarFromString("ui_rank_level_", rank .. "") + + return LUI.Options.CreateOptionButton(menu, "ui_rank_level_", "@LUA_MENU_RANK", "@LUA_MENU_RANK_DESC", options, nil, + nil, callback) +end + +local isclasslocked = Cac.IsCustomClassLocked +Cac.IsCustomClassLocked = function(...) + if (Engine.GetDvarBool("cg_unlockall_classes")) then + return false + end + + return isclasslocked(...) +end diff --git a/data/scripts/logging/__init__.lua b/data/scripts/logging/__init__.lua deleted file mode 100644 index 33db4cfc..00000000 --- a/data/scripts/logging/__init__.lua +++ /dev/null @@ -1,117 +0,0 @@ --- modified version of https://github.com/Joelrau/S1x-IW6x-g_log-script (permission to use by author) - -if (game:getdvar("gamemode") ~= "mp") then - return -end - --- setup dvars -game:setdvarifuninitialized("logfile", 1) -if (tonumber(game:getdvar("logfile")) < 1) then - return -end -game:setdvarifuninitialized("g_log", "logs/games_mp.log") - -start_time = 0 - -function get_time() - local seconds = math.floor((game:gettime() - start_time) / 1000) - local minutes = math.floor(seconds / 60) - time = string.format("%d:%02d", minutes, seconds - minutes * 60) - while (string.len(time) < 6) do - time = " " .. time - end - time = time .. " " - return time -end - -function create_path(path) - local dir = path:gsub("%/", "\\"):match("(.*[\\])") - os.execute("if not exist " .. dir .. " mkdir " .. dir) -end - -function log_print(message) - local path = game:getdvar("g_log") - local file = io.open(path, "a") - if (file == nil) then - create_path(path) - file = assert(io.open(path, "a")) - end - file:write(get_time() .. message .. "\n") - file:close() -end - -function init() - start_time = game:gettime() - - log_print("------------------------------------------------------------") - log_print("InitGame") - - -- player callbacks - level:onnotify("connected", function(player) - player:player_connected() - end) - level:onnotify("say", function(player, message, hidden) - player:say(message) - end) - level:onnotify("say_team", function(player, message, hidden) - player:say(message, "say_team") - end) - - -- damage/killed hooks - game:onplayerdamage(player_damage) - game:onplayerkilled(player_killed) - - -- other level notifies for log - level:onnotify("exitLevel_called", function() - log_print("ExitLevel: executed") - end) - level:onnotify("shutdownGame_called", function() - log_print("ShutdownGame:") - log_print("------------------------------------------------------------") - end) -end - -function entity:player_connected() - log_print(string.format("J;%s;%i;%s", self:getguid(), self:getentitynumber(), self.name)) - - self:onnotifyonce("disconnect", function() - self:disconnect() - end) -end - -function entity:disconnect() - log_print(string.format("Q;%s;%i;%s", self:getguid(), self:getentitynumber(), self.name)) -end - -function player_damage(self_, inflictor, attacker, damage, dflags, mod, weapon, vPoint, vDir, hitLoc) - if (game:isplayer(attacker) == 1) then - log_print(string.format("D;%s;%i;%s;%s;%s;%i;%s;%s;%s;%i;%s;%s", self_:getguid(), self_:getentitynumber(), - self_.team, self_.name, attacker:getguid(), attacker:getentitynumber(), attacker.team, attacker.name, - weapon, damage, mod, hitLoc)) - else - log_print(string.format("D;%s;%i;%s;%s;%s;%i;%s;%s;%s;%i;%s;%s", self_:getguid(), self_:getentitynumber(), - self_.team, self_.name, "", "-1", "world", "", weapon, damage, mod, hitLoc)) - end -end - -function player_killed(self_, inflictor, attacker, damage, mod, weapon, vDir, hitLoc, psTimeOffset, deathAnimDuration) - if (game:isplayer(attacker) == 1) then - log_print(string.format("K;%s;%i;%s;%s;%s;%i;%s;%s;%s;%i;%s;%s", self_:getguid(), self_:getentitynumber(), - self_.team, self_.name, attacker:getguid(), attacker:getentitynumber(), attacker.team, attacker.name, - weapon, damage, mod, hitLoc)) - else - log_print(string.format("K;%s;%i;%s;%s;%s;%i;%s;%s;%s;%i;%s;%s", self_:getguid(), self_:getentitynumber(), - self_.team, self_.name, "", "-1", "world", "", weapon, damage, mod, hitLoc)) - end -end - --- this function handles 'say' and 'say_team' -function entity:say(message, mode) - if (not mode) then - mode = "say" - end - - log_print(string.format("%s;%s;%i;%s;%s", mode, self:getguid(), self:getentitynumber(), self.name, message)) -end - -init() diff --git a/data/ui_scripts/custom_depot/mod_eula.lua b/data/ui_scripts/custom_depot/mod_eula.lua deleted file mode 100644 index 31bae99b..00000000 --- a/data/ui_scripts/custom_depot/mod_eula.lua +++ /dev/null @@ -1,23 +0,0 @@ -game:addlocalizedstring("CUSTOM_DEPOT_EULA_1", "Dear User,") -game:addlocalizedstring("CUSTOM_DEPOT_EULA_2", - "By using this feature, you acknowledge that you are over 18 years old, and that any sort of chance games / gambling are allowed in your country (even if they do not involve real money).") -game:addlocalizedstring("CUSTOM_DEPOT_EULA_3", - "The H1-Mod team is not responsible if you break the law within your country, and the sole responsibility will be upon you to respect the same.") -game:addlocalizedstring("CUSTOM_DEPOT_EULA_4", - "The H1-Mod team will never include real money transactions within the modified systems. The only way to get currency, should you wish to, is by playing the game.") -game:addlocalizedstring("CUSTOM_DEPOT_EULA_5", "Best Regards,") -game:addlocalizedstring("CUSTOM_DEPOT_EULA_6", "The H1-Mod Team.") - -local mod_eula = function(unk1, unk2) - return LUI.EULABase.new(CoD.CreateState(0, 0, 0, 0, CoD.AnchorTypes.All), { - textStrings = LUI.EULABase.CreateTextStrings("@CUSTOM_DEPOT_EULA_", 6), - declineCallback = function(unk3) - unk2.declineCallback(unk3) - end, - acceptCallback = function(unk4) - unk2.acceptCallback(unk4) - end - }) -end - -LUI.MenuBuilder.registerPopupType("mod_eula", mod_eula) diff --git a/data/ui_scripts/discord/__init__.lua b/data/ui_scripts/discord/__init__.lua deleted file mode 100644 index bb6e5075..00000000 --- a/data/ui_scripts/discord/__init__.lua +++ /dev/null @@ -1,272 +0,0 @@ -if (game:issingleplayer() or Engine.InFrontend()) then - return -end - -local container = LUI.UIVerticalList.new({ - topAnchor = true, - rightAnchor = true, - top = 20, - right = 200, - width = 200, - spacing = 5, -}) - -function canasktojoin(userid) - history = history or {} - if (history[userid] ~= nil) then - return false - end - - history[userid] = true - game:ontimeout(function() - history[userid] = nil - end, 15000) - - return true -end - -function truncatename(name, length) - if (#name <= length - 3) then - return name - end - - return name:sub(1, length - 3) .. "..." -end - -function addrequest(request) - if (not canasktojoin(request.userid)) then - return - end - - if (container.temp) then - container:removeElement(container.temp) - container.temp = nil - end - - local invite = LUI.UIElement.new({ - leftAnchor = true, - rightAnchor = true, - height = 75, - }) - - invite:registerAnimationState("move_in", { - leftAnchor = true, - height = 75, - width = 200, - left = -220, - }) - - invite:animateToState("move_in", 100) - - local background = LUI.UIImage.new({ - topAnchor = true, - leftAnchor = true, - rightAnchor = true, - bottomAnchor = true, - top = 1, - left = 1, - bottom = -1, - right = -1, - material = RegisterMaterial("white"), - color = { - r = 0, - b = 0, - g = 0, - }, - alpha = 0.6, - }) - - local border = LUI.UIImage.new({ - topAnchor = true, - leftAnchor = true, - rightAnchor = true, - bottomAnchor = true, - material = RegisterMaterial("btn_focused_rect_innerglow"), - }) - - border:setup9SliceImage(10, 5, 0.25, 0.12) - - local paddingvalue = 10 - local padding = LUI.UIElement.new({ - topAnchor = true, - leftAnchor = true, - rightAnchor = true, - bottomAnchor = true, - top = paddingvalue, - left = paddingvalue, - right = -paddingvalue, - bottom = -paddingvalue, - }) - - local avatarmaterial = discord.getavatarmaterial(request.userid) - local avatar = LUI.UIImage.new({ - leftAnchor = true, - topAnchor = true, - width = 32, - height = 32, - left = 1, - material = RegisterMaterial(avatarmaterial) - }) - - local username = LUI.UIText.new({ - leftAnchor = true, - topAnchor = true, - height = 12, - left = 32 + paddingvalue, - color = Colors.white, - alignment = LUI.Alignment.Left, - rightAnchor = true, - font = CoD.TextSettings.BodyFontBold.Font - }) - - username:setText(string.format("%s^7#%s requested to join your game!", - truncatename(request.username, 18), request.discriminator)) - - local buttons = LUI.UIElement.new({ - leftAnchor = true, - rightAnchor = true, - topAnchor = true, - top = 37, - height = 18, - }) - - local createbutton = function(text, left) - local button = LUI.UIElement.new({ - leftAnchor = left, - rightAnchor = not left, - topAnchor = true, - height = 18, - width = 85, - material = RegisterMaterial("btn_focused_rect_innerglow"), - }) - - local center = LUI.UIText.new({ - rightAnchor = true, - height = 12, - width = 85, - top = -6.5, - alignment = LUI.Alignment.Center, - font = CoD.TextSettings.BodyFontBold.Font - }) - - button:setup9SliceImage(10, 5, 0.25, 0.12) - center:setText(text) - button:addElement(center) - - return button - end - - buttons:addElement(createbutton("[F1] Accept", true)) - buttons:addElement(createbutton("[F2] Deny")) - - local fadeouttime = 50 - local timeout = 10 * 1000 - fadeouttime - - local function close() - container:processEvent({ - name = "update_navigation", - dispatchToChildren = true - }) - invite:animateToState("fade_out", fadeouttime) - invite:addElement(LUI.UITimer.new(fadeouttime + 50, "remove")) - - invite:registerEventHandler("remove", function() - container:removeElement(invite) - if (container.temp) then - container:removeElement(container.temp) - container.temp = nil - end - local temp = LUI.UIElement.new({}) - container.temp = temp - container:addElement(temp) - end) - end - - buttons:registerEventHandler("keydown_", function(element, event) - if (event.key == "F1") then - close() - discord.respond(request.userid, discord.reply.yes) - end - - if (event.key == "F2") then - close() - discord.respond(request.userid, discord.reply.no) - end - end) - - invite:registerAnimationState("fade_out", { - leftAnchor = true, - rightAnchor = true, - height = 75, - alpha = 0, - left = 0 - }) - - invite:addElement(LUI.UITimer.new(timeout, "end_invite")) - invite:registerEventHandler("end_invite", function() - close() - discord.respond(request.userid, discord.reply.ignore) - end) - - local bar = LUI.UIImage.new({ - bottomAnchor = true, - leftAnchor = true, - bottom = -3, - left = 3, - width = 200 - 6, - material = RegisterMaterial("white"), - height = 2, - color = { - r = 92 / 255, - g = 206 / 255, - b = 113 / 255, - } - }) - - bar:registerAnimationState("closing", { - bottomAnchor = true, - leftAnchor = true, - bottom = -3, - left = 3, - width = 0, - height = 2, - }) - - bar:animateToState("closing", timeout) - - avatar:registerEventHandler("update", function() - local avatarmaterial = discord.getavatarmaterial(request.userid) - avatar:setImage(RegisterMaterial(avatarmaterial)) - end) - - avatar:addElement(LUI.UITimer.new(100, "update")) - - invite:addElement(background) - invite:addElement(bar) - invite:addElement(border) - invite:addElement(padding) - padding:addElement(username) - padding:addElement(avatar) - padding:addElement(buttons) - - container:addElement(invite) -end - -container:registerEventHandler("keydown", function(element, event) - local first = container:getFirstChild() - - if (not first) then - return - end - - first:processEvent({ - name = "keydown_", - key = event.key - }) -end) - -LUI.roots.UIRoot0:registerEventHandler("discord_join_request", function(element, event) - addrequest(event.request) -end) - -LUI.roots.UIRoot0:addElement(container) diff --git a/data/ui_scripts/extra_gamemodes/__init__.lua b/data/ui_scripts/extra_gamemodes/__init__.lua deleted file mode 100644 index 6880b983..00000000 --- a/data/ui_scripts/extra_gamemodes/__init__.lua +++ /dev/null @@ -1 +0,0 @@ --- this patch has been moved to ui_scripts/patches/gamemodes.lua diff --git a/data/ui_scripts/hud_info/hud.lua b/data/ui_scripts/hud_info/hud.lua deleted file mode 100644 index fe2e4a5a..00000000 --- a/data/ui_scripts/hud_info/hud.lua +++ /dev/null @@ -1,198 +0,0 @@ -local mphud = luiglobals.require("LUI.mp_hud.MPHud") -local barheight = 16 -local textheight = 13 -local textoffsety = barheight / 2 - textheight / 2 - -function createinfobar() - local infobar = LUI.UIElement.new({ - left = 213, - top = -6, - height = barheight, - width = 70, - leftAnchor = true, - topAnchor = true - }) - - infobar:registerAnimationState("minimap_on", { - left = 213, - top = -6, - height = barheight, - width = 70, - leftAnchor = true, - topAnchor = true - }) - - infobar:registerAnimationState("minimap_off", { - left = 0, - top = 0, - height = barheight, - width = 70, - leftAnchor = true, - topAnchor = true - }) - - infobar:registerAnimationState("hud_on", { - alpha = 1 - }) - - infobar:registerAnimationState("hud_off", { - alpha = 0 - }) - - return infobar -end - -function updateinfobarvisibility() - local root = Engine.GetLuiRoot() - local menus = root:AnyActiveMenusInStack() - local infobar = root.infobar - - if (not infobar) then - return - end - - if (menus or Game.InKillCam()) then - infobar:animateToState("hud_off") - else - infobar:animateToState("hud_on") - end - - local validstates = { - "hud_on", - "active", - "nosignal", - "scrambled" - } - - infobar:animateToState("minimap_off") - for i = 1, #validstates do - if (validstates[i] == root.hud.minimap.current_state) then - infobar:animateToState("minimap_on") - break - end - end -end - -function populateinfobar(infobar) - elementoffset = 0 - - if (Engine.GetDvarBool("cg_infobar_fps")) then - infobar:addElement(infoelement({ - label = "FPS: ", - getvalue = function() - return game:getfps() - end, - width = 70, - interval = 100 - })) - end - - if (Engine.GetDvarBool("cg_infobar_ping")) then - infobar:addElement(infoelement({ - label = "Latency: ", - getvalue = function() - return game:getping() .. " ms" - end, - width = 115, - interval = 100 - })) - end - - updateinfobarvisibility() -end - -function infoelement(data) - local container = LUI.UIElement.new({ - bottomAnchor = true, - leftAnchor = true, - topAnchor = true, - width = data.width, - left = elementoffset - }) - - elementoffset = elementoffset + data.width + 10 - - local background = LUI.UIImage.new({ - bottomAnchor = true, - leftAnchor = true, - topAnchor = true, - rightAnchor = true, - material = luiglobals.RegisterMaterial("white"), - color = luiglobals.Colors.black, - alpha = 0.5 - }) - - local labelfont = CoD.TextSettings.FontBold110 - - local label = LUI.UIText.new({ - left = 5, - top = textoffsety, - font = labelfont.Font, - height = textheight, - leftAnchor = true, - topAnchor = true, - color = { - r = 0.8, - g = 0.8, - b = 0.8 - } - }) - - label:setText(data.label) - - local _, _, left = luiglobals.GetTextDimensions(data.label, labelfont.Font, textheight) - local value = LUI.UIText.new({ - left = left + 5, - top = textoffsety, - font = labelfont.Font, - height = textheight, - leftAnchor = true, - topAnchor = true, - color = { - r = 0.6, - g = 0.6, - b = 0.6 - } - }) - - value:addElement(LUI.UITimer.new(data.interval, "update")) - value:setText(data.getvalue()) - value:addEventHandler("update", function() - value:setText(data.getvalue()) - end) - - container:addElement(background) - container:addElement(label) - container:addElement(value) - - return container -end - -local updatehudvisibility = mphud.updateHudVisibility -mphud.updateHudVisibility = function(a1, a2) - updatehudvisibility(a1, a2) - updateinfobarvisibility() -end - -LUI.onmenuopen("mp_hud", function(hud) - if (Engine.InFrontend()) then - return - end - - local infobar = createinfobar() - local root = Engine.GetLuiRoot() - root.infobar = infobar - root.hud = hud - populateinfobar(infobar) - - root:registerEventHandler("update_hud_infobar_settings", function() - infobar:removeAllChildren() - populateinfobar(infobar) - end) - - root:processEvent({ - name = "update_hud_infobar_settings" - }) - - hud.static.scalable:addElement(infobar) -end) diff --git a/data/ui_scripts/hud_info/settings.lua b/data/ui_scripts/hud_info/settings.lua deleted file mode 100644 index 603bad07..00000000 --- a/data/ui_scripts/hud_info/settings.lua +++ /dev/null @@ -1,144 +0,0 @@ -local pcdisplay = luiglobals.require("LUI.PCDisplay") - -game:addlocalizedstring("LUA_MENU_FPS", "FPS Counter") -game:addlocalizedstring("LUA_MENU_FPS_DESC", "Show FPS Counter.") - -game:addlocalizedstring("LUA_MENU_LATENCY", "Server Latency") -game:addlocalizedstring("LUA_MENU_LATENCY_DESC", "Show server latency.") - -game:addlocalizedstring("LUA_MENU_RED_DOT_BRIGHTNESS", "Red dot Brightness") -game:addlocalizedstring("LUA_MENU_RED_DOT_BRIGHTNESS_DESC", "Adjust the brightness of red dot reticles.") - -game:addlocalizedstring("MENU_SYSINFO_CUSTOMER_SUPPORT_URL", "https://h1.gg/") - -function createdivider(menu, text) - local element = LUI.UIElement.new({ - leftAnchor = true, - rightAnchor = true, - left = 0, - right = 0, - topAnchor = true, - bottomAnchor = false, - top = 0, - bottom = 33.33 - }) - - element.scrollingToNext = true - element:addElement(LUI.MenuBuilder.BuildRegisteredType("h1_option_menu_titlebar", { - title_bar_text = text - })) - - menu.list:addElement(element) -end - -pcdisplay.CreateOptions = function(menu) - LUI.Options.AddButtonOptionVariant(menu, luiglobals.GenericButtonSettings.Variants.Select, - "@LUA_MENU_COLORBLIND_FILTER", "@LUA_MENU_COLOR_BLIND_DESC", LUI.Options.GetRenderColorBlindText, - LUI.Options.RenderColorBlindToggle, LUI.Options.RenderColorBlindToggle) - - if Engine.IsMultiplayer() and Engine.GetDvarType("cg_paintballFx") == luiglobals.DvarTypeTable.DvarBool then - LUI.Options.AddButtonOptionVariant(menu, luiglobals.GenericButtonSettings.Variants.Select, - "@LUA_MENU_PAINTBALL", "@LUA_MENU_PAINTBALL_DESC", - LUI.Options.GetDvarEnableTextFunc("cg_paintballFx", false), LUI.Options.ToggleDvarFunc("cg_paintballFx"), - LUI.Options.ToggleDvarFunc("cg_paintballFx")) - end - - LUI.Options.AddButtonOptionVariant(menu, luiglobals.GenericButtonSettings.Variants.Select, "@LUA_MENU_BLOOD", - "@LUA_MENU_BLOOD_DESC", LUI.Options.GetDvarEnableTextFunc("cg_blood", false), LUI.Options - .ToggleProfiledataFunc("showblood", Engine.GetControllerForLocalClient(0)), LUI.Options - .ToggleProfiledataFunc("showblood", Engine.GetControllerForLocalClient(0))) - - if not Engine.IsMultiplayer() then - LUI.Options.AddButtonOptionVariant(menu, luiglobals.GenericButtonSettings.Variants.Select, - "@LUA_MENU_CROSSHAIR", "@LUA_MENU_CROSSHAIR_DESC", - LUI.Options.GetDvarEnableTextFunc("cg_drawCrosshairOption", false), - LUI.Options.ToggleDvarFunc("cg_drawCrosshairOption"), LUI.Options.ToggleDvarFunc("cg_drawCrosshairOption")) - - LUI.Options.CreateOptionButton(menu, "cg_drawDamageFeedbackOption", "@LUA_MENU_HIT_MARKER", - "@LUA_MENU_HIT_MARKER_DESC", {{ - text = "@LUA_MENU_ENABLED", - value = true - }, { - text = "@LUA_MENU_DISABLED", - value = false - }}) - end - - if Engine.IsMultiplayer() then - LUI.Options.AddButtonOptionVariant(menu, luiglobals.GenericButtonSettings.Variants.Select, - "@MENU_DISPLAY_KILLSTREAK_COUNTER", "@MENU_DISPLAY_KILLSTREAK_COUNTER_DESC", - pcdisplay.GetDisplayKillstreakCounterText, pcdisplay.DisplayKillstreakCounterToggle, - pcdisplay.DisplayKillstreakCounterToggle) - - LUI.Options.AddButtonOptionVariant(menu, luiglobals.GenericButtonSettings.Variants.Select, - "@MENU_DISPLAY_MEDAL_SPLASHES", "@MENU_DISPLAY_MEDAL_SPLASHES_DESC", pcdisplay.GetDisplayMedalSplashesText, - pcdisplay.DisplayMedalSplashesToggle, pcdisplay.DisplayMedalSplashesToggle) - - LUI.Options.AddButtonOptionVariant(menu, luiglobals.GenericButtonSettings.Variants.Select, - "@MENU_DISPLAY_WEAPON_EMBLEMS", "@MENU_DISPLAY_WEAPON_EMBLEMS_DESC", pcdisplay.GetDisplayWeaponEmblemsText, - pcdisplay.DisplayWeaponEmblemsToggle, pcdisplay.DisplayWeaponEmblemsToggle) - end - - LUI.Options.AddButtonOptionVariant(menu, luiglobals.GenericButtonSettings.Variants.Common, "@MENU_BRIGHTNESS", - "@MENU_BRIGHTNESS_DESC1", nil, nil, nil, pcdisplay.OpenBrightnessMenu, nil, nil, nil) - - - local reddotbounds = { - step = 0.2, - max = 4, - min = 0.2 - } - - LUI.Options.AddButtonOptionVariant( - menu, - GenericButtonSettings.Variants.Slider, - "@LUA_MENU_RED_DOT_BRIGHTNESS", - "@LUA_MENU_RED_DOT_BRIGHTNESS_DESC", - function() - return (Engine.GetDvarFloat( "r_redDotBrightnessScale" ) - - reddotbounds.min) / (reddotbounds.max - reddotbounds.min) - end, - function() - Engine.SetDvarFloat("r_redDotBrightnessScale", - math.min(reddotbounds.max, - math.max(reddotbounds.min, Engine.GetDvarFloat("r_redDotBrightnessScale") - reddotbounds.step)) - ) - end, - function() - Engine.SetDvarFloat("r_redDotBrightnessScale", - math.min(reddotbounds.max, - math.max(reddotbounds.min, Engine.GetDvarFloat("r_redDotBrightnessScale") + reddotbounds.step)) - ) - end - ) - - createdivider(menu, "TELEMETRY") - - LUI.Options.CreateOptionButton(menu, "cg_infobar_ping", "@LUA_MENU_LATENCY", "@LUA_MENU_LATENCY_DESC", {{ - text = "@LUA_MENU_ENABLED", - value = true - }, { - text = "@LUA_MENU_DISABLED", - value = false - }}, nil, nil, function(value) - Engine.SetDvarBool("cg_infobar_ping", value) - Engine.GetLuiRoot():processEvent({ - name = "update_hud_infobar_settings" - }) - end) - - LUI.Options.CreateOptionButton(menu, "cg_infobar_fps", "@LUA_MENU_FPS", "@LUA_MENU_FPS_DESC", {{ - text = "@LUA_MENU_ENABLED", - value = true - }, { - text = "@LUA_MENU_DISABLED", - value = false - }}, nil, nil, function(value) - Engine.SetDvarBool("cg_infobar_fps", value) - Engine.GetLuiRoot():processEvent({ - name = "update_hud_infobar_settings" - }) - end) - - LUI.Options.InitScrollingList(menu.list, nil) -end diff --git a/data/ui_scripts/mods/__init__.lua b/data/ui_scripts/mods/__init__.lua deleted file mode 100644 index 1ca1f8d2..00000000 --- a/data/ui_scripts/mods/__init__.lua +++ /dev/null @@ -1,3 +0,0 @@ -if (game:issingleplayer()) then - require("loading") -end diff --git a/data/ui_scripts/mods/loading.lua b/data/ui_scripts/mods/loading.lua deleted file mode 100644 index 9600d8a5..00000000 --- a/data/ui_scripts/mods/loading.lua +++ /dev/null @@ -1,123 +0,0 @@ -game:addlocalizedstring("MENU_MODS", "MODS") -game:addlocalizedstring("MENU_MODS_DESC", "Load installed mods.") -game:addlocalizedstring("LUA_MENU_MOD_DESC_DEFAULT", "Load &&1.") -game:addlocalizedstring("LUA_MENU_MOD_DESC", "&&1\nAuthor: &&2\nVersion: &&3") -game:addlocalizedstring("LUA_MENU_LOADED_MOD", "Loaded mod: ^2&&1") -game:addlocalizedstring("LUA_MENU_AVAILABLE_MODS", "Available mods") -game:addlocalizedstring("LUA_MENU_UNLOAD", "Unload") -game:addlocalizedstring("LUA_MENU_UNLOAD_DESC", "Unload the currently loaded mod.") - -function createdivider(menu, text) - local element = LUI.UIElement.new({ - leftAnchor = true, - rightAnchor = true, - left = 0, - right = 0, - topAnchor = true, - bottomAnchor = false, - top = 0, - bottom = 33.33 - }) - - element.scrollingToNext = true - element:addElement(LUI.MenuBuilder.BuildRegisteredType("h1_option_menu_titlebar", { - title_bar_text = Engine.ToUpperCase(text) - })) - - element.text = element:getFirstChild():getFirstChild():getNextSibling() - - menu.list:addElement(element) - return element -end - -function string:truncate(length) - if (#self <= length) then - return self - end - - return self:sub(1, length - 3) .. "..." -end - -LUI.addmenubutton("main_campaign", { - index = 6, - text = "@MENU_MODS", - description = Engine.Localize("@MENU_MODS_DESC"), - callback = function() - LUI.FlowManager.RequestAddMenu(nil, "mods_menu") - end -}) - -function getmodname(path) - local name = path - game:addlocalizedstring(name, name) - game:addlocalizedstring("LUA_MENU_MOD_DESC_DEFAULT", "Load &&1.") - local desc = Engine.Localize("LUA_MENU_MOD_DESC_DEFAULT", name) - local infofile = path .. "/info.json" - - if (io.fileexists(infofile)) then - pcall(function() - local data = json.decode(io.readfile(infofile)) - game:addlocalizedstring(data.description, data.description) - game:addlocalizedstring(data.author, data.author) - game:addlocalizedstring(data.version, data.version) - desc = Engine.Localize("@LUA_MENU_MOD_DESC", - data.description, data.author, data.version) - name = data.name - end) - end - - return name, desc -end - -LUI.MenuBuilder.registerType("mods_menu", function(a1) - local menu = LUI.MenuTemplate.new(a1, { - menu_title = "@MENU_MODS", - exclusiveController = 0, - menu_width = 400, - menu_top_indent = LUI.MenuTemplate.spMenuOffset, - showTopRightSmallBar = true - }) - - local modfolder = game:getloadedmod() - if (modfolder ~= "") then - local name = getmodname(modfolder) - createdivider(menu, Engine.Localize("@LUA_MENU_LOADED_MOD", name:truncate(24))) - - menu:AddButton("@LUA_MENU_UNLOAD", function() - Engine.Exec("unloadmod") - end, nil, true, nil, { - desc_text = Engine.Localize("@LUA_MENU_UNLOAD_DESC") - }) - end - - createdivider(menu, Engine.Localize("@LUA_MENU_AVAILABLE_MODS")) - - if (io.directoryexists("mods")) then - local mods = io.listfiles("mods/") - for i = 1, #mods do - if (io.directoryexists(mods[i]) and not io.directoryisempty(mods[i])) then - local name, desc = getmodname(mods[i]) - - if (mods[i] ~= modfolder) then - game:addlocalizedstring(name, name) - menu:AddButton(name, function() - Engine.Exec("loadmod " .. mods[i]) - end, nil, true, nil, { - desc_text = desc - }) - end - end - end - end - - menu:AddBackButton(function(a1) - Engine.PlaySound(CoD.SFX.MenuBack) - LUI.FlowManager.RequestLeaveMenu(a1) - end) - - LUI.Options.InitScrollingList(menu.list, nil) - menu:CreateBottomDivider() - menu.optionTextInfo = LUI.Options.AddOptionTextInfo(menu) - - return menu -end) diff --git a/data/ui_scripts/no_mode_switch/__init__.lua b/data/ui_scripts/no_mode_switch/__init__.lua deleted file mode 100644 index 8761ea04..00000000 --- a/data/ui_scripts/no_mode_switch/__init__.lua +++ /dev/null @@ -1 +0,0 @@ --- this patch has been moved to ui_scripts/patches/no_mode_switch.lua diff --git a/data/ui_scripts/patches/__init__.lua b/data/ui_scripts/patches/__init__.lua deleted file mode 100644 index b5a19097..00000000 --- a/data/ui_scripts/patches/__init__.lua +++ /dev/null @@ -1,20 +0,0 @@ -if (game:issingleplayer()) then - return -end - -if (Engine.InFrontend()) then - require("shaderdialog") - require("gamemodes") - require("no_mode_switch") - require("disable_useless_things") -end - --- defined in mp_hud/hudutils.lua -function GetGameModeName() - return Engine.Localize(Engine.TableLookup(GameTypesTable.File, - GameTypesTable.Cols.Ref, GameX.GetGameMode(), GameTypesTable.Cols.Name)) -end - -function NeverAllowChangeTeams() - return false -end diff --git a/data/ui_scripts/patches/disable_useless_things.lua b/data/ui_scripts/patches/disable_useless_things.lua deleted file mode 100644 index 59696748..00000000 --- a/data/ui_scripts/patches/disable_useless_things.lua +++ /dev/null @@ -1,17 +0,0 @@ --- Disable CP -Engine.SetDvarInt("ui_enable_cp", 0) - --- Disable CP store -Engine.SetDvarInt("ui_show_store", 0) - --- Remove CoD account button -if Engine.IsMultiplayer() and CoD.IsCoDAccountRegistrationAvailableInMyRegion() then - LUI.removemenubutton("pc_controls", 4) -end - --- Remove social button -LUI.MenuBuilder.m_definitions["online_friends_widget"] = function() - return { - type = "UIElement" - } -end diff --git a/data/ui_scripts/patches/shaderdialog.lua b/data/ui_scripts/patches/shaderdialog.lua deleted file mode 100644 index 9d658d27..00000000 --- a/data/ui_scripts/patches/shaderdialog.lua +++ /dev/null @@ -1,28 +0,0 @@ -LUI.MenuBuilder.registerPopupType("ShaderCacheDialog_original", LUI.ShaderCacheDialog.new) - -game:addlocalizedstring("PLATFORM_SHADER_PRECACHE_ASK", "Would you like to populate the shader cache? It may cause crashes with certain GPUs (e.g. RTX cards) but will improve performance if successful.") -game:addlocalizedstring("MENU_NO_DONT_ASK", "No, don't ask me again") - -local function dialog(...) - if (game:sharedget("has_accepted_shader_caching") == "1") then - return LUI.ShaderCacheDialog.new(...) - end - - return LUI.MenuBuilder.BuildRegisteredType("generic_yesno_popup", { - popup_title = Engine.Localize("@MENU_WARNING"), - message_text = Engine.Localize("@PLATFORM_SHADER_PRECACHE_ASK"), - yes_action = function() - game:sharedset("has_accepted_shader_caching", "1") - LUI.FlowManager.RequestAddMenu(nil, "ShaderCacheDialog_original") - end, - yes_text = Engine.Localize("@MENU_YES"), - no_text = Engine.Localize("@MENU_NO_DONT_ASK"), - no_action = function() - Engine.SetDvarInt("r_preloadShadersFrontendAllow", 0) - end, - default_focus_index = 2, - cancel_will_close = false - }) -end - -LUI.MenuBuilder.m_types_build["ShaderCacheDialog"] = dialog diff --git a/data/ui_scripts/server_list/serverlist.lua b/data/ui_scripts/server_list/serverlist.lua deleted file mode 100644 index 10e48598..00000000 --- a/data/ui_scripts/server_list/serverlist.lua +++ /dev/null @@ -1,263 +0,0 @@ -local Lobby = luiglobals.Lobby -local SystemLinkJoinMenu = LUI.mp_menus.SystemLinkJoinMenu - -if (not SystemLinkJoinMenu) then - return -end - -game:addlocalizedstring("MENU_NUMPLAYERS", "Players") -game:addlocalizedstring("MENU_PING", "Ping") -game:addlocalizedstring("SERVERLIST_PLAYER_COUNT", "&&1 Players") -game:addlocalizedstring("SERVERLIST_SERVER_COUNT", "&&1 Servers") - -local columns = { - { - offset = 40, - text = "@MENU_HOST_NAME", - dataindex = 0 - }, - { - offset = 500, - text = "@MENU_MAP", - dataindex = 1 - }, - { - offset = 700, - text = "@MENU_TYPE1", - dataindex = 3 - }, - { - offset = 950, - text = "@MENU_NUMPLAYERS", - dataindex = 2 - }, - { - offset = 1100, - text = "@MENU_PING", - dataindex = 4 - }, - { - offset = 10, - image = "s1_icon_locked", - customelement = function(value, offset) - return LUI.UIImage.new({ - leftAnchor = true, - topAnchor = true, - height = 20, - width = 20, - left = offset, - top = 2, - material = RegisterMaterial(CoD.Material.RestrictedIcon), - alpha = value == "1" and 1 or 0, - color = { - r = 1, - b = 1, - g = 1 - } - }) - end, - dataindex = 5 - } -} - -function textlength(text, font, height) - local _, _, width = luiglobals.GetTextDimensions(text, font, height) - return width -end - -function trimtext(text, font, height, maxwidth) - if (maxwidth < 0) then - return text - end - - while (textlength(text, font, height) > maxwidth) do - text = text:sub(1, #text - 1) - end - - return text -end - -SystemLinkJoinMenu.AddHeaderButton = function(menu, f12_arg1, width) - local state = CoD.CreateState(0, f12_arg1, nil, nil, CoD.AnchorTypes.TopLeft) - state.width = width - local element = LUI.UIElement.new(state) - local button = SystemLinkJoinMenu.CreateButton("header", 24) - - button:addElement(LUI.Divider.new(CoD.CreateState(nil, 0, nil, nil, CoD.AnchorTypes.TopLeftRight), 40, - LUI.Divider.Grey)) - button:makeNotFocusable() - button:addElement(LUI.Divider.new(CoD.CreateState(nil, 0, nil, nil, CoD.AnchorTypes.BottomLeftRight), 40, - LUI.Divider.Grey)) - - button.m_eventHandlers = {} - - for i = 1, #columns do - if (columns[i].text) then - SystemLinkJoinMenu.MakeText(button.textHolder, columns[i].offset, Engine.Localize(columns[i].text), nil) - elseif (columns[i].image) then - local image = LUI.UIImage.new({ - leftAnchor = true, - topAnchor = true, - height = 20, - width = 20, - top = 2, - left = columns[i].offset, - material = RegisterMaterial(columns[i].image) - }) - button.textHolder:addElement(image) - end - end - - element:addElement(button) - menu:addElement(element) -end - -SystemLinkJoinMenu.AddServerButton = function(menu, controller, index) - local button = SystemLinkJoinMenu.CreateButton(index or "header", 24) - button:makeFocusable() - button.index = index - button:addEventHandler("button_action", SystemLinkJoinMenu.OnJoinGame) - - local gettext = function(i) - local text = Lobby.GetServerData(controller, index, columns[i].dataindex) - if (columns[i].customelement) then - text = columns[i].customelement(text) - end - - local islast = not columns[i + 1] - local end_ = islast and 1130 or columns[i + 1].offset - local maxlength = end_ - columns[i].offset - - if (maxlength < 0) then - maxlength = columns[i].offset - end_ - end - - if (not islast) then - maxlength = maxlength - 50 - end - - return trimtext(text, CoD.TextSettings.TitleFontSmall.Font, 14, maxlength) - end - - for i = 1, #columns do - if (columns[i].customelement) then - local value = Lobby.GetServerData(controller, index, columns[i].dataindex) - local element = columns[i].customelement(value, columns[i].offset) - button.textHolder:addElement(element) - else - SystemLinkJoinMenu.MakeText(button.textHolder, columns[i].offset, gettext(i), luiglobals.Colors.h1.medium_grey) - end - end - - menu.list:addElement(button) - return button -end - -SystemLinkJoinMenu.MakeText = function(menu, f5_arg1, text, color) - local state = CoD.CreateState(f5_arg1, nil, f5_arg1 + 200, nil, CoD.AnchorTypes.Left) - state.font = CoD.TextSettings.TitleFontSmall.Font - state.top = -6 - state.height = 14 - state.alignment = nil - state.glow = LUI.GlowState.None - state.color = color - - local el = LUI.UIText.new(state) - el:registerAnimationState("focused", { - color = luiglobals.Colors.white - }) - - el:registerEventHandler("focused", function(element, event) - element:animateToState("focused", 0) - end) - - el:registerEventHandler("unfocused", function(element, event) - element:animateToState("default", 0) - end) - - el:setText(text) - menu:addElement(el) - - return el -end - -function menu_systemlink_join(f19_arg0, f19_arg1) - local width = 1145 - - local menu = LUI.MenuTemplate.new(f19_arg0, { - menu_title = "@PLATFORM_SYSTEM_LINK_TITLE", - menu_width = width, - menu_top_indent = 20, - disableDeco = true, - spacing = 1 - }) - - SystemLinkJoinMenu.AddHeaderButton(menu, 80, width) - SystemLinkJoinMenu.AddLowerCounter(menu, width) - SystemLinkJoinMenu.UpdateCounterText(menu, nil) - Lobby.BuildServerList(Engine.GetFirstActiveController()) - - local playercount = LUI.UIText.new({ - rightAnchor = true, - topAnchor = true, - height = 18, - bottom = 58, - font = CoD.TextSettings.BodyFont.Font, - width = 300, - alignment = LUI.Alignment.Right, - }) - menu:addElement(playercount) - - local servercount = LUI.UIText.new({ - rightAnchor = true, - topAnchor = true, - height = 18, - bottom = 58 - 25, - font = CoD.TextSettings.BodyFont.Font, - width = 300, - alignment = LUI.Alignment.Right, - }) - menu:addElement(servercount) - - menu.list:registerEventHandler(LUI.UIScrollIndicator.UpdateEvent, function(element, event) - SystemLinkJoinMenu.UpdateCounterText(menu, event) - - playercount:setText(Engine.Localize("@SERVERLIST_PLAYER_COUNT", serverlist:getplayercount())) - servercount:setText(Engine.Localize("@SERVERLIST_SERVER_COUNT", serverlist:getservercount())) - end) - - SystemLinkJoinMenu.UpdateGameList(menu) - menu:registerEventHandler("updateGameList", SystemLinkJoinMenu.UpdateGameList) - - LUI.ButtonHelperText.ClearHelperTextObjects(menu.help, { - side = "all" - }) - - menu:AddHelp({ - name = "add_button_helper_text", - button_ref = "button_alt1", - helper_text = Engine.Localize("@MENU_SB_TOOLTIP_BTN_REFRESH"), - side = "right", - clickable = true, - priority = -1000 - }, function(f21_arg0, f21_arg1) - SystemLinkJoinMenu.RefreshServers(f21_arg0, f21_arg1, menu) - end) - - menu:AddHelp({ - name = "add_button_helper_text", - button_ref = "button_action", - helper_text = Engine.Localize("@MENU_JOIN_GAME1"), - side = "left", - clickable = false, - priority = -1000 - }, nil, nil, true) - - menu:AddBackButton() - - Lobby.RefreshServerList(Engine.GetFirstActiveController()) - - return menu -end - -LUI.MenuBuilder.m_types_build["menu_systemlink_join"] = menu_systemlink_join diff --git a/data/ui_scripts/stats/__init__.lua b/data/ui_scripts/stats/__init__.lua deleted file mode 100644 index 306c7ae0..00000000 --- a/data/ui_scripts/stats/__init__.lua +++ /dev/null @@ -1,171 +0,0 @@ -if (game:issingleplayer() or not Engine.InFrontend()) then - return -end - -game:addlocalizedstring("LUA_MENU_STATS", "Stats") -game:addlocalizedstring("LUA_MENU_STATS_DESC", "Edit player stats settings.") - -game:addlocalizedstring("LUA_MENU_UNLOCKALL_ITEMS", "Unlock all items") -game:addlocalizedstring("LUA_MENU_UNLOCKALL_ITEMS_DESC", - "Whether items should be locked based on the player's stats or always unlocked.") - -game:addlocalizedstring("LUA_MENU_UNLOCKALL_LOOT", "Unlock all loot") -game:addlocalizedstring("LUA_MENU_UNLOCKALL_LOOT_DESC", - "Whether loot should be locked based on the player's stats or always unlocked.") - -game:addlocalizedstring("LUA_MENU_UNLOCKALL_CLASSES", "Unlock all classes") -game:addlocalizedstring("LUA_MENU_UNLOCKALL_CLASSES_DESC", - "Whether classes should be locked based on the player's stats or always unlocked.") - -game:addlocalizedstring("LUA_MENU_PRESTIGE", "Prestige") -game:addlocalizedstring("LUA_MENU_PRESTIGE_DESC", "Edit prestige level.") -game:addlocalizedstring("LUA_MENU_RANK", "Rank") -game:addlocalizedstring("LUA_MENU_RANK_DESC", "Edit rank.") - -game:addlocalizedstring("LUA_MENU_UNSAVED_CHANGES", "You have unsaved changes, are you sure you want to exit?") -game:addlocalizedstring("LUA_MENU_SAVE", "Save changes") -game:addlocalizedstring("LUA_MENU_SAVE_DESC", "Save changes.") -game:addlocalizedstring("LUA_MENU_SETTINGS", "Settings") -game:addlocalizedstring("LUA_MENU_EDIT_STATS", "Edit Stats") - -function createdivider(menu, text) - local element = LUI.UIElement.new({ - leftAnchor = true, - rightAnchor = true, - left = 0, - right = 0, - topAnchor = true, - bottomAnchor = false, - top = 0, - bottom = 33.33 - }) - - element.scrollingToNext = true - element:addElement(LUI.MenuBuilder.BuildRegisteredType("h1_option_menu_titlebar", { - title_bar_text = Engine.ToUpperCase(Engine.Localize(text)) - })) - - menu.list:addElement(element) -end - -local personalizationbutton = LUI.MPLobbyBase.AddPersonalizationButton -LUI.MPLobbyBase.AddPersonalizationButton = function(menu) - personalizationbutton(menu) - menu:AddButton("@LUA_MENU_STATS", function() - LUI.FlowManager.RequestAddMenu(nil, "stats_menu") - end) -end - -LUI.MenuBuilder.registerType("stats_menu", function(a1) - local menu = LUI.MenuTemplate.new(a1, { - menu_title = Engine.ToUpperCase(Engine.Localize("@LUA_MENU_STATS")), - menu_width = luiglobals.GenericMenuDims.OptionMenuWidth - }) - - createdivider(menu, "@LUA_MENU_SETTINGS") - - LUI.Options.CreateOptionButton(menu, "cg_unlockall_items", "@LUA_MENU_UNLOCKALL_ITEMS", - "@LUA_MENU_UNLOCKALL_ITEMS_DESC", {{ - text = "@LUA_MENU_ENABLED", - value = true - }, { - text = "@LUA_MENU_DISABLED", - value = false - }}, nil, nil) - - LUI.Options.CreateOptionButton(menu, "cg_unlockall_loot", "@LUA_MENU_UNLOCKALL_LOOT", - "@LUA_MENU_UNLOCKALL_LOOT_DESC", {{ - text = "@LUA_MENU_ENABLED", - value = true - }, { - text = "@LUA_MENU_DISABLED", - value = false - }}, nil, nil) - - LUI.Options.CreateOptionButton(menu, "cg_unlockall_classes", "@LUA_MENU_UNLOCKALL_CLASSES", - "@LUA_MENU_UNLOCKALL_CLASSES_DESC", {{ - text = "@LUA_MENU_ENABLED", - value = true - }, { - text = "@LUA_MENU_DISABLED", - value = false - }}, nil, nil) - - createdivider(menu, "@LUA_MENU_EDIT_STATS") - - local prestige = Engine.GetPlayerData(0, CoD.StatsGroup.Ranked, "prestige") or 0 - local experience = Engine.GetPlayerData(0, CoD.StatsGroup.Ranked, "experience") or 0 - local rank = Lobby.GetRankForXP(experience, prestige) - - prestigeeditbutton(menu, function(value) - Engine.SetPlayerData(0, CoD.StatsGroup.Ranked, "prestige", tonumber(value)) - end) - - rankeditbutton(menu, function(value) - local rank = tonumber(value) - local prestige = Engine.GetPlayerData(0, CoD.StatsGroup.Ranked, "prestige") or 0 - local experience = rank == 0 and 0 or Rank.GetRankMaxXP(tonumber(value) - 1, prestige) - - Engine.SetPlayerData(0, CoD.StatsGroup.Ranked, "experience", experience) - end) - - LUI.Options.InitScrollingList(menu.list, nil) - LUI.Options.AddOptionTextInfo(menu) - - menu:AddBackButton() - - return menu -end) - -function prestigeeditbutton(menu, callback) - local options = {} - local max = Lobby.GetMaxPrestigeLevel() - local prestige = Engine.GetPlayerData(0, CoD.StatsGroup.Ranked, "prestige") or 0 - - for i = 0, max do - game:addlocalizedstring("LUA_MENU_" .. i, i .. "") - - table.insert(options, { - text = "@" .. i, - value = i .. "" - }) - end - - Engine.SetDvarFromString("ui_prestige_level", prestige .. "") - - LUI.Options.CreateOptionButton(menu, "ui_prestige_level", "@LUA_MENU_PRESTIGE", "@LUA_MENU_PRESTIGE_DESC", options, - nil, nil, callback) -end - -function rankeditbutton(menu, callback) - local options = {} - local prestige = Engine.GetPlayerData(0, CoD.StatsGroup.Ranked, "prestige") or 0 - local experience = Engine.GetPlayerData(0, CoD.StatsGroup.Ranked, "experience") or 0 - - local rank = Lobby.GetRankForXP(experience, prestige) - local max = Rank.GetMaxRank(prestige) - local maxprestige = Lobby.GetMaxPrestigeLevel() - - for i = 0, max do - game:addlocalizedstring("LUA_MENU_" .. i, i .. "") - - table.insert(options, { - text = "@" .. (i + 1), - value = i .. "" - }) - end - - Engine.SetDvarFromString("ui_rank_level_", rank .. "") - - return LUI.Options.CreateOptionButton(menu, "ui_rank_level_", "@LUA_MENU_RANK", "@LUA_MENU_RANK_DESC", options, nil, - nil, callback) -end - -local isclasslocked = Cac.IsCustomClassLocked -Cac.IsCustomClassLocked = function(...) - if (Engine.GetDvarBool("cg_unlockall_classes")) then - return false - end - - return isclasslocked(...) -end diff --git a/data/zone_source/build.txt b/data/zone_source/build.txt new file mode 100644 index 00000000..eac96812 --- /dev/null +++ b/data/zone_source/build.txt @@ -0,0 +1,16 @@ +deu_h1_mod_common +eng_h1_mod_common +ens_h1_mod_common +fra_h1_mod_common +fra_h1_mod_common_mp +h1_mod_common +ita_h1_mod_common +jpp_h1_mod_common +kor_h1_mod_common +pol_h1_mod_common +por_h1_mod_common +rus_h1_mod_common +rus_h1_mod_common_mp +sch_h1_mod_common +spa_h1_mod_common +tch_h1_mod_common \ No newline at end of file diff --git a/data/zone_source/deu_h1_mod_common.csv b/data/zone_source/deu_h1_mod_common.csv new file mode 100644 index 00000000..dacfeb4c --- /dev/null +++ b/data/zone_source/deu_h1_mod_common.csv @@ -0,0 +1 @@ +localize,german \ No newline at end of file diff --git a/data/zone_source/eng_h1_mod_common.csv b/data/zone_source/eng_h1_mod_common.csv new file mode 100644 index 00000000..7bf70ec8 --- /dev/null +++ b/data/zone_source/eng_h1_mod_common.csv @@ -0,0 +1 @@ +localize,english \ No newline at end of file diff --git a/data/zone_source/ens_h1_mod_common.csv b/data/zone_source/ens_h1_mod_common.csv new file mode 100644 index 00000000..220210d8 --- /dev/null +++ b/data/zone_source/ens_h1_mod_common.csv @@ -0,0 +1 @@ +localize,english_safe \ No newline at end of file diff --git a/data/zone_source/fra_h1_mod_common.csv b/data/zone_source/fra_h1_mod_common.csv new file mode 100644 index 00000000..9b2dbec0 --- /dev/null +++ b/data/zone_source/fra_h1_mod_common.csv @@ -0,0 +1 @@ +localize,french \ No newline at end of file diff --git a/data/zone_source/fra_h1_mod_common_mp.csv b/data/zone_source/fra_h1_mod_common_mp.csv new file mode 100644 index 00000000..9b2dbec0 --- /dev/null +++ b/data/zone_source/fra_h1_mod_common_mp.csv @@ -0,0 +1 @@ +localize,french \ No newline at end of file diff --git a/data/zone_source/h1_mod_common.csv b/data/zone_source/h1_mod_common.csv new file mode 100644 index 00000000..b69a5031 --- /dev/null +++ b/data/zone_source/h1_mod_common.csv @@ -0,0 +1,2 @@ +localize,english +ttf,fonts/default.otf \ No newline at end of file diff --git a/data/zone_source/ita_h1_mod_common.csv b/data/zone_source/ita_h1_mod_common.csv new file mode 100644 index 00000000..1705a41b --- /dev/null +++ b/data/zone_source/ita_h1_mod_common.csv @@ -0,0 +1 @@ +localize,italian \ No newline at end of file diff --git a/data/zone_source/jpp_h1_mod_common.csv b/data/zone_source/jpp_h1_mod_common.csv new file mode 100644 index 00000000..fd88ef04 --- /dev/null +++ b/data/zone_source/jpp_h1_mod_common.csv @@ -0,0 +1 @@ +localize,japanese_partial \ No newline at end of file diff --git a/data/zone_source/kor_h1_mod_common.csv b/data/zone_source/kor_h1_mod_common.csv new file mode 100644 index 00000000..44b82ad3 --- /dev/null +++ b/data/zone_source/kor_h1_mod_common.csv @@ -0,0 +1 @@ +localize,korean \ No newline at end of file diff --git a/data/zone_source/pol_h1_mod_common.csv b/data/zone_source/pol_h1_mod_common.csv new file mode 100644 index 00000000..e5eb6beb --- /dev/null +++ b/data/zone_source/pol_h1_mod_common.csv @@ -0,0 +1 @@ +localize,polish \ No newline at end of file diff --git a/data/zone_source/por_h1_mod_common.csv b/data/zone_source/por_h1_mod_common.csv new file mode 100644 index 00000000..7fdb2bf6 --- /dev/null +++ b/data/zone_source/por_h1_mod_common.csv @@ -0,0 +1 @@ +localize,portuguese \ No newline at end of file diff --git a/data/zone_source/rus_h1_mod_common.csv b/data/zone_source/rus_h1_mod_common.csv new file mode 100644 index 00000000..6f44d9c7 --- /dev/null +++ b/data/zone_source/rus_h1_mod_common.csv @@ -0,0 +1 @@ +localize,russian \ No newline at end of file diff --git a/data/zone_source/rus_h1_mod_common_mp.csv b/data/zone_source/rus_h1_mod_common_mp.csv new file mode 100644 index 00000000..6f44d9c7 --- /dev/null +++ b/data/zone_source/rus_h1_mod_common_mp.csv @@ -0,0 +1 @@ +localize,russian \ No newline at end of file diff --git a/data/zone_source/sch_h1_mod_common.csv b/data/zone_source/sch_h1_mod_common.csv new file mode 100644 index 00000000..d5a16e93 --- /dev/null +++ b/data/zone_source/sch_h1_mod_common.csv @@ -0,0 +1 @@ +localize,simplified_chinese \ No newline at end of file diff --git a/data/zone_source/spa_h1_mod_common.csv b/data/zone_source/spa_h1_mod_common.csv new file mode 100644 index 00000000..c3d7c1f2 --- /dev/null +++ b/data/zone_source/spa_h1_mod_common.csv @@ -0,0 +1 @@ +localize,spanish \ No newline at end of file diff --git a/data/zone_source/tch_h1_mod_common.csv b/data/zone_source/tch_h1_mod_common.csv new file mode 100644 index 00000000..201d2000 --- /dev/null +++ b/data/zone_source/tch_h1_mod_common.csv @@ -0,0 +1 @@ +localize,traditional_chinese \ No newline at end of file diff --git a/data/zonetool/fra_h1_mod_common_mp/localizedstrings/french.json b/data/zonetool/fra_h1_mod_common_mp/localizedstrings/french.json new file mode 100644 index 00000000..e1886c63 --- /dev/null +++ b/data/zonetool/fra_h1_mod_common_mp/localizedstrings/french.json @@ -0,0 +1,33 @@ +{ + "LUA_MENU_SERVERLIST": "LISTE DES SERVEURS", + "MENU_NUMPLAYERS": "Joueurs", + "MENU_PING": "Latence", + "SERVERLIST_PLAYER_COUNT": "&&1 Joueurs", + "SERVERLIST_SERVER_COUNT": "&&1 Serveurs", + + "LUA_MENU_STATS": "Stats", + "LUA_MENU_STATS_DESC": "Modifier les paramètres des statistiques du joueur.", + "LUA_MENU_UNLOCKALL_ITEMS": "Déverrouiller tous les éléments", + "LUA_MENU_UNLOCKALL_ITEMS_DESC": "Si les éléments doivent être verrouillés en fonction des statistiques du joueur ou toujours déverrouillés.", + "LUA_MENU_UNLOCKALL_LOOT": "Débloquez tout le butin", + "LUA_MENU_UNLOCKALL_LOOT_DESC": "Si le butin doit être verrouillé en fonction des statistiques du joueur ou toujours déverrouillé.", + "LUA_MENU_UNLOCKALL_CLASSES": "Débloquer toutes les classes", + "LUA_MENU_UNLOCKALL_CLASSES_DESC": "Si les classes doivent être verrouillées en fonction des statistiques du joueur ou toujours déverrouillées.", + "LUA_MENU_PRESTIGE": "Prestige", + "LUA_MENU_PRESTIGE_DESC": "Modifier le niveau de prestige.", + "LUA_MENU_RANK": "Grade", + "LUA_MENU_RANK_DESC": "Modifier le grade.", + "LUA_MENU_UNSAVED_CHANGES": "Vous avez des modifications non enregistrées, êtes-vous sûr de vouloir quitter ?", + "LUA_MENU_SAVE": "Sauvegarder les modifications", + "LUA_MENU_SAVE_DESC": "Sauvegarder les modifications.", + "LUA_MENU_SETTINGS": "Paramètres", + "LUA_MENU_EDIT_STATS": "Modifier les statistiques", + + "UPDATER_POPUP_NO_UPDATES_AVAILABLE": "Aucune mise à jour disponible", + "UPDATER_POPUP_AVAILABLE_UPDATE_TEXT": "Une mise à jour est disponible,\npoursuivre l'installation ?", + "UPDATER_POPUP_SUCCESSFUL": "Mise à jour réussie", + "UPDATER_POPUP_RESTART_POPUP_TEXT": "La mise à jour nécessite un redémarrage", + "UPDATER_POPUP_CHECKING_FOR_UPDATES": "Vérification des mises à jour...", + + "PLATFORM_SYSTEM_LINK_TITLE": "LISTE DES SERVEURS" +} \ No newline at end of file diff --git a/data/zonetool/h1_mod_common/fonts/default.otf b/data/zonetool/h1_mod_common/fonts/default.otf new file mode 100644 index 00000000..ad4f12ef Binary files /dev/null and b/data/zonetool/h1_mod_common/fonts/default.otf differ diff --git a/data/zonetool/h1_mod_common/localizedstrings/english.json b/data/zonetool/h1_mod_common/localizedstrings/english.json new file mode 100644 index 00000000..caebe8bb --- /dev/null +++ b/data/zonetool/h1_mod_common/localizedstrings/english.json @@ -0,0 +1,94 @@ +{ + "CUSTOM_DEPOT_EULA_1": "Dear User,", + "CUSTOM_DEPOT_EULA_2": "By using this feature, you acknowledge that you are over the age of 18 years old, and that any sort of gambling is allowed in your country. (even if they do not involve real money)", + "CUSTOM_DEPOT_EULA_3": "The H1-mod team is not responsible if you break any law within your country, and the sole responsibility will be upon you to respect the same.", + "CUSTOM_DEPOT_EULA_4": "The H1-mod team will never include real money transactions within the modified systems. The only way to get currency, should you wish to, is by playing the game.", + "CUSTOM_DEPOT_EULA_5": "Best regards,", + "CUSTOM_DEPOT_EULA_6": "The H1-mod team.", + + "LUA_MENU_FPS": "FPS Counter", + "LUA_MENU_FPS_DESC": "Show FPS counter.", + "LUA_MENU_LATENCY": "Server Latency", + "LUA_MENU_LATENCY_DESC": "Show server latency.", + "LUA_MENU_RED_DOT_BRIGHTNESS": "Red Dot Brightness", + "LUA_MENU_RED_DOT_BRIGHTNESS_DESC": "Adjust the brightness of red dot reticles.", + + "MENU_SYSINFO_CUSTOMER_SUPPORT_URL": "https://h1.gg/", + + "MENU_MODS": "MODS", + "MENU_MODS_DESC": "Load installed mods.", + "LUA_MENU_MOD_DESC_DEFAULT": "Load &&1.", + "LUA_MENU_MOD_DESC": "&&1\nAuthor: &&2\nVersion: &&3", + "LUA_MENU_LOADED_MOD": "Loaded mod: ^2&&1", + "LUA_MENU_AVAILABLE_MODS": "Available mods", + "LUA_MENU_UNLOAD": "Unload", + "LUA_MENU_UNLOAD_DESC": "Unload the currently loaded mod.", + + "PLATFORM_SHADER_PRECACHE_ASK": "Would you like to populate the shader cache? It may cause crashes with certain GPUs (e.g. RTX cards) but will improve performance if successful.", + "MENU_NO_DONT_ASK": "No, don't ask me again", + + "LUA_MENU_SERVERLIST": "SERVER LIST", + "MENU_NUMPLAYERS": "Players", + "MENU_PING": "Ping", + "SERVERLIST_PLAYER_COUNT": "&&1 Players", + "SERVERLIST_SERVER_COUNT": "&&1 Servers", + + "LUA_MENU_STATS": "Stats", + "LUA_MENU_STATS_DESC": "Edit player stats settings.", + "LUA_MENU_UNLOCKALL_ITEMS": "Unlock all items", + "LUA_MENU_UNLOCKALL_ITEMS_DESC": "Whether items should be locked based on the player's stats or always unlocked.", + "LUA_MENU_UNLOCKALL_LOOT": "Unlock all loot", + "LUA_MENU_UNLOCKALL_LOOT_DESC": "Whether loot should be locked based on the player's stats or always unlocked.", + "LUA_MENU_UNLOCKALL_CLASSES": "Unlock all classes", + "LUA_MENU_UNLOCKALL_CLASSES_DESC": "Whether classes should be locked based on the player's stats or always unlocked.", + "LUA_MENU_PRESTIGE": "Prestige", + "LUA_MENU_PRESTIGE_DESC": "Edit prestige level.", + "LUA_MENU_RANK": "Rank", + "LUA_MENU_RANK_DESC": "Edit rank.", + "LUA_MENU_UNSAVED_CHANGES": "You have unsaved changes: are you sure you want to exit?", + "LUA_MENU_SAVE": "Save changes", + "LUA_MENU_SAVE_DESC": "Save changes.", + "LUA_MENU_SETTINGS": "Settings", + "LUA_MENU_EDIT_STATS": "Edit Stats", + + "UPDATER_POPUP_NO_UPDATES_AVAILABLE": "No updates available", + "UPDATER_POPUP_AVAILABLE_UPDATE_TEXT": "An update is available, proceed with installation?", + "UPDATER_POPUP_SUCCESSFUL": "Update successful", + "UPDATER_POPUP_RESTART_POPUP_TEXT": "Update requires restart", + "UPDATER_POPUP_CHECKING_FOR_UPDATES": "Checking for updates...", + + "MPHUD_FPS": "FPS: ", + "MPHUD_LATENCY": "Latency: ", + "MPHUD_LATENCY_MS": " ms", + "LUA_MENU_TELEMETRY": "TELEMETRY", + + "LUA_MENU_3RD_PARTY_CONTENT_DESC": "Would you like to install required 3rd-party content for this server? (from &&1)", + + "MENU_ENGLISH": "English", + "MENU_ENGLISH_SAFE": "English (Safe)", + "MENU_FRENCH": "Français", + "MENU_GERMAN": "Deutsch", + "MENU_ITALIAN": "Italiano", + "MENU_JAPANESE_PARTIAL": "日本語(一部)", + "MENU_KOREAN": "한국어", + "MENU_POLISH": "Polski", + "MENU_PORTUGUESE": "Português", + "MENU_RUSSIAN": "Русский", + "MENU_SIMPLIFIED_CHINESE": "简体中文", + "MENU_SPANISH": "Español", + "MENU_TRADITIONAL_CHINESE": "繁體中文", + + "LOCALE_ENGLISH": "English", + "LOCALE_ENGLISH_SAFE": "English (Safe)", + "LOCALE_FRENCH": "French", + "LOCALE_GERMAN": "German", + "LOCALE_ITALIAN": "Italian", + "LOCALE_JAPANESE_PARTIAL": "Japanese (Partial)", + "LOCALE_KOREAN": "Korean", + "LOCALE_POLISH": "Polish", + "LOCALE_PORTUGUESE": "Portuguese", + "LOCALE_RUSSIAN": "Russian", + "LOCALE_SIMPLIFIED_CHINESE": "Simplified Chinese", + "LOCALE_SPANISH": "Spanish", + "LOCALE_TRADITIONAL_CHINESE": "Traditional Chinese" +} \ No newline at end of file diff --git a/data/zonetool/localizedstrings/english.json b/data/zonetool/localizedstrings/english.json new file mode 100644 index 00000000..f0b9b8dc --- /dev/null +++ b/data/zonetool/localizedstrings/english.json @@ -0,0 +1,6 @@ +{ + "LUA_MENU_CAMPAIGN_UNLOCKED_ALL_TITLE": "Unlock All Missions and Intel", + "LUA_MENU_CANCEL_UNLOCK_CAPS": "Cancel Unlock All Missions", + "LUA_MENU_CHOOSE_LANGUAGE_DESC": "Choose your language.", + "MENU_APPLY_LANGUAGE_SETTINGS": "Apply language settings?" +} \ No newline at end of file diff --git a/data/zonetool/localizedstrings/english_safe.json b/data/zonetool/localizedstrings/english_safe.json new file mode 100644 index 00000000..f0b9b8dc --- /dev/null +++ b/data/zonetool/localizedstrings/english_safe.json @@ -0,0 +1,6 @@ +{ + "LUA_MENU_CAMPAIGN_UNLOCKED_ALL_TITLE": "Unlock All Missions and Intel", + "LUA_MENU_CANCEL_UNLOCK_CAPS": "Cancel Unlock All Missions", + "LUA_MENU_CHOOSE_LANGUAGE_DESC": "Choose your language.", + "MENU_APPLY_LANGUAGE_SETTINGS": "Apply language settings?" +} \ No newline at end of file diff --git a/data/zonetool/localizedstrings/french.json b/data/zonetool/localizedstrings/french.json new file mode 100644 index 00000000..f32229e3 --- /dev/null +++ b/data/zonetool/localizedstrings/french.json @@ -0,0 +1,51 @@ +{ + "LUA_MENU_FPS": "Compteur d'IPS", + "LUA_MENU_FPS_DESC": "Afficher le compteur d'IPS.", + "LUA_MENU_LATENCY": "Latence du serveur", + "LUA_MENU_LATENCY_DESC": "Afficher la latence du serveur", + "LUA_MENU_RED_DOT_BRIGHTNESS": "Luminosité du point rouge", + "LUA_MENU_RED_DOT_BRIGHTNESS_DESC": "Ajustez la luminosité du point rouge des réticules.", + + "LUA_MENU_CAMPAIGN_UNLOCKED_ALL_TITLE": "Débloquer toutes les missions", + "LUA_MENU_CANCEL_UNLOCK_CAPS": "Annuler déblocage", + + "MENU_MODS": "MODS", + "MENU_MODS_DESC": "Charger les mods installés.", + "LUA_MENU_MOD_DESC_DEFAULT": "Charger &&1.", + "LUA_MENU_MOD_DESC": "&&1\nAuteur: &&2\nVersion: &&3", + "LUA_MENU_LOADED_MOD": "Mod chargé: ^2&&1", + "LUA_MENU_AVAILABLE_MODS": "Mods disponibles", + "LUA_MENU_UNLOAD": "Décharger", + "LUA_MENU_UNLOAD_DESC": "Déchargez le mod actuellement chargé.", + + "PLATFORM_SHADER_PRECACHE_ASK": "Souhaitez-vous remplir le cache de shader ? Cela peut provoquer des plantages avec certains GPU (par exemple, les cartes RTX), mais améliorera les performances en cas de succès.", + "MENU_NO_DONT_ASK": "Non, ne plus me le demander", + + "UPDATER_POPUP_NO_UPDATES_AVAILABLE": "Aucune mise à jour disponible", + "UPDATER_POPUP_AVAILABLE_UPDATE_TEXT": "Une mise à jour est disponible,\npoursuivre l'installation ?", + "UPDATER_POPUP_SUCCESSFUL": "Mise à jour réussie", + "UPDATER_POPUP_RESTART_POPUP_TEXT": "La mise à jour nécessite un redémarrage", + "UPDATER_POPUP_CHECKING_FOR_UPDATES": "Vérification des mises à jour...", + + "MPHUD_FPS": "IPS: ", + "MPHUD_LATENCY": "Latence: ", + "MPHUD_LATENCY_MS": " ms", + "LUA_MENU_TELEMETRY": "TÉLÉMÉTRIE", + + "LOCALE_ENGLISH": "Anglais", + "LOCALE_ENGLISH_SAFE": "Anglais (sécuritaire)", + "LOCALE_FRENCH": "Français", + "LOCALE_GERMAN": "Allemand", + "LOCALE_ITALIAN": "Italien", + "LOCALE_JAPANESE_PARTIAL": "Japonais (partiel)", + "LOCALE_KOREAN": "Coréen", + "LOCALE_POLISH": "Polonais", + "LOCALE_PORTUGUESE": "Portugais", + "LOCALE_RUSSIAN": "Russe", + "LOCALE_SIMPLIFIED_CHINESE": "Chinois simplifié", + "LOCALE_SPANISH": "Espagnol", + "LOCALE_TRADITIONAL_CHINESE": "Chinois traditionnel", + + "LUA_MENU_CHOOSE_LANGUAGE": "Choisissez la langue", + "LUA_MENU_CHOOSE_LANGUAGE_DESC": "Choisissez la langue." +} \ No newline at end of file diff --git a/data/zonetool/localizedstrings/german.json b/data/zonetool/localizedstrings/german.json new file mode 100644 index 00000000..d73eb817 --- /dev/null +++ b/data/zonetool/localizedstrings/german.json @@ -0,0 +1,17 @@ +{ + "LOCALE_ENGLISH": "Englisch", + "LOCALE_ENGLISH_SAFE": "Englisch (Sicher)", + "LOCALE_FRENCH": "Französisch", + "LOCALE_GERMAN": "Deutsch", + "LOCALE_ITALIAN": "Italienisch", + "LOCALE_JAPANESE_PARTIAL": "Japanisch (Untertitelt)", + "LOCALE_KOREAN": "Koreanisch", + "LOCALE_POLISH": "Polnisch", + "LOCALE_PORTUGUESE": "Portugiesisch", + "LOCALE_RUSSIAN": "Russisch", + "LOCALE_SIMPLIFIED_CHINESE": "Vereinfachtes Chinesisch", + "LOCALE_SPANISH": "Spanisch", + "LOCALE_TRADITIONAL_CHINESE": "Traditionelles Chinesisch", + "LUA_MENU_CAMPAIGN_UNLOCKED_ALL_TITLE": "Alle Missionen freischalten", + "LUA_MENU_CANCEL_UNLOCK_CAPS": "Freischalten abbrechen" +} \ No newline at end of file diff --git a/data/zonetool/localizedstrings/italian.json b/data/zonetool/localizedstrings/italian.json new file mode 100644 index 00000000..40344f39 --- /dev/null +++ b/data/zonetool/localizedstrings/italian.json @@ -0,0 +1,26 @@ +{ + "LUA_MENU_CAMPAIGN_UNLOCKED_ALL_TITLE": "Sblocca tutte le missioni", + "LUA_MENU_CANCEL_UNLOCK_CAPS": "Annulla sblocco", + + "MENU_MODS_DESC": "Abilita mod installate.", + "LUA_MENU_MOD_DESC_DEFAULT": "Abilita &&1.", + "LUA_MENU_MOD_DESC": "&&1\nAutore: &&2\nVersione: &&3", + "LUA_MENU_LOADED_MOD": "Mod attiva: ^2&&1", + "LUA_MENU_AVAILABLE_MODS": "Mod disponibili", + "LUA_MENU_UNLOAD": "Disabilita", + "LUA_MENU_UNLOAD_DESC": "Disabilita la mod attualmente attiva.", + + "LOCALE_ENGLISH": "Inglese", + "LOCALE_ENGLISH_SAFE": "Inglese (sicuro)", + "LOCALE_FRENCH": "Francese", + "LOCALE_GERMAN": "Tedesco", + "LOCALE_ITALIAN": "Italiano", + "LOCALE_JAPANESE_PARTIAL": "Giapponese (parziale)", + "LOCALE_KOREAN": "Coreano", + "LOCALE_POLISH": "Polacco", + "LOCALE_PORTUGUESE": "Portoghese", + "LOCALE_RUSSIAN": "Russo", + "LOCALE_SIMPLIFIED_CHINESE": "Cinese semplificato", + "LOCALE_SPANISH": "Spagnolo", + "LOCALE_TRADITIONAL_CHINESE": "Cinese tradizionale" +} \ No newline at end of file diff --git a/data/zonetool/localizedstrings/japanese_partial.json b/data/zonetool/localizedstrings/japanese_partial.json new file mode 100644 index 00000000..a767592f --- /dev/null +++ b/data/zonetool/localizedstrings/japanese_partial.json @@ -0,0 +1,18 @@ +{ + "LOCALE_ENGLISH": "英語", + "LOCALE_ENGLISH_SAFE": "英語(検閲)", + "LOCALE_FRENCH": "フランス語", + "LOCALE_GERMAN": "ドイツ語", + "LOCALE_ITALIAN": "イタリア語", + "LOCALE_JAPANESE_PARTIAL": "日本語(一部)", + "LOCALE_KOREAN": "韓国語", + "LOCALE_POLISH": "ポーランド語", + "LOCALE_PORTUGUESE": "ポルトガル語", + "LOCALE_RUSSIAN": "ロシア語", + "LOCALE_SIMPLIFIED_CHINESE": "簡体字中国語", + "LOCALE_SPANISH": "スペイン語", + "LOCALE_TRADITIONAL_CHINESE": "繁体字中国語", + + "LUA_MENU_CAMPAIGN_UNLOCKED_ALL_TITLE": "全ミッションをアンロック", + "LUA_MENU_CANCEL_UNLOCK_CAPS": "アンロックをキャンセル" +} \ No newline at end of file diff --git a/data/zonetool/localizedstrings/korean.json b/data/zonetool/localizedstrings/korean.json new file mode 100644 index 00000000..0940d164 --- /dev/null +++ b/data/zonetool/localizedstrings/korean.json @@ -0,0 +1,18 @@ +{ + "LOCALE_ENGLISH": "영어", + "LOCALE_ENGLISH_SAFE": "영어(검열)", + "LOCALE_FRENCH": "프랑스어", + "LOCALE_GERMAN": "독일어", + "LOCALE_ITALIAN": "이탈리아어", + "LOCALE_JAPANESE_PARTIAL": "일본어(일부)", + "LOCALE_KOREAN": "한국어", + "LOCALE_POLISH": "폴란드어", + "LOCALE_PORTUGUESE": "포르투갈어", + "LOCALE_RUSSIAN": "러시아어", + "LOCALE_SIMPLIFIED_CHINESE": "중국어(간체)", + "LOCALE_SPANISH": "스페인어", + "LOCALE_TRADITIONAL_CHINESE": "중국어(번체)", + + "LUA_MENU_CAMPAIGN_UNLOCKED_ALL_TITLE": "모든 임무 잠금 해제", + "LUA_MENU_CANCEL_UNLOCK_CAPS": "잠금 해제 취소" +} \ No newline at end of file diff --git a/data/zonetool/localizedstrings/polish.json b/data/zonetool/localizedstrings/polish.json new file mode 100644 index 00000000..125b44e6 --- /dev/null +++ b/data/zonetool/localizedstrings/polish.json @@ -0,0 +1,18 @@ +{ + "LOCALE_ENGLISH": "Angielski", + "LOCALE_ENGLISH_SAFE": "Angielski (cenzuralny)", + "LOCALE_FRENCH": "Francuski", + "LOCALE_GERMAN": "Niemiecki", + "LOCALE_ITALIAN": "Włoski", + "LOCALE_JAPANESE_PARTIAL": "Japoński (częściowy)", + "LOCALE_KOREAN": "Koreański", + "LOCALE_POLISH": "Polski", + "LOCALE_PORTUGUESE": "Portugalski", + "LOCALE_RUSSIAN": "Rosyjski", + "LOCALE_SIMPLIFIED_CHINESE": "Chiński uproszczony", + "LOCALE_SPANISH": "Hiszpański", + "LOCALE_TRADITIONAL_CHINESE": "Chiński tradycyjny", + + "LUA_MENU_CAMPAIGN_UNLOCKED_ALL_TITLE": "Odblokuj wszystkie misje", + "LUA_MENU_CANCEL_UNLOCK_CAPS": "Anuluj odblokowanie" +} \ No newline at end of file diff --git a/data/zonetool/localizedstrings/portuguese.json b/data/zonetool/localizedstrings/portuguese.json new file mode 100644 index 00000000..388e5837 --- /dev/null +++ b/data/zonetool/localizedstrings/portuguese.json @@ -0,0 +1,18 @@ +{ + "LOCALE_ENGLISH": "Inglês", + "LOCALE_ENGLISH_SAFE": "Inglês (Censurado)", + "LOCALE_FRENCH": "Français", + "LOCALE_GERMAN": "Alemão", + "LOCALE_ITALIAN": "Italiano", + "LOCALE_JAPANESE_PARTIAL": "Japonês (Parcial)", + "LOCALE_KOREAN": "Coreano", + "LOCALE_POLISH": "Polonês", + "LOCALE_PORTUGUESE": "Português", + "LOCALE_RUSSIAN": "Russo", + "LOCALE_SIMPLIFIED_CHINESE": "Chinês simplificado", + "LOCALE_SPANISH": "Español", + "LOCALE_TRADITIONAL_CHINESE": "Chinês tradicional", + + "LUA_MENU_CAMPAIGN_UNLOCKED_ALL_TITLE": "Desbloquear todas as missões", + "LUA_MENU_CANCEL_UNLOCK_CAPS": "Cancelar desbloqueio" +} \ No newline at end of file diff --git a/data/zonetool/localizedstrings/russian.json b/data/zonetool/localizedstrings/russian.json new file mode 100644 index 00000000..5ac4fe31 --- /dev/null +++ b/data/zonetool/localizedstrings/russian.json @@ -0,0 +1,242 @@ +{ + "LUA_MENU_FPS": "Счетчик кадров", + "LUA_MENU_FPS_DESC": "Показывать частоту кадров в секунду (FPS).", + "LUA_MENU_LATENCY": "Задержка до сервера", + "LUA_MENU_LATENCY_DESC": "Показывать пинг до сервера.", + "LUA_MENU_RED_DOT_BRIGHTNESS": "Яркость коллиматора", + "LUA_MENU_RED_DOT_BRIGHTNESS_DESC": "Регулировка яркости красной точки коллиматорных прицелов.", + + "LUA_MENU_CAMPAIGN_UNLOCKED_ALL_TITLE": "Открыть все задания и чит-коды", + "LUA_MENU_CANCEL_UNLOCK_CAPS": "Вернуться к своему прогрессу прохождения", + "LUA_MENU_CHOOSE_LANGUAGE": "Выбор языка", + "LUA_MENU_CHOOSE_LANGUAGE_DESC": "Поменять язык интерфейса и озвучки.", + + "MENU_MODS": "МОДЫ", + "MENU_MODS_DESC": "Запуск установленных модов.", + "LUA_MENU_MOD_DESC_DEFAULT": "Запустить &&1.", + "LUA_MENU_MOD_DESC": "&&1\nАвтор: &&2\nВерсия: &&3.", + "LUA_MENU_LOADED_MOD": "Запущенный мод: ^3&&1", + "LUA_MENU_AVAILABLE_MODS": "Доступные моды", + "LUA_MENU_UNLOAD": "Выгрузить", + "LUA_MENU_UNLOAD_DESC": "Выгрузить из игры запущенный сейчас мод.", + + "PLATFORM_SHADER_PRECACHE_ASK": "Хотите скомпилировать шейдеры? С некоторыми видеокартами (например, серии GeForce RTX, GTX 16xx) это может привести к вылетам игры, но в общем случае повысит производительность.", + "MENU_NO_DONT_ASK": "Нет, больше не спрашивать", + + "UPDATER_POPUP_NO_UPDATES_AVAILABLE": "У вас установлены все последние обновления", + "UPDATER_POPUP_AVAILABLE_UPDATE_TEXT": "Доступно обновление клиента игры,\nначать установку сейчас?", + "UPDATER_POPUP_SUCCESSFUL": "Обновление завершено", + "UPDATER_POPUP_RESTART_POPUP_TEXT": "Для применения изменений необходим перезапуск игры", + "UPDATER_POPUP_CHECKING_FOR_UPDATES": "Проверка наличия обновлений...", + + "MPHUD_FPS": "К/С: ", + "MPHUD_LATENCY": "Задержка: ", + "MPHUD_LATENCY_MS": " мс", + "LUA_MENU_TELEMETRY": "ТЕЛЕМЕТРИЯ", + + "LOCALE_ENGLISH": "Английский", + "LOCALE_ENGLISH_SAFE": "Английский цензурный", + "LOCALE_FRENCH": "Французский", + "LOCALE_GERMAN": "Немецкий", + "LOCALE_ITALIAN": "Итальянский", + "LOCALE_JAPANESE_PARTIAL": "Японский (английская озвучка)", + "LOCALE_KOREAN": "Корейский", + "LOCALE_POLISH": "Польский", + "LOCALE_PORTUGUESE": "Португальский", + "LOCALE_RUSSIAN": "Русский", + "LOCALE_SIMPLIFIED_CHINESE": "Китайский упрощенный", + "LOCALE_SPANISH": "Испанский", + "LOCALE_TRADITIONAL_CHINESE": "Китайский традиционный", + "LUA_MENU_DOWNLOAD": "Скачать", + + "MPUI_MP44": "MP-44", + "WEAPON_AT4": "AT4", + "WEAPON_BARRETT": "Barrett .50", + "WEAPON_BERETTA": "M9", + "WEAPON_COLT1911": "M1911 .45", + "WEAPON_COLT45": "M1911 .45", + "WEAPON_DESERTEAGLE55": "Командирский Дезерт Игл", + "WEAPON_DESERTEAGLEGOLD": "Золотой Дезерт Игл", + "WEAPON_HK79": "HK79", + "WEAPON_MELEESHOVEL": "Могильщик", + "WEAPON_MP44": "MP-44", + "WEAPON_MP5_SILENCER": "MP5 с глушителем", + "WEAPON_NO_AMMO_CAPS": "НЕТ ПАТРОНОВ", + "WEAPON_NO_FRAG_GRENADE": "Осколочных гранат не осталось", + "WEAPON_NO_SPECIAL_GRENADE": "Особых гранат не осталось", + "WEAPON_P90_SILENCER": "P90 с глушителем", + "WEAPON_SILENCER_ATTACHMENT": "с глушителем", + "WEAPON_USP": "USP .45", + + "ARMADA_INTRO": "Чарли не сëрфят", + "CGAME_CONTINUE_SAVING": "Сохранить и выйти", + "CGAME_MISSIONOBJECTIVES": "ЦЕЛИ ЗАДАНИЯ", + "CGAME_PRONE_BLOCKED": "Движение лежа заблокировано", + "CGAME_PRONE_BLOCKED_WEAPON": "С этим оружием нельзя лечь", + "CGAME_RESTART_WARNING": "Если начать игру заново, \nвесь прогресс в текущем \nзадании будет утрачен\n\nНачать заново?", + "CGAME_SAVE_WARNING": "Если вы сохраните игру сейчас,\nвесь прогресс с момента последней\nконтрольной точки будет утрачен\n\nСохранить игру?", + "CGAME_TEAMMATE": "НАПАРНИК", + "CGAME_UNKNOWN": "неизвестно", + "EXE_INVALIDUPDATESERVERDOWNLOAD": "Загруженное обновление повреждено", + "EXE_KEYWAIT": "Нажмите ESC для отмены или BACKSPACE для сброса", + "EXE_SHADERPRELOAD": "Подгрузка шейдеров... &&1%", + "EXE_YES": "Да", + "GAME_CHEATSNOTENABLED": "На этом сервере отключены чит-коды.", + "GAME_CROUCH_BLOCKED": "Здесь нельзя пригнуться", + "GAME_DIFFICULTY_HARD": "Сложность: Закаленный", + "GAME_DIFFICULTY_MEDIUM": "Сложность: Рядовой", + "GAME_DIFFICULTY_UNKNOWN": "Сложность: Неизвестна", + "GAME_OBJECTIVECOMPLETED": "Задача выполнена.", + "GAME_OBJECTIVEFAILED": "Цель не достигнута.", + "GAME_OBJECTIVESUPDATED": "Новая цель.", + "KEY_COMMAND": "Command", + "KEY_ENTER": "Enter", + "KEY_ESCAPE": "Escape", + "KEY_KP_MINUS": "- (цифр.)", + "KEY_KP_PLUS": "+ (цифр.)", + "KEY_KP_STAR": "* (цифр.)", + "KEY_MOUSE1": "ЛКМ", + "KEY_MOUSE2": "ПКМ", + "KEY_MOUSE3": "СКМ", + "KEY_USE": "использовать", + "LUA_MENU_ADVANCED_VIDEO": "Параметры графики", + "LUA_MENU_AIM_ASSIST_LOCKON_DESC": "Поворот оружия в сторону цели во время движения.", + "LUA_MENU_AIM_ASSIST_SLOWDOWN_DESC": "Замедление движений прицела при наведении оружия на цель.", + "LUA_MENU_AUTO_MANTLE_DESC": "Автоматически цепляться за уступ при прыжке рядом с ним.", + "LUA_MENU_BOTS_REGULAR": "Рядовой", + "LUA_MENU_COLOR_BLIND_DESC": "Включение и отключение цветовой схемы для людей с пониженной чувствительностью к цвету.", + "LUA_MENU_COMPLETE": "ЗАВЕРШЕНО", + "LUA_MENU_DATE": "&&2.&&1.&&3", + "LUA_MENU_DAYS": "&&1 д", + "LUA_MENU_DAYS_HOURS_MINUTES_SECONDS": "&&1 д &&2 ч &&3 м &&4 с", + "LUA_MENU_DEFENDS": "Защиты", + "LUA_MENU_DEFENDS_CAPS": "ЗАЩИТЫ", + "LUA_MENU_DISPLAY_OPTIONS": "Интерфейс", + "LUA_MENU_END_GAME": "Выйти из боя", + "LUA_MENU_GAME_SETUP": "Параметры боя", + "LUA_MENU_GAME_SETUP_CAPS": "ПАРАМЕТРЫ БОЯ", + "LUA_MENU_GRAPHICS": "Изображение", + "LUA_MENU_GRAPHIC_OPTIONS": "Изображение", + "LUA_MENU_LEAVE_GAME_TITLE": "Покинуть бой?", + "LUA_MENU_MODE_WINDOWED_NO_BORDER": "В окне (без границ)", + "LUA_MENU_OPTIMAL_VIDEO_AUDIO": "Сбросить настройки изображения", + "LUA_MENU_OPTIONS_UPPER_CASE": "НАСТРОЙКИ", + "LUA_MENU_RESTORE_EACH_SETTING": "Вернуть параметры управления к значениям по умолчанию?", + "LUA_MENU_RESTRICTIONS_TACTICAL_CAPS": "ОГРАНИЧЕНИЯ НА ТАКТИЧЕСКОЕ ОРУЖИЕ", + "LUA_MENU_RESTRICT_TACTICAL": "Ограничение на тактическое оружие", + "LUA_MENU_SYSTEM_INFO": "Другое", + "LUA_MENU_SYSTEM_INFO_CAPS": "ДРУГОЕ", + "LUA_MENU_VIDEO_OPTIONS": "Отображение", + "LUA_MENU_VIDEO_OPTIONS_CAPS": "ОТОБРАЖЕНИЕ", + "MENU_ACT_I": "Акт I", + "MENU_ACT_II": "Акт II", + "MENU_ACT_III": "Акт III", + "MENU_ADVANCED_VIDEO": "Параметры графики", + "MENU_AIM_DOWN_THE_SIGHT": "Прицелиться", + "MENU_AIM_DOWN_THE_SIGHT_AUTOAIM": "Автонаведение при прицеливании", + "MENU_APPLY_SETTINGS": "Применить новые значения?", + "MENU_AUTOAIM": "Автонаведение", + "MENU_BRIGHTNESS": "Яркость изображения", + "MENU_BUTTON_LAYOUT": "Раскладка кнопок", + "MENU_CCS_RESTART_BUTTON_LABEL": "Перезапустить", + "MENU_CHEAT_ENABLED": "Чит-код активирован", + "MENU_COMPLETED": "Пройдено", + "MENU_COMPLETED_CHEAT": "Доступен чит-код", + "MENU_COMPLETED_HARDENED": "Пройдено (Закаленный)", + "MENU_COMPLETED_REGULAR": "Пройдено (Рядовой)", + "MENU_COMPLETED_SKILLED": "Пройдено (Опытный)", + "MENU_COMPLETED_VETERAN": "Пройдено (Ветеран)", + "MENU_CORRUPT_SAVEDATA_MESSAGE": "Сохраненные данные не были загружены, поскольку они повреждены. В случае продолжения они будут удалены.", + "MENU_CUSTOM": "Свои", + "MENU_CUSTOM_N": "Свой (&&1)", + "MENU_DEFAULT_ALT": "Станд. перевернутая", + "MENU_DIFFICULTY_HARDENED": "Сложность: Закаленный", + "MENU_DIFFICULTY_REGULAR": "Сложность: Рядовой", + "MENU_DIFFICULTY_WARNING": "Вам рекомендуется другой уровень сложности. Хотите продолжить на этом?", + "MENU_DISPLAY_MODE": "Режим вывода", + "MENU_DOF": "Глубина резкости", + "MENU_EXTRA": "Ультра", + "MENU_FILL_MEMORY_TEXTURES": "Заполнить оставшуюся память", + "MENU_FIRE_RATE": "Темп стрельбы:", + "MENU_FRAG_EQUIPMENT": "Граната/снаряжение", + "MENU_FREE_LOOK": "Своб. обзор", + "MENU_FRIENDLY_FIRE": "Огонь по своим: ", + "MENU_GO_TO_CROUCH": "Пригнуться", + "MENU_GO_TO_PRONE": "Лечь", + "MENU_GRAPHICS": "Изображение", + "MENU_HARDENED": "Закаленный", + "MENU_HIGH": "Высок.", + "MENU_INSANE": "Безумн.", + "MENU_INSPECT_WEAPON": "Осмотреть оружие", + "MENU_INTEL": "ЧИТ-КОДЫ", + "MENU_JUMP_STANCE_UP": "Прыгнуть/Подняться", + "MENU_KILLS": "Убийства", + "MENU_LARGE": "Больш.", + "MENU_LAST_CHECKPOINT": "Посл. контрольная точка", + "MENU_LAUNCH_WITHOUT_MODS": "Запустить без модов", + "MENU_LEGACY": "Классика", + "MENU_LOAD_MISSION": "Загрузить задание?", + "MENU_LOOK_INVERSION": "Инверсия обзора", + "MENU_LOWER_DIFFICULTY": "Понизить сложность", + "MENU_NORMAL_MAP_RESOLUTION": "Разрешение карт нормалей", + "MENU_NO_CONTROLLER_INITIAL": "У вас не подключен геймпад. Переключиться на схему управления клавиатурой и мышью?", + "MENU_OPTIONS": "Настройки", + "MENU_OPTIONS_UPPER_CASE": "НАСТРОЙКИ", + "MENU_PAUSED_CAP": "ПАУЗА", + "MENU_QUIT": "Выйти", + "MENU_RECRUIT": "Новобранец", + "MENU_REGULAR": "Рядовой", + "MENU_REGULAR_CAPS": "РЯДОВОЙ", + "MENU_RESET_SYSTEM_DEFAULTS": "Оптимальные настройки игры", + "MENU_RESTART_LEVEL_Q": "Начать уровень сначала?", + "MENU_RESTORE_DEFAULTS": "Системные настройки будут возвращены к значениям по умолчанию, продолжить?", + "MENU_RESTORE_EACH_SETTING": "Все параметры будут возвращены к значениям по умолчанию, продолжить?", + "MENU_RESUMEGAME_NOSAVE": "Продолжить без сохранения", + "MENU_RESUMEGAME_Q_DESC": "Хотите возобновить прохождение задания?", + "MENU_RESUME_CREDITS": "Продолжить", + "MENU_SAVEDATA_CORRUPTED": "Невозможно возобновить игру, т.к. поврежден файл сохранения. Пожалуйста, перезапустите уровень из меню выбора задания.", + "MENU_SCREENSHOT": "Скриншот", + "MENU_SELECT_DIFFICULTY": "Выбор сложности", + "MENU_SELECT_GAME_TYPE": "Выбрать режим игры", + "MENU_SELECT_LEVEL": "Выбрать уровень", + "MENU_SELECT_NEXT_MISSION": "Выбрать следующее задание", + "MENU_SPECULAR_MAP": "Карта бликов", + "MENU_SPECULAR_MAP_RESOLUTION": "Разрешение карт бликов", + "MENU_SPRINT_HOLD_BREATH": "Бег/Задержка дыхания", + "MENU_SPRINT_STEADY_SNIPER_RIFLE": "Бег/Удержание прицела", + "MENU_SP_H1_ARMADA": "Чарли не сëрфят", + "MENU_STANDARD_4_3": "Стандартное 4:3", + "MENU_TEXTURE_RESOLUTION": "Разрешение текстур", + "MENU_UNLOCK": "Открыть", + "MENU_VERY_LOW": "Очень низк.", + "MENU_VIDEO": "Отображение", + "MENU_WARNING": "Внимание", + "MENU_WARNING_CHECKPOINT_RESET_TITLE": "Откат к началу задания", + "MENU_WIDE_16_10": "Широкое 16:10", + "MENU_WIDE_16_9": "Широкое 16:9", + "MENU_WIDE_21_9": "Сверхширокое 21:9", + "MENU_YES": "Да", + "PLATFORM_FOV": "Угол обзора (FOV)", + "PLATFORM_HOLD_TO_SKIP": "Удерживайте \u0001 для пропуска", + "PLATFORM_HOLD_TO_SKIP_KEYBOARD": "Удерживайте ^2ENTER^7 для пропуска\n", + "PLATFORM_LOW_AMMO_NO_RELOAD": "Мало боеприпасов", + "PLATFORM_LOW_AMMO_NO_RELOAD_CAPS": "МАЛО БОЕПРИПАСОВ", + "PLATFORM_MDAO": "Затенение методом MDAO", + "PLATFORM_PLAY_ONLINE": "Сетевая игра", + "PLATFORM_RELOAD_CAPS": "ПЕРЕЗАРЯДКА", + "PLATFORM_SSAO": "Затенение методом SSAO", + "PLATFORM_UI_ADAPTER": "Видеокарта", + "PLATFORM_UI_ANTI_ALIASING_OPTIONS": "Настройки сглаживания", + "PLATFORM_UI_CACHED_SPOT_SHADOWS": "Кэшировать точечные тени", + "PLATFORM_UI_CACHED_SUN_SHADOWS": "Кэшировать тени от солнца", + "PLATFORM_UI_DEDICATED_VIDEO_MEMORY": "Загрузка видеопамяти", + "PLATFORM_UI_IMAGE_QUALITY": "Разрешение картинки", + "PLATFORM_UI_NATIVE_RENDER_RESOLUTION": "Отрисовка в родном разрешении", + "PLATFORM_UI_NATIVE_RENDER_RESOLUTION_OPTION": "Родное (&&1 x &&2)", + "PLATFORM_UI_POST_AA": "Постобработка", + "PLATFORM_UI_SHADER_PRELOAD_AFTER_CINEMATIC": "Во время роликов", + "PLATFORM_UI_VIDEO_ADAPTER": "Видеокарта", + "PLATFORM_YES": "Да", + "PRESENCE_SP_ARMADA": "Чарли не сëрфят", + "PRESENCE_SP_ARMADA_SYSTEM_DIALOG": "Чарли не сëрфят" +} \ No newline at end of file diff --git a/data/zonetool/localizedstrings/simplified_chinese.json b/data/zonetool/localizedstrings/simplified_chinese.json new file mode 100644 index 00000000..07264d09 --- /dev/null +++ b/data/zonetool/localizedstrings/simplified_chinese.json @@ -0,0 +1,18 @@ +{ + "LOCALE_ENGLISH": "英语", + "LOCALE_ENGLISH_SAFE": "英语 (审查制度)", + "LOCALE_FRENCH": "法语", + "LOCALE_GERMAN": "德语", + "LOCALE_ITALIAN": "意大利语", + "LOCALE_JAPANESE_PARTIAL": "日语(部分)", + "LOCALE_KOREAN": "韩语", + "LOCALE_POLISH": "波兰语", + "LOCALE_PORTUGUESE": "葡萄牙语", + "LOCALE_RUSSIAN": "俄语", + "LOCALE_SIMPLIFIED_CHINESE": "简体中文", + "LOCALE_SPANISH": "西班牙语", + "LOCALE_TRADITIONAL_CHINESE": "繁体中文", + + "LUA_MENU_CAMPAIGN_UNLOCKED_ALL_TITLE": "解锁全部任务", + "LUA_MENU_CANCEL_UNLOCK_CAPS": "取消解锁" +} \ No newline at end of file diff --git a/data/zonetool/localizedstrings/spanish.json b/data/zonetool/localizedstrings/spanish.json new file mode 100644 index 00000000..4fcdf805 --- /dev/null +++ b/data/zonetool/localizedstrings/spanish.json @@ -0,0 +1,18 @@ +{ + "LOCALE_ENGLISH": "Inglés", + "LOCALE_ENGLISH_SAFE": "Inglés (censura)", + "LOCALE_FRENCH": "Français", + "LOCALE_GERMAN": "Alemán", + "LOCALE_ITALIAN": "Italiano", + "LOCALE_JAPANESE_PARTIAL": "Japonés (parcial)", + "LOCALE_KOREAN": "Coreano", + "LOCALE_POLISH": "Polaco", + "LOCALE_PORTUGUESE": "Portugués", + "LOCALE_RUSSIAN": "Ruso", + "LOCALE_SIMPLIFIED_CHINESE": "Chino simplificado", + "LOCALE_SPANISH": "Español", + "LOCALE_TRADITIONAL_CHINESE": "Chino tradicional", + + "LUA_MENU_CAMPAIGN_UNLOCKED_ALL_TITLE": "Desbloquear todas las misiones", + "LUA_MENU_CANCEL_UNLOCK_CAPS": "Cancelar desbloqueo" +} \ No newline at end of file diff --git a/data/zonetool/localizedstrings/traditional_chinese.json b/data/zonetool/localizedstrings/traditional_chinese.json new file mode 100644 index 00000000..526ed6da --- /dev/null +++ b/data/zonetool/localizedstrings/traditional_chinese.json @@ -0,0 +1,18 @@ +{ + "LOCALE_ENGLISH": "英文", + "LOCALE_ENGLISH_SAFE": "英文 (審查制度)", + "LOCALE_FRENCH": "法文", + "LOCALE_GERMAN": "德文", + "LOCALE_ITALIAN": "義大利文", + "LOCALE_JAPANESE_PARTIAL": "日文(部份)", + "LOCALE_KOREAN": "韓文", + "LOCALE_POLISH": "波蘭文", + "LOCALE_PORTUGUESE": "葡萄牙文", + "LOCALE_RUSSIAN": "俄文", + "LOCALE_SIMPLIFIED_CHINESE": "簡體中文", + "LOCALE_SPANISH": "西班牙文", + "LOCALE_TRADITIONAL_CHINESE": "繁體中文", + + "LUA_MENU_CAMPAIGN_UNLOCKED_ALL_TITLE": "解鎖所有任務", + "LUA_MENU_CANCEL_UNLOCK_CAPS": "取消解鎖" +} \ No newline at end of file diff --git a/data/zonetool/rus_h1_mod_common_mp/localizedstrings/russian.json b/data/zonetool/rus_h1_mod_common_mp/localizedstrings/russian.json new file mode 100644 index 00000000..e0e43212 --- /dev/null +++ b/data/zonetool/rus_h1_mod_common_mp/localizedstrings/russian.json @@ -0,0 +1,354 @@ +{ + "CUSTOM_DEPOT_EULA_1": "Уважаемый игрок,", + "CUSTOM_DEPOT_EULA_2": "Используя эту функцию, вы подтверждаете, что вам исполнилось 18 лет и что в вашей стране разрешены азартные игры и сюрприз-механики (даже если они в них не вовлечены реальные деньги)", + "CUSTOM_DEPOT_EULA_3": "Команда H1-mod не несет ответственности, если вы нарушите какой-либо закон у себя в стране, и вы несете исключительную ответственность за соблюдение правил.", + "CUSTOM_DEPOT_EULA_4": "Команда H1-mod никогда не добавит микротранзакции с реальными деньгами в свой мод. Единственный способ получить внутриигровую валюту, если вы того захотите, – это играть в игру.", + "CUSTOM_DEPOT_EULA_5": "С наилучшими пожеланиями,", + "CUSTOM_DEPOT_EULA_6": "Команда H1-mod.", + + "LUA_MENU_SERVERLIST": "Список серверов", + "MENU_NUMPLAYERS": "Игроки [+боты]", + "MENU_PING": "Пинг", + "SERVERLIST_PLAYER_COUNT": "Игроков: &&1", + "SERVERLIST_SERVER_COUNT": "Серверов: &&1", + + "LUA_MENU_STATS": "Статистика", + "LUA_MENU_STATS_DESC": "Изменение статистических показателей игрока.", + "LUA_MENU_UNLOCKALL_ITEMS": "Открыть все предметы", + "LUA_MENU_UNLOCKALL_ITEMS_DESC": "Определяет, должны ли камуфляжи и базовые предметы открываться в соответствии со статистикой игрока или всегда быть разблокированы.", + "LUA_MENU_UNLOCKALL_LOOT": "Открыть все трофеи", + "LUA_MENU_UNLOCKALL_LOOT_DESC": "Определяет, должно ли оружие из DLC (не включая DLC-камуфляжи) открываться по правилам или всегда быть разблокировано.", + "LUA_MENU_UNLOCKALL_CLASSES": "Открыть все классы", + "LUA_MENU_UNLOCKALL_CLASSES_DESC": "Определяет, должны ли дополнительные классы открываться при достижении нового уровня престижа или всегда быть разблокированы.", + "LUA_MENU_PRESTIGE": "Престиж", + "LUA_MENU_PRESTIGE_DESC": "Изменение уровня престижа.", + "LUA_MENU_RANK": "Ранг", + "LUA_MENU_RANK_DESC": "Изменение ранга.", + "LUA_MENU_UNSAVED_CHANGES": "Изменения не были сохранены, вы уверены, что хотите выйти?", + "LUA_MENU_SAVE": "Сохранить изменения", + "LUA_MENU_SAVE_DESC": "Внесение изменений в игру.", + "LUA_MENU_SETTINGS": "Параметры", + "LUA_MENU_EDIT_STATS": "Корректировка статистики", + + "LUA_MENU_3RD_PARTY_CONTENT_DESC": "Согласны загрузить сторонний контент, необходимый для игры на этом сервере? (&&1)", + + "PLATFORM_SYSTEM_LINK_TITLE": "СПИСОК СЕРВЕРОВ", + + "EXE_SAY": "^3Всем^7", + "EXE_SAYTEAM": "^5Команде^7", + + "MENU_SB_TOOLTIP_BTN_REFRESH": "Обновить список", + "MENU_TYPE1": "Режим", + "SERVERLIST_ADD_TO_BLACKLIST": "Добавить в ЧС", + "SERVERLIST_REMOVE_FROM_BLACKLIST": "Убрать из ЧС", + "LUI_MENU_BLACKLIST": "Черный список", + + "MPUI_ATDM_RECIPE_NAME": "Usilenie", + "MPUI_BALL_RECIPE_NAME": "Stancija svjazi", + "MPUI_CONF_RECIPE_NAME": "Ubijstvo podtverzhdeno", + "MPUI_CTF_PRO_RECIPE_NAME": "Zahvat flaga PRO", + "MPUI_CTF_RECIPE_NAME": "Zahvat flaga", + "MPUI_DD_RECIPE_NAME": "Unichtozhenie", + "MPUI_DEATHMATCH_RECIPE_NAME": "Kazhdyj za sebja", + "MPUI_DOMINATION_RECIPE_NAME": "Prevoshodstvo", + "MPUI_GUN_RECIPE_NAME": "Oruzhie", + "MPUI_HEADQUARTERS_RECIPE_NAME": "Shtab", + "MPUI_HP_RECIPE_NAME": "Opornyj punkt", + "MPUI_INFECT_RECIPE_NAME": "Zarazhenie", + "MPUI_JUGG_RECIPE_NAME": "Dzhaggernaut", + "MPUI_KINGS_RECIPE_NAME": "Koroli", + "MPUI_KOTM_RECIPE_NAME": "Gora", + "MPUI_OIC_RECIPE_NAME": "Poslednij patron", + "MPUI_SABOTAGE_RECIPE_NAME": "Sabotazh", + "MPUI_SD_RECIPE_NAME": "NiU", + "MPUI_SOTF_FFA_RECIPE_NAME": "Ohota KZS", + "MPUI_SOTF_RECIPE_NAME": "Ohota", + "MPUI_SR_RECIPE_NAME": "NiS", + "MPUI_TWAR_RECIPE_NAME": "Impuls", + "MPUI_WAR_RECIPE_NAME": "Komandnyj boj", + "MPUI_XTDM_RECIPE_NAME": "XTDM", + + "CLASS_SPETSNAZ_CLASSES": "КЛАССЫ СПЕЦНАЗА", + "LOOT_DEC_CHAR_REWARD_02": "ЧАД", + "LOOT_DEC_COSTUME_15": "ЧАД", + "LOOT_DEC_ITEMSET_32_SPECIAL": "ЧАД", + "LUA_MENU_SCOREBOARD_MARINES": "%d · МОРПЕХИ", + "LUA_MENU_SCOREBOARD_MARINES_LOST": "%d · МОРПЕХИ ПРОИГРАЛИ", + "MPUI_MARINES_DESERT": "Морпехи", + "MPUI_MARINES_SHORT": "Морпехи", + "MPUI_OPFOR": "Оппозиция", + "MPUI_OPFOR_SHORT": "Оппозиция", + "MPUI_SPETSNAZ": "Спецназ", + "MPUI_SPETSNAZ_SHORT": "Спецназ", + + "CGAME_COMPLAINTDISMISSED": "Жалоба отклонена", + "CGAME_COMPLAINTFILED": "Жалоба учтена", + "CGAME_COMPLAINTSERVERHOST": "Нельзя пожаловаться на хост", + "CGAME_COMPLAINTTEAMKILLFILE": "Отправить жалобу на игрока &&1 за убийство товарища по команде?", + "CGAME_CONNECTIONINTERUPTED": "Связь прервана", + "CGAME_CRUSH": "раздавлен", + "CGAME_FALLING": "упал", + "CGAME_HEAD_SHOT": "выстрел в голову", + "CGAME_NOSPECTATORVOICECHAT": "Зрители не могут использовать голосовую связь.", + "CGAME_PRESSYESNO": "Для ответа ДА нажмите '&&1', для ответа НЕТ - '&&2'", + "CGAME_SB_ASSISTS": "Помощь", + "CGAME_SB_DEATHS": "Смерти", + "CGAME_SB_KILLS": "Убийства", + "CGAME_SB_PING": "Пинг", + "CGAME_SERVERHOSTTEAMKILLED": "Вас убил хост вашей же команды", + "CGAME_SPECTATOR": "ЗРИТЕЛЬ", + "CGAME_SPECTATORS": "Зрители", + "CGAME_WAITINGFORSERVERLOAD": "Ожидание загрузки новой карты на сервере", + "CGAME_YOUKILLED": "Вы убили игрока &&1", + "CGAME_YOUWEREKILLED": "Вас убил игрок &&1", + "CLANS_OFFENSIVENAME": "Имя клана отвергнуто: запрещенный текст", + "DEPOT_NEXT_DEPOT_CREDIT": "До след. начисления", + "DLC_MAPS": "Загружаемые карты", + "EXE_DISCONNECTED": "Соединение с сервером разорвано", + "EXE_ERR_BAD_GAME_FOLDER": "Указана неверная папка с игрой.", + "EXE_ERR_CORRECT_FOLDER": "Убедитесь, что игра запущена из правильной папки.", + "EXE_ERR_HIGH_PING_ONLY": "Этот сервер предназначен только для игроков с большим пингом.", + "EXE_ERR_HUNK_ALLOC_FAILED": "Не удалось выделить &&1 Мб.", + "EXE_ERR_LOW_PING_ONLY": "Этот сервер предназначен только для игроков с небольшим пингом.", + "EXE_ERR_WRONG_MAP_VERSION_NUM": "Неверная версия карты '&&1'.", + "EXE_FAVORITES": "Избранное", + "EXE_GAMEISENDING": "Не удалось войти - игра уже заканчивается", + "EXE_HOST_HANDLE_ERROR": "Не удалось обеспечить защищенную связь с сервером.", + "EXE_SERVERFILTER": "Фильтр: &&1", + "EXE_SERVERKILLED": "Сервер остановлен.", + "EXE_SV_INFO_FRIENDLY_FIRE": "Огонь по своим", + "EXE_SV_INFO_GAMETYPE": "Режим игры", + "EXE_SV_INFO_KILLCAM": "Повтор", + "EXE_SV_INFO_NAME": "имя", + "EXE_SV_INFO_PASSWORD": "С паролем", + "EXE_SV_INFO_PING": "пинг", + "EXE_TIMEDOUT": "Время ожидания запроса истекло", + "GAME_DROPPEDFORINACTIVITY": "Отключен от сервера из-за бездействия.", + "GAME_INVALIDGAMETYPE": "Неверный режим игры.", + "GAME_NOSPECTATORCALLVOTE": "Зрители не могут начинать голосование.", + "GAME_NOSPECTATORVOTE": "Зрители не могут голосовать.", + "GAME_SPECTATOR": "Зритель", + "GAME_VOTE_GAMETYPE": "Режим: ", + "LUA_MENU_CHALLENGE_XP": "&&1 XP", + "LUA_MENU_CHANGE_FACTION": "Смена фракции", + "LUA_MENU_CHANGE_TEAM": "Смена команды", + "LUA_MENU_CHANGE_TEAM_CAPS": "ВЫБОР КОМАНДЫ", + "LUA_MENU_CHOOSE_CLASS": "Смена класса", + "LUA_MENU_CHOOSE_CLASS_CAPS": "ВЫБОР КЛАССА", + "LUA_MENU_CONFIRMS": "Жетоны", + "LUA_MENU_CONFIRMS_CAPS": "ЖЕТОНЫ", + "LUA_MENU_CONFIRM_REDEEM_DUPLICATES_CAPS": "ОБМЕНЯТЬ ДУБЛИКАТЫ ПРЕДМЕТОВ (&&1) НА ОПЫТ?", + "LUA_MENU_CONF_CAPS": "УБИЙСТВО ПОДТВЕРЖДЕНО", + "LUA_MENU_CONF_RECIPE_DESC": "Версия на основе режима Убийство подтверждено", + "LUA_MENU_CONF_RECIPE_NAME": "Версия У.П.", + "LUA_MENU_CREATE_A_CLASS": "Создание классов", + "LUA_MENU_CREATE_A_CLASS_CAPS": "ИЗМЕНИТЬ КЛАСС", + "LUA_MENU_DEATHMATCH": "Каждый за себя", + "LUA_MENU_DEATHMATCH_CAPS": "КАЖДЫЙ ЗА СЕБЯ", + "LUA_MENU_DEATHMATCH_RECIPE_DESC": "Версия на основе режима Каждый за себя", + "LUA_MENU_DEATHMATCH_RECIPE_NAME": "Версия Каждый за себя", + "LUA_MENU_DESC_LEADERBOARD_CONF": "Убийство подтверждено - список лидеров", + "LUA_MENU_DESC_LEADERBOARD_DM": "Каждый за себя - список лидеров", + "LUA_MENU_DESC_LEADERBOARD_DOM": "Превосходство - список лидеров", + "LUA_MENU_DESC_LEADERBOARD_SOTF_FFA": "Охота КЗС - список лидеров", + "LUA_MENU_DM_HARDCORE": "Каждый за себя. Хардкор", + "LUA_MENU_DOMINATION": "Превосходство", + "LUA_MENU_DOMINATION_CAPS": "ПРЕВОСХОДСТВО", + "LUA_MENU_DOMINATION_RECIPE_DESC": "Версия на основе режима Превосходство", + "LUA_MENU_DOMINATION_RECIPE_NAME": "Версия Превосходство", + "LUA_MENU_ENVIRONMENT_KILLS": "Убито предметами окружения", + "LUA_MENU_FREE_ONLY": "Свободная камера", + "LUA_MENU_HEALTH_AND_DAMAGE": "Здоровье и урон", + "LUA_MENU_KILLCAM_FINAL_CAPS": "ПОСЛЕДНЕЕ УБИЙСТВО", + "LUA_MENU_KILLS": "Убийства", + "LUA_MENU_KILLS_CAPS": "УБИЙСТВА", + "LUA_MENU_LOSING": "Вы проигрываете", + "LUA_MENU_LOSSES_CAPS": "ПОРАЖЕНИЯ", + "LUA_MENU_MELEEKILLS": "С ножа", + "LUA_MENU_PLAY_TIME": "Всего наиграно", + "LUA_MENU_PRESET_CLASSES": "Готовые классы", + "LUA_MENU_PRESET_CLASSES_CAPS": "ГОТОВЫЕ КЛАССЫ", + "LUA_MENU_RATIO_CAPS": "У/С", + "LUA_MENU_RECIPE_LOAD_CUSTOM": "Загрузить свою версию", + "LUA_MENU_REPORT_DEFEAT": "Поражение", + "LUA_MENU_REPORT_DEFEAT_CAPS": "ПОРАЖЕНИЕ", + "LUA_MENU_REPORT_VICTORY": "Победа", + "LUA_MENU_REPORT_VICTORY_CAPS": "ПОБЕДА", + "LUA_MENU_ROTATION": "Несколько", + "LUA_MENU_RULES_EDIT_DEF_CLASSES": "Список заготовленных классов", + "LUA_MENU_RULES_FAST": "Быстро", + "LUA_MENU_RULES_GUN_CQC": "Холодное", + "LUA_MENU_RULES_GUN_MELEE": "Холодное", + "LUA_MENU_RULES_GUN_MELEE_RPG": "Холодное, РПГ", + "LUA_MENU_RULES_GUN_PROGRESSION_END": "Последнее оружие", + "LUA_MENU_RULES_GUN_RPG_MELEE": "РПГ, холодное", + "LUA_MENU_RULES_HEADSHOTS_ONLY": "Только попадания в голову", + "LUA_MENU_RULES_NORMAL": "По умолчанию", + "LUA_MENU_RULES_RETURN_TIME": "Время автовозврата флага", + "LUA_MENU_RULES_SETBACK_LEVELS": "Потеря уровня от ножа", + "LUA_MENU_RULES_STREAK_GRACE_PERIOD": "Отсрочка серии", + "LUA_MENU_RULES_TEAMKILL_KICK": "Исключение за убийство союзников", + "LUA_MENU_RULES_TEAM_SWITCH": "Смена команды посреди игры", + "LUA_MENU_RULES_TOGGLE_ROTATION_OFF": "Указать одну карту", + "LUA_MENU_RULES_TOGGLE_ROTATION_ON": "Указать несколько карт", + "LUA_MENU_RULES_UNLIMITED": "Без ограничений", + "LUA_MENU_SAS": "S.A.S", + "LUA_MENU_SD_RECIPE_DESC": "Версия на основе режима Найти и Уничтожить", + "LUA_MENU_SD_RECIPE_NAME": "Версия Найти и уничтожить", + "LUA_MENU_SETBACKS": "Откаты", + "LUA_MENU_SPAWN_SETTINGS": "Возрождение", + "LUA_MENU_SR_RECIPE_DESC": "Версия на основе Найти и спасти", + "LUA_MENU_SR_RECIPE_NAME": "Версия Найти и спасти", + "LUA_MENU_SUPPLY_DROP_MTX": "Редкий ящик снабжения", + "LUA_MENU_VERSUS": "VS", + "LUA_MENU_WAR_RECIPE_DESC": "Версия на основе режима Командный бой", + "LUA_MENU_WAR_RECIPE_NAME": "Версия Командный бой", + "LUA_MENU_WEAPON_ATTRIBUTE_HEADER": "СВОЙСТВА", + "LUA_MENU_WEAPON_STAT_MOBILITY": "МОБИЛЬН.", + "LUA_MENU_WEAPPERF_KILLS": "Убийства", + "LUA_MENU_WIN_PERCENTAGE": "% побед", + "LUA_MENU_WL_RATIO": "Победы/поражения", + "LUA_MP_FRONTEND_SCORE_PER_MINUTE_CAPS": "Очков в минуту", + "MENU_ALLOW_ENEMY_SPECTATING": "Наблюдение за противником: ", + "MENU_A_GAME_TYPE_WILL_BE_SELECTED_AT_RANDOM": "Режим игры будет выбран случайно.", + "MENU_CAPTURE_AND_HOLD_THE": "Зарабатывайте очки, захватывая и удерживая указанные позиции. ", + "MENU_CHANGE_GAME_TYPE": "Изменить режим игры", + "MENU_CHANGE_RATE_OF_FIRE": "Изменить темп стрельбы", + "MENU_CHANGE_WEAPON": "Сменить оружие", + "MENU_CONF_DESC": "Зарабатывайте очки, убивая противников и собирая их жетоны.", + "MENU_CREATE_A_CLASS_CAPS": "СОЗДАНИЕ КЛАССОВ", + "MENU_DEATHS": "Смерти", + "MENU_FREE_FOR_ALL": "Каждый за себя", + "MENU_GAME_OPTIONS": "Правила игры", + "MENU_GAME_SETUP_CAPS": "ПАРАМЕТРЫ БОЯ", + "MENU_GAME_TYPE": "Режим игры: ", + "MENU_GAME_TYPE1": "Режим игры", + "MENU_GAME_TYPES": "Режимы игры", + "MENU_GAME_TYPE_SETTINGS": "Настройки режима игры", + "MENU_HP_DESC": "Зарабатывайте очки, захватывая и удерживая опорные пункты. ", + "MENU_JOIN_SERVER_CAP": "ПОДКЛЮЧИТЬСЯ", + "MENU_KILLSTREAK_REWARD_SLOT_1": "Награды за серию убийств - ячейка 1", + "MENU_KILLSTREAK_REWARD_SLOT_2": "Награды за серию убийств - ячейка 2", + "MENU_KILLSTREAK_REWARD_SLOT_3": "Награды за серию убийств - ячейка 3", + "MENU_KILLSTREAK_REWARD_SLOT_4": "Награды за серию убийств - ячейка 4", + "MENU_KILLSTREAK_REWARD_SLOT_5": "Награды за серию убийств - ячейка 5", + "MENU_LEADERBOARD": "Список лидеров", + "MENU_LEAVE_GAME_RANKED2": "игры лишит вас бонуса за матч и", + "MENU_LEAVE_GAME_RANKED3": "будет засчитан как поражение.", + "MENU_LOAD_RECIPE_CAPS": "ЗАГРУЗИТЬ ВЕРСИЮ", + "MENU_LOAD_RECIPE_FROM_DISK": "Загрузить версию с диска", + "MENU_LOSSES": "Проигрыши", + "MENU_MELEE_CAPS": "ХОЛОДНОЕ", + "MENU_MISSES": "Промахи", + "MENU_MODIFIERS": "Модификаторы", + "MENU_ONLINE_STATS": "Сетевая статистика", + "MENU_PRESTIGE_RESET_TITLE2": "Подробнее", + "MENU_PRESTIGE_RESET_WARNING3": "Обратного пути нет...", + "MENU_PRIVATE_MATCH": "Закрытый матч", + "MENU_RATIO": "У/С", + "MENU_RECIPE_CHANGE_BASE_CAPS": "СМЕНИТЬ ТИП ВЕРСИИ", + "MENU_RECIPE_LOAD_CUSTOM": "Загрузить свою версию", + "MENU_RECIPE_SAVE_CUSTOM": "Сохранить свою версию", + "MENU_RECIPE_SETUP_CAPS": "НАСТРОЙКА ВЕРСИИ", + "MENU_REMOVE_FROM_FAVORITES": "Убрать из избранного", + "MENU_REPORT_CHEATING": "Читерство", + "MENU_SCORE_LOSING": "Вы проигрываете &&1 - &&2", + "MENU_SCORE_LOSING_WITH": "Вы проигрываете с &&1 из &&2 очков.", + "MENU_SCORE_WINNING": "Вы ведете &&1 - &&2", + "MENU_SCORE_WINNING_WITH": "Вы ведете с &&1 из &&2 очков.", + "MENU_SELECT_MATCH_TYPE": "Выбрать тип матча", + "MENU_SET_MAP_PREFERENCES": "Укажите, на каких картах вы предпочитаете играть", + "MENU_SHARED": "Урон обоим", + "MENU_SPECTATOR": "Зритель", + "MENU_SPECTATOR_MODE": "Режим зрителя", + "MENU_VIEW_FRIENDLY_FIRE": "Огонь по своим:", + "MENU_WINS": "Победы", + "MENU_WLRATIO": "Победы/Поражения", + "MPUI_ACCURACY_FRIENDS": "Меткость (Друзья)", + "MPUI_ACCURACY_GLOBAL": "Меткость (Все игроки)", + "MPUI_BOTS_REGULAR": "Рядовой", + "MPUI_CHANGE_GAMETYPEMAP": "Сменить режим игры/карту", + "MPUI_CHANGE_GAME_TYPE": "Изменить режим игры", + "MPUI_CHANGE_GAME_TYPEMAP": "Сменить режим/карту", + "MPUI_COMBATRECORD_GAMEMODESTAT_CONFIRMS": "Жетоны", + "MPUI_COMBATRECORD_GAMEMODESTAT_DEFENDS": "Защиты", + "MPUI_CONF": "Убийство подтверждено", + "MPUI_CONF_CAPS": "УБИЙСТВО ПОДТВЕРЖДЕНО", + "MPUI_CONF_RECIPE_DESC": "Версия на основе режима Убийство подтверждено", + "MPUI_DD": "Уничтожение", + "MPUI_DD_CAPS": "УНИЧТОЖЕНИЕ", + "MPUI_DEATHMATCH": "Каждый за себя", + "MPUI_DEATHMATCH_CAPS": "КАЖДЫЙ ЗА СЕБЯ", + "MPUI_DESC_CHANGE_GAMETYPE": "Выбрать другой режим игры.", + "MPUI_DESC_CHANGE_RULES": "Изменить правила матча.", + "MPUI_DESC_GAME_SETUP": "Сменить карту, режим и правила игры.", + "MPUI_DESC_LEADERBOARD_CONF": "Убийство подтверждено - список лидеров", + "MPUI_DESC_LEADERBOARD_DM": "Каждый за себя - список лидеров", + "MPUI_DESC_LEADERBOARD_DOM": "Превосходство - список лидеров", + "MPUI_DOMINATION": "Превосходство", + "MPUI_DOMINATION_CAPS": "ПРЕВОСХОДСТВО", + "MPUI_ENABLED_NO_BLEED": "Без ограничений", + "MPUI_FRIENDY_FIRE_PRE": "Огонь по своим:", + "MPUI_HARDCORE_PRE": "Хардкор:", + "MPUI_HARDPOINT": "Каждый за себя", + "MPUI_HITS": "Попадания", + "MPUI_LOSING_CAPS": "ТЕРЯЕМ", + "MPUI_LOSSES": "Поражения", + "MPUI_MISSES": "Промахи", + "MPUI_N_XP": "&&1 XP", + "MPUI_RANK": "Уровень", + "MPUI_RATIO": "У/С", + "MPUI_RECOMMENDEDPLAYERS": "Рекоменд. лимит игроков: &&1", + "MPUI_ROUND_SWITCH_PRE": "Смена сторон:", + "MPUI_RULES_FRIENDLY_FIRE": "Огонь по своим:", + "MPUI_RULES_HARDCORE": "Хардкор:", + "MPUI_RULES_INSTANT": "Сразу", + "MPUI_RULES_OLDSCHOOL": "Режим \"Старая школа\":", + "MPUI_RULES_ROUND_SWITCH": "Смена сторон:", + "MPUI_RULES_SHARED": "Урон обоим", + "MPUI_RULES_SPECTATING": "Вид от:", + "MPUI_RULES_TOGGLE_ROTATION_OFF": "Отключить ротацию карт", + "MPUI_RULES_TOGGLE_ROTATION_ON": "Включить ротацию карт", + "MPUI_RUSSIAN": "Русский", + "MPUI_SPECTATE": "Смотреть", + "MPUI_SPECTATING_PRE": "Вид от:", + "MPUI_SPECTATOR": "Зритель", + "MPUI_WAR_HARDCORE": "Командный бой. Хардкор", + "MPUI_WAR_RECIPE_DESC": "Версия на основе режима Командный бой", + "MPUI_WINLOSSRATIO": "Победы/поражения", + "MPUI_WINS": "Победы", + "MPUI_YES": "Да", + "MP_DEATHMATCH": "Каждый за себя", + "MP_DEATHMATCH_TIMER": "Каждый за себя - &&1", + "MP_HALFTIME": "Конец раунда", + "MP_IED_PRESS_LEFT_TRIGGER_TO_DETONATE": "Нажмите левый триггер, чтобы активировать детонатор", + "MP_INVALIDGAMETYPE": "Неверный режим игры.", + "MP_JOINED_ONE": "%s присоединяется к первому отряду", + "MP_KILLCAM": "ПОВТОР", + "MP_MATCH_BEGINS_IN_VAL": "Бой начнется через &&1 с", + "MP_MELEE": "Холодное", + "MP_NOSPECTATORCALLVOTE": "Зрители не могут начинать голосование.", + "MP_NOSPECTATORVOTE": "Зрители не могут голосовать.", + "MP_PLAY_TYPE": "Выберите режим", + "MP_SWITCHING_SIDES": "СМЕНА СТОРОН", + "MP_TEAMKILL": "Убийство своего", + "MP_THE_SERVER_DOES_NOT_HAVE": "На сервере нет этой карты.", + "MP_VOTE_GAMETYPE": "Режим игры: &&1", + "MP_VOTE_TEMPBAN": "Временно забанить: &&1", + "MP_ZOOM": "^3[{+ads}]^7 Смена кратности", + "PLATFORM_EMBLEM_LAYER_DOWN_KBM": "Опустить слой", + "PLATFORM_EMBLEM_LAYER_UP_KBM": "Поднять слой", + "PLATFORM_UI_HEADER_PLAY_MP_CAPS": "СЕТЕВАЯ ИГРА", + "PRESENCE_CONF_TDM": "Убийство подтверждено", + "PRESENCE_C_DOM": "Превосходство. Классика", + "PRESENCE_C_DOM_SYSTEM_DIALOG": "Превосходство, классика", + "PRESENCE_FFA": "Каждый за себя", + "PRESENCE_FFA_SYSTEM_DIALOG": "Каждый за себя", + "PRESENCE_HC_DOM": "Превосходство. Хардкор", + "PRESENCE_HC_FFA": "Каждый за себя. Хардкор", + "PRESENCE_HC_FFA_SYSTEM_DIALOG": "Каждый за себя, хардкор", + "SPLASHES_CAPTURE": "Плеймейкер!", + "SPLASHES_LAST_MAN_DEFUSE": "Крепкая хватка", + "SPLASHES_TAG_COLLECTOR": "Инкассатор", + "XBOXLIVE_DESTROYPARTY": "Распустить команду?" +} \ No newline at end of file diff --git a/deps/GSL b/deps/GSL index 330583f4..6c6111ac 160000 --- a/deps/GSL +++ b/deps/GSL @@ -1 +1 @@ -Subproject commit 330583f47800c60cf001239550d291d16274756a +Subproject commit 6c6111acb7b5d687ac006969ac96e5b1f21374cd diff --git a/deps/asmjit b/deps/asmjit index 5c469e3f..5b5b0b38 160000 --- a/deps/asmjit +++ b/deps/asmjit @@ -1 +1 @@ -Subproject commit 5c469e3f7c307da939d38d72e09f08db7ca076ef +Subproject commit 5b5b0b38775938df4d3779604ff1db60b9a9dcbf diff --git a/deps/curl b/deps/curl index 8beff435..af5999a6 160000 --- a/deps/curl +++ b/deps/curl @@ -1 +1 @@ -Subproject commit 8beff4355956e3d18ceb3afc21c1f3edec82543c +Subproject commit af5999a6742ea90011e7fa08aade7eac9943b76a diff --git a/deps/extra/gsc-tool/interface.cpp b/deps/extra/gsc-tool/interface.cpp new file mode 100644 index 00000000..28b7ad73 --- /dev/null +++ b/deps/extra/gsc-tool/interface.cpp @@ -0,0 +1,30 @@ +#include "stdafx.hpp" + +#include + +#include "interface.hpp" + +namespace gsc +{ + std::unique_ptr compiler() + { + auto compiler = std::make_unique(); + compiler->mode(xsk::gsc::build::prod); + return compiler; + } + + std::unique_ptr decompiler() + { + return std::make_unique(); + } + + std::unique_ptr assembler() + { + return std::make_unique(); + } + + std::unique_ptr disassembler() + { + return std::make_unique(); + } +} diff --git a/deps/extra/gsc-tool/interface.hpp b/deps/extra/gsc-tool/interface.hpp new file mode 100644 index 00000000..133e6ae2 --- /dev/null +++ b/deps/extra/gsc-tool/interface.hpp @@ -0,0 +1,9 @@ +#pragma once + +namespace gsc +{ + std::unique_ptr compiler(); + std::unique_ptr decompiler(); + std::unique_ptr assembler(); + std::unique_ptr disassembler(); +} diff --git a/deps/gsc-tool b/deps/gsc-tool new file mode 160000 index 00000000..7d374025 --- /dev/null +++ b/deps/gsc-tool @@ -0,0 +1 @@ +Subproject commit 7d374025b7675bada64c247ebe9378dd335a33da diff --git a/deps/json b/deps/json new file mode 160000 index 00000000..4c6cde72 --- /dev/null +++ b/deps/json @@ -0,0 +1 @@ +Subproject commit 4c6cde72e533158e044252718c013a48bcff346c diff --git a/deps/libtomcrypt b/deps/libtomcrypt index 8fd5dad9..29986d04 160000 --- a/deps/libtomcrypt +++ b/deps/libtomcrypt @@ -1 +1 @@ -Subproject commit 8fd5dad96b56beb53b5cf199cb63fb76dfba32bb +Subproject commit 29986d04f2dca985ee64fbca1c7431ea3e3422f4 diff --git a/deps/libtommath b/deps/libtommath index 4b473685..03de03de 160000 --- a/deps/libtommath +++ b/deps/libtommath @@ -1 +1 @@ -Subproject commit 4b47368501321c795d5b54d87a5bab35a21a7940 +Subproject commit 03de03dee753442d4b23166982514639c4ccbc39 diff --git a/deps/lua b/deps/lua index d61b0c60..be908a7d 160000 --- a/deps/lua +++ b/deps/lua @@ -1 +1 @@ -Subproject commit d61b0c60287c38008d312ddd11724a15b1737f7b +Subproject commit be908a7d4d8130264ad67c5789169769f824c5d1 diff --git a/deps/minhook b/deps/minhook index 4a455528..49d03ad1 160000 --- a/deps/minhook +++ b/deps/minhook @@ -1 +1 @@ -Subproject commit 4a455528f61b5a375b1f9d44e7d296d47f18bb18 +Subproject commit 49d03ad118cf7f6768c79a8f187e14b8f2a07f94 diff --git a/deps/premake/gsc-tool.lua b/deps/premake/gsc-tool.lua new file mode 100644 index 00000000..08da07de --- /dev/null +++ b/deps/premake/gsc-tool.lua @@ -0,0 +1,68 @@ +gsc_tool = { + source = path.join(dependencies.basePath, "gsc-tool/src") +} + +function gsc_tool.import() + links {"xsk-gsc-h1", "xsk-gsc-utils"} + gsc_tool.includes() +end + +function gsc_tool.includes() + includedirs { + path.join(gsc_tool.source, "utils"), + path.join(gsc_tool.source, "h1"), + path.join(dependencies.basePath, "extra/gsc-tool") -- https://github.com/GEEKiDoS/open-teknomw3/blob/master/deps/extra/gsc-tool + } +end + +-- https://github.com/xensik/gsc-tool/blob/dev/premake5.lua#L95 +function gsc_tool.project() + project "xsk-gsc-utils" + kind "StaticLib" + language "C++" + + pchheader "stdafx.hpp" + pchsource(path.join(gsc_tool.source, "utils/stdafx.cpp")) + + files { + path.join(gsc_tool.source, "utils/**.h"), + path.join(gsc_tool.source, "utils/**.hpp"), + path.join(gsc_tool.source, "utils/**.cpp") + } + + includedirs { + path.join(gsc_tool.source, "utils"), + gsc_tool.source + } + + zlib.includes() + + project "xsk-gsc-h1" + kind "StaticLib" + language "C++" + + pchheader "stdafx.hpp" + pchsource(path.join(gsc_tool.source, "h1/stdafx.cpp")) + + files { + path.join(gsc_tool.source, "h1/**.h"), + path.join(gsc_tool.source, "h1/**.hpp"), + path.join(gsc_tool.source, "h1/**.cpp"), + path.join(dependencies.basePath, "extra/gsc-tool/interface.cpp") + } + + includedirs { + path.join(gsc_tool.source, "h1"), + gsc_tool.source, + path.join(dependencies.basePath, "extra/gsc-tool") + } + + -- https://github.com/xensik/gsc-tool/blob/dev/premake5.lua#L25 + -- adding these build options fixes a bunch of parser stuff + filter "action:vs*" + buildoptions "/bigobj" + buildoptions "/Zc:__cplusplus" + filter {} +end + +table.insert(dependencies, gsc_tool) diff --git a/deps/premake/json.lua b/deps/premake/json.lua new file mode 100644 index 00000000..c060e3a0 --- /dev/null +++ b/deps/premake/json.lua @@ -0,0 +1,17 @@ +json = { + source = path.join(dependencies.basePath, "json") +} + +function json.import() + json.includes() +end + +function json.includes() + includedirs {path.join(json.source, "single_include/*")} +end + +function json.project() + +end + +table.insert(dependencies, json) diff --git a/deps/protobuf b/deps/protobuf index fb6f8da0..7ce9c415 160000 --- a/deps/protobuf +++ b/deps/protobuf @@ -1 +1 @@ -Subproject commit fb6f8da08b60b6beb5bb360d79dd3feda0147da7 +Subproject commit 7ce9c415455c098409222702b3b4572b47232882 diff --git a/deps/rapidjson b/deps/rapidjson index 27c3a8dc..a98e9999 160000 --- a/deps/rapidjson +++ b/deps/rapidjson @@ -1 +1 @@ -Subproject commit 27c3a8dc0e2c9218fe94986d249a12b5ed838f1d +Subproject commit a98e99992bd633a2736cc41f96ec85ef0c50e44d diff --git a/deps/sol2 b/deps/sol2 index 4de99c5b..f81643aa 160000 --- a/deps/sol2 +++ b/deps/sol2 @@ -1 +1 @@ -Subproject commit 4de99c5b41b64b7e654bf8e48b177e8414a756b7 +Subproject commit f81643aa0c0c507c0cd8400b8cfedc74a34a19f6 diff --git a/deps/stb b/deps/stb index af1a5bc3..8b5f1f37 160000 --- a/deps/stb +++ b/deps/stb @@ -1 +1 @@ -Subproject commit af1a5bc352164740c1cc1354942b1c6b72eacb8a +Subproject commit 8b5f1f37b5b75829fc72d38e7b5d4bcbf8a26d55 diff --git a/deps/zlib b/deps/zlib index eff308af..e5546956 160000 --- a/deps/zlib +++ b/deps/zlib @@ -1 +1 @@ -Subproject commit eff308af425b67093bab25f80f1ae950166bece1 +Subproject commit e554695638228b846d49657f31eeff0ca4680e8a diff --git a/src/client/component/arena.cpp b/src/client/component/arena.cpp new file mode 100644 index 00000000..0941ca79 --- /dev/null +++ b/src/client/component/arena.cpp @@ -0,0 +1,88 @@ +#include +#include "loader/component_loader.hpp" + +#include "game/game.hpp" + +#include "filesystem.hpp" +#include "console.hpp" + +#include +#include +#include + +#define MAX_ARENAS 64 + +namespace arena +{ + namespace + { + std::recursive_mutex arena_mutex; + + bool parse_arena(const std::string& path) + { + std::lock_guard _0(arena_mutex); + + std::string buffer{}; + if (filesystem::read_file(path, &buffer) && !buffer.empty()) + { + *game::ui_num_arenas += game::GameInfo_ParseArenas(buffer.data(), MAX_ARENAS - *game::ui_num_arenas, + &game::ui_arena_infos[*game::ui_num_arenas]); + return true; + } + + if (!game::DB_XAssetExists(game::ASSET_TYPE_RAWFILE, path.data()) || + game::DB_IsXAssetDefault(game::ASSET_TYPE_RAWFILE, path.data())) + { + return false; + } + + const auto rawfile = game::DB_FindXAssetHeader(game::ASSET_TYPE_RAWFILE, path.data(), 0).rawfile; + const auto len = game::DB_GetRawFileLen(rawfile); + + const auto rawfile_buffer = utils::memory::get_allocator()->allocate_array(len); + const auto _1 = gsl::finally([&] + { + utils::memory::get_allocator()->free(rawfile_buffer); + }); + + game::DB_GetRawBuffer(rawfile, rawfile_buffer, len); + *game::ui_num_arenas += game::GameInfo_ParseArenas(rawfile_buffer, MAX_ARENAS - *game::ui_num_arenas, + &game::ui_arena_infos[*game::ui_num_arenas]); + return true; + } + + void load_arenas_stub() + { + *game::ui_num_arenas = 0; + *game::ui_arena_buf_pos = 0; + + parse_arena("mp/basemaps.arena"); + + // read usermap arena from disk + const auto mapname = game::Dvar_FindVar("ui_mapname"); + if (mapname && mapname->current.string) + { + const auto usermap_path = "usermaps/"s + mapname->current.string; + const auto arena_path = usermap_path + "/" + mapname->current.string + ".arena"; + parse_arena(arena_path); + } + } + } + + class component final : public component_interface + { + public: + void post_unpack() override + { + if (!game::environment::is_mp()) + { + return; + } + + // load custom arenas + utils::hook::jump(0x4DE030_b, load_arenas_stub); + } + }; +} + +REGISTER_COMPONENT(arena::component) diff --git a/src/client/component/arxan.cpp b/src/client/component/arxan.cpp index 3f77095f..6bb915e7 100644 --- a/src/client/component/arxan.cpp +++ b/src/client/component/arxan.cpp @@ -1,6 +1,9 @@ #include #include "loader/component_loader.hpp" + +#include "arxan.hpp" #include "scheduler.hpp" + #include "game/game.hpp" #include @@ -137,9 +140,12 @@ namespace arxan void post_unpack() override { // cba to implement sp, not sure if it's even needed - if (game::environment::is_sp()) return; + if (game::environment::is_sp()) + { + return; + } } }; } -REGISTER_COMPONENT(arxan::component) \ No newline at end of file +REGISTER_COMPONENT(arxan::component) diff --git a/src/client/component/arxan.hpp b/src/client/component/arxan.hpp new file mode 100644 index 00000000..e69de29b diff --git a/src/client/component/auth.cpp b/src/client/component/auth.cpp index 268d23ab..ba38b3bd 100644 --- a/src/client/component/auth.cpp +++ b/src/client/component/auth.cpp @@ -2,18 +2,19 @@ #include "loader/component_loader.hpp" #include "auth.hpp" -#include "component/command.hpp" +#include "command.hpp" +#include "console.hpp" #include "network.hpp" +#include "game/game.hpp" +#include "steam/steam.hpp" + #include #include #include #include #include -#include "game/game.hpp" -#include "steam/steam.hpp" - namespace auth { namespace @@ -162,7 +163,6 @@ namespace auth if (xuid != key.get_hash()) { - //MessageBoxA(nullptr, steam_id.data(), std::to_string(key.get_hash()).data(), 0); network::send(*from, "error", utils::string::va("XUID doesn't match the certificate: %llX != %llX", xuid, key.get_hash()), '\n'); return; @@ -250,9 +250,9 @@ namespace auth utils::hook::set(0x12D93C_b, 0xC3); } - command::add("guid", []() + command::add("guid", [] { - printf("Your guid: %llX\n", steam::SteamUser()->GetSteamID().bits); + console::info("Your guid: %llX\n", steam::SteamUser()->GetSteamID().bits); }); } }; diff --git a/src/client/component/binding.cpp b/src/client/component/binding.cpp index 5ef5bf66..821ec3f7 100644 --- a/src/client/component/binding.cpp +++ b/src/client/component/binding.cpp @@ -1,5 +1,6 @@ #include #include "loader/component_loader.hpp" + #include "game/game.hpp" #include diff --git a/src/client/component/bots.cpp b/src/client/component/bots.cpp index 3bb8382d..3cba3a48 100644 --- a/src/client/component/bots.cpp +++ b/src/client/component/bots.cpp @@ -29,10 +29,10 @@ namespace bots void bot_team_join(const int entity_num) { const game::scr_entref_t entref{static_cast(entity_num), 0}; - scheduler::once([entref]() + scheduler::once([entref] { scripting::notify(entref, "luinotifyserver", {"team_select", 2}); - scheduler::once([entref]() + scheduler::once([entref] { auto* _class = utils::string::va("class%d", utils::cryptography::random::get_integer() % 5); scripting::notify(entref, "luinotifyserver", {"class_select", _class}); @@ -65,7 +65,7 @@ namespace bots } else { - scheduler::once([]() + scheduler::once([] { add_bot(); }, scheduler::pipeline::server, 100ms); @@ -151,10 +151,13 @@ namespace bots }); // Clear bot names and reset ID on game shutdown to allow new names to be added without restarting - scripting::on_shutdown([] + scripting::on_shutdown([](bool /*free_scripts*/, bool post_shutdown) { - bot_names.clear(); - bot_id = 0; + if (!post_shutdown) + { + bot_names.clear(); + bot_id = 0; + } }); } }; diff --git a/src/client/component/branding.cpp b/src/client/component/branding.cpp index cbbcb0eb..342b8b0f 100644 --- a/src/client/component/branding.cpp +++ b/src/client/component/branding.cpp @@ -1,13 +1,13 @@ #include #include "loader/component_loader.hpp" +#include "command.hpp" +#include "dvars.hpp" #include "localized_strings.hpp" #include "scheduler.hpp" -#include "command.hpp" #include "version.hpp" #include "game/game.hpp" -#include "dvars.hpp" #include #include @@ -20,7 +20,7 @@ namespace branding { utils::hook::detour ui_get_formatted_build_number_hook; - float color[4] = {0.666f, 0.666f, 0.666f, 0.666f}; + float color[4] = {0.39f, 0.9f, 0.4f, 0.9f}; const char* ui_get_formatted_build_number_stub() { @@ -30,15 +30,19 @@ namespace branding void draw_branding() { - const auto font = game::R_RegisterFont("fonts/fira_mono_bold.ttf", 20); + const auto font = game::R_RegisterFont("fonts/fira_mono_bold.ttf", 22); if (font) { #ifdef DEBUG - game::R_AddCmdDrawText("H1-Mod: " VERSION " (" __DATE__ " " __TIME__ ")", 0x7FFFFFFF, font, 10.f, - 5.f + static_cast(font->pixelHeight), 1.f, 1.f, 0.0f, color, 0); + game::R_AddCmdDrawText("h1-mod: " VERSION " (" __DATE__ " " __TIME__ ")", + 0x7FFFFFFF, font, 10.f, + 5.f + static_cast(font->pixelHeight), + 1.f, 1.f, 0.0f, color, 0); #else - game::R_AddCmdDrawText("H1-Mod: " VERSION, 0x7FFFFFFF, font, 10.f, - 5.f + static_cast(font->pixelHeight), 1.f, 1.f, 0.0f, color, 0); + game::R_AddCmdDrawText("h1-mod", + 0x7FFFFFFF, font, 10.f, + 5.f + static_cast(font->pixelHeight), + 1.f, 1.f, 0.0f, color, 0); #endif } } @@ -59,12 +63,6 @@ namespace branding return; } - if (game::environment::is_mp()) - { - localized_strings::override("LUA_MENU_MULTIPLAYER_CAPS", "H1-MOD: MULTIPLAYER\n"); - localized_strings::override("MENU_MULTIPLAYER_CAPS", "H1-MOD: MULTIPLAYER"); - } - ui_get_formatted_build_number_hook.create( SELECT_VALUE(0x406EC0_b, 0x1DF300_b), ui_get_formatted_build_number_stub); } diff --git a/src/client/component/bullet.cpp b/src/client/component/bullet.cpp index 72f6f4ff..58431474 100644 --- a/src/client/component/bullet.cpp +++ b/src/client/component/bullet.cpp @@ -34,9 +34,8 @@ namespace bullet return; } - bg_surface_penetration = dvars::register_float("bg_surfacePenetration", 0.0f, - 0.0f, std::numeric_limits::max(), 0, - "Set to a value greater than 0 to override the surface penetration depth"); + bg_surface_penetration = dvars::register_float("bg_surfacePenetration", 0.0f, 0.0f, std::numeric_limits::max(), 0, + "Set to a value greater than 0 to override the bullet surface penetration depth"); bg_get_surface_penetration_depth_hook.create(0x2E1110_b, &bg_get_surface_penetration_depth_stub); } diff --git a/src/client/component/chat.cpp b/src/client/component/chat.cpp index e78550b7..c798b455 100644 --- a/src/client/component/chat.cpp +++ b/src/client/component/chat.cpp @@ -34,11 +34,6 @@ namespace chat utils::hook::inject(0x18A980_b, reinterpret_cast(0x2E6F588_b)); utils::hook::call(0x33EDEC_b, ui_get_font_handle_stub); - // set text style to 0 (non-blurry) - utils::hook::set(0x18A9F2_b, 0); - utils::hook::set(0x0F7151_b, 0); - utils::hook::set(0x33EE0E_b, 0); - localized_strings::override("EXE_SAY", "^3Match^7"); localized_strings::override("EXE_SAYTEAM", "^5Team^7"); diff --git a/src/client/component/command.cpp b/src/client/component/command.cpp index c26eea7e..58c0918d 100644 --- a/src/client/component/command.cpp +++ b/src/client/component/command.cpp @@ -1,17 +1,19 @@ #include #include "loader/component_loader.hpp" +#include "command.hpp" +#include "console.hpp" +#include "dvars.hpp" +#include "game_console.hpp" +#include "fastfiles.hpp" +#include "filesystem.hpp" +#include "scheduler.hpp" +#include "logfile.hpp" + #include "game/game.hpp" #include "game/dvars.hpp" #include "game/scripting/execution.hpp" -#include "command.hpp" -#include "console.hpp" -#include "game_console.hpp" -#include "fastfiles.hpp" -#include "scheduler.hpp" -#include "logfile.hpp" - #include #include #include @@ -22,11 +24,12 @@ namespace command namespace { utils::hook::detour client_command_hook; - utils::hook::detour parse_commandline_hook; std::unordered_map> handlers; std::unordered_map> handlers_sv; + std::optional saved_fs_game; + void main_handler() { params params = {}; @@ -105,10 +108,44 @@ namespace command parsed = true; } - void parse_commandline_stub() + void parse_startup_variables() { + auto& com_num_console_lines = *reinterpret_cast(0x35634B8_b); + auto* com_console_lines = reinterpret_cast(0x35634C0_b); + + for (int i = 0; i < com_num_console_lines; i++) + { + game::Cmd_TokenizeString(com_console_lines[i]); + + // only +set dvar value + if (game::Cmd_Argc() >= 3 && game::Cmd_Argv(0) == "set"s) + { + const std::string& dvar_name = game::Cmd_Argv(1); + const std::string& value = game::Cmd_Argv(2); + + const auto* dvar = game::Dvar_FindVar(dvar_name.data()); + if (dvar) + { + game::Dvar_SetCommand(dvar->hash, "", value.data()); + } + else + { + dvars::callback::on_register(dvar_name, [dvar_name, value]() + { + game::Dvar_SetCommand(game::generateHashValue(dvar_name.data()), "", value.data()); + }); + } + } + + game::Cmd_EndTokenizeString(); + } + } + + void parse_commandline_stub(char* commandline) + { + //utils::hook::invoke(0x17CB60_b, commandline); // Com_ParseCommandLine parse_command_line(); - parse_commandline_hook.invoke(); + parse_startup_variables(); } game::dvar_t* dvar_command_stub() @@ -520,18 +557,50 @@ namespace command } } + void register_fs_game_path() + { + const auto* fs_game = game::Dvar_FindVar("fs_game"); + const auto new_mod_path = fs_game->current.string; + + // check if the last saved fs_game value isn't empty and if it doesn't equal the new fs_game + if (saved_fs_game.has_value() && saved_fs_game != new_mod_path) + { + // unregister path to be used as a fs directory + filesystem::unregister_path(saved_fs_game.value()); + } + + if (new_mod_path && !new_mod_path[0]) + { + return; + } + + // register fs_game value as a fs directory used for many things + filesystem::register_path(new_mod_path); + saved_fs_game = new_mod_path; + } + class component final : public component_interface { public: void post_unpack() override { + // it might be overdone to change the filesystem path on every new value change, but to be fair, + // for the mods that don't need full restarts, this is good because it'll adjust and work like so + // in my opinion, this is fine. if a user tries to modify the dvar themselves, they'll have problems + // but i seriously doubt it'll be bad. + dvars::callback::on_new_value("fs_game", []() + { + console::warn("fs_game value changed, filesystem paths will be adjusted to new dvar value."); + register_fs_game_path(); + }); + if (game::environment::is_sp()) { add_commands_sp(); } else { - parse_commandline_hook.create(0x157D50_b, parse_commandline_stub); + utils::hook::call(0x15C44B_b, parse_commandline_stub); add_commands_mp(); } diff --git a/src/client/component/command.hpp b/src/client/component/command.hpp index 06228c9d..a891b802 100644 --- a/src/client/component/command.hpp +++ b/src/client/component/command.hpp @@ -49,4 +49,6 @@ namespace command void add_sv(const char* name, std::function callback); void execute(std::string command, bool sync = false); + + void register_fs_game_path(); } \ No newline at end of file diff --git a/src/client/component/console.cpp b/src/client/component/console.cpp index 7daaa3f7..f6c769b8 100644 --- a/src/client/component/console.cpp +++ b/src/client/component/console.cpp @@ -1,10 +1,10 @@ #include -#include "console.hpp" #include "loader/component_loader.hpp" #include "game/game.hpp" #include "command.hpp" +#include "console.hpp" #include "rcon.hpp" #include "version.hpp" @@ -62,7 +62,7 @@ namespace console { static thread_local char buffer[0x1000]; - const auto count = _vsnprintf_s(buffer, sizeof(buffer), sizeof(buffer), message, *ap); + const auto count = vsnprintf_s(buffer, _TRUNCATE, message, *ap); if (count < 0) { return {}; diff --git a/src/client/component/dedicated.cpp b/src/client/component/dedicated.cpp index 2031788a..dae3124c 100644 --- a/src/client/component/dedicated.cpp +++ b/src/client/component/dedicated.cpp @@ -19,6 +19,8 @@ namespace dedicated utils::hook::detour gscr_set_dynamic_dvar_hook; utils::hook::detour com_quit_f_hook; + const game::dvar_t* sv_lanOnly; + void init_dedicated_server() { static bool initialized = false; @@ -31,8 +33,7 @@ namespace dedicated void send_heartbeat() { - auto* const dvar = game::Dvar_FindVar("sv_lanOnly"); - if (dvar && dvar->current.enabled) + if (sv_lanOnly->current.enabled) { return; } @@ -80,12 +81,11 @@ namespace dedicated return console_command_queue; } - void execute_console_command(const int client, const char* command) + void execute_console_command([[maybe_unused]] const int local_client_num, const char* command) { if (game::Live_SyncOnlineDataFlags(0) == 0) { - game::Cbuf_AddText(client, 0, command); - game::Cbuf_AddText(client, 0, "\n"); + command::execute(command); } else { @@ -100,8 +100,7 @@ namespace dedicated for (const auto& command : queue) { - game::Cbuf_AddText(0, 0, command.data()); - game::Cbuf_AddText(0, 0, "\n"); + command::execute(command); } } @@ -110,21 +109,6 @@ namespace dedicated std::this_thread::sleep_for(1ms); } - game::dvar_t* gscr_set_dynamic_dvar() - { - /* - auto s = game::Scr_GetString(0); - auto* dvar = game::Dvar_FindVar(s); - - if (dvar && !strncmp("scr_", dvar->name, 4)) - { - return dvar; - } - */ - - return gscr_set_dynamic_dvar_hook.invoke(); - } - void kill_server() { const auto* svs_clients = *game::mp::svs_clients; @@ -145,16 +129,16 @@ namespace dedicated void sys_error_stub(const char* msg, ...) { - char buffer[2048]; + char buffer[2048]{}; va_list ap; va_start(ap, msg); - vsnprintf_s(buffer, sizeof(buffer), _TRUNCATE, msg, ap); + vsnprintf_s(buffer, _TRUNCATE, msg, ap); va_end(ap); - scheduler::once([]() + scheduler::once([] { command::execute("map_rotate"); }, scheduler::main, 3s); @@ -210,7 +194,7 @@ namespace dedicated dvars::register_bool("dedicated", true, game::DVAR_FLAG_READ, "Dedicated server"); // Add lanonly mode - dvars::register_bool("sv_lanOnly", false, game::DVAR_FLAG_NONE, "Don't send heartbeat"); + sv_lanOnly = dvars::register_bool("sv_lanOnly", false, game::DVAR_FLAG_NONE, "Don't send heartbeat"); // Disable VirtualLobby dvars::override::register_bool("virtualLobbyEnabled", false, game::DVAR_FLAG_READ); @@ -237,15 +221,12 @@ namespace dedicated a.popad64(); a.jmp(0x157DDF_b); - }), true);// + }), true); // delay console commands until the initialization is done // COULDN'T FOUND // utils::hook::call(0x1400D808C, execute_console_command); // utils::hook::nop(0x1400D80A4, 5); - // patch GScr_SetDynamicDvar to behave better - gscr_set_dynamic_dvar_hook.create(0x43CF60_b, &gscr_set_dynamic_dvar); - utils::hook::nop(0x189514_b, 248); // don't load config file utils::hook::nop(0x156C46_b, 5); // ^ utils::hook::set(0x17F470_b, 0xC3); // don't save config file @@ -332,7 +313,7 @@ namespace dedicated { if (game::Live_SyncOnlineDataFlags(0) == 32 && game::Sys_IsDatabaseReady2()) { - scheduler::once([]() + scheduler::once([] { command::execute("xstartprivateparty", true); command::execute("disconnect", true); // 32 -> 0 @@ -343,7 +324,7 @@ namespace dedicated return scheduler::cond_continue; }, scheduler::pipeline::main, 1s); - scheduler::on_game_initialized([]() + scheduler::on_game_initialized([] { initialize(); diff --git a/src/client/component/dedicated_info.cpp b/src/client/component/dedicated_info.cpp index fa5c9a34..8164345b 100644 --- a/src/client/component/dedicated_info.cpp +++ b/src/client/component/dedicated_info.cpp @@ -1,8 +1,11 @@ #include #include "loader/component_loader.hpp" -#include "game/game.hpp" + #include "scheduler.hpp" -#include + +#include "game/game.hpp" + +#include namespace dedicated_info { @@ -16,7 +19,7 @@ namespace dedicated_info return; } - scheduler::loop([]() + scheduler::loop([] { auto* sv_running = game::Dvar_FindVar("sv_running"); if (!sv_running || !sv_running->current.enabled || (*game::mp::svs_clients) == nullptr) @@ -25,9 +28,9 @@ namespace dedicated_info return; } - auto* const sv_hostname = game::Dvar_FindVar("sv_hostname"); - auto* const sv_maxclients = game::Dvar_FindVar("sv_maxclients"); - auto* const mapname = game::Dvar_FindVar("mapname"); + const auto sv_hostname = game::Dvar_FindVar("sv_hostname"); + const auto sv_maxclients = game::Dvar_FindVar("sv_maxclients"); + const auto mapname = game::Dvar_FindVar("mapname"); auto bot_count = 0; auto client_count = 0; diff --git a/src/client/component/demonware.cpp b/src/client/component/demonware.cpp index 1a3ea084..c1451299 100644 --- a/src/client/component/demonware.cpp +++ b/src/client/component/demonware.cpp @@ -1,16 +1,16 @@ #include #include "loader/component_loader.hpp" -#include -#include - #include "game/game.hpp" #include "game/demonware/servers/lobby_server.hpp" #include "game/demonware/servers/auth3_server.hpp" #include "game/demonware/servers/stun_server.hpp" #include "game/demonware/servers/umbrella_server.hpp" #include "game/demonware/server_registry.hpp" -#include +#include "game/dvars.hpp" + +#include +#include #define TCP_BLOCKING true #define UDP_BLOCKING false @@ -119,7 +119,7 @@ namespace demonware int getaddrinfo_stub(const char* name, const char* service, const addrinfo* hints, addrinfo** res) { -#ifdef DEBUG +#ifdef DW_DEBUG printf("[ network ]: [getaddrinfo]: \"%s\" \"%s\"\n", name, service); #endif @@ -202,7 +202,7 @@ namespace demonware hostent* gethostbyname_stub(const char* name) { -#ifdef DEBUG +#ifdef DW_DEBUG printf("[ network ]: [gethostbyname]: \"%s\"\n", name); #endif @@ -430,7 +430,7 @@ namespace demonware //printf("logged\n"); } -#ifdef DEBUG +#ifdef DW_DEBUG void a(unsigned int n) { printf("bdAuth: Auth task failed with HTTP code [%u]\n", n); diff --git a/src/client/component/discord.cpp b/src/client/component/discord.cpp index 2495b7bd..7677662f 100644 --- a/src/client/component/discord.cpp +++ b/src/client/component/discord.cpp @@ -1,15 +1,15 @@ #include #include "loader/component_loader.hpp" -#include "scheduler.hpp" -#include "game/game.hpp" #include "console.hpp" #include "command.hpp" +#include "discord.hpp" +#include "materials.hpp" #include "network.hpp" #include "party.hpp" -#include "materials.hpp" -#include "discord.hpp" +#include "scheduler.hpp" +#include "game/game.hpp" #include "game/ui_scripting/execution.hpp" #include @@ -57,21 +57,28 @@ namespace discord { static char details[0x80] = {0}; const auto map = game::Dvar_FindVar("mapname")->current.string; - const auto mapname = game::UI_SafeTranslateString( - utils::string::va("PRESENCE_%s%s", SELECT_VALUE("SP_", ""), map)); + const auto key = utils::string::va("PRESENCE_%s%s", SELECT_VALUE("SP_", ""), map); + const char* mapname = map; + + if (game::DB_XAssetExists(game::ASSET_TYPE_LOCALIZE, key) && !game::DB_IsXAssetDefault(game::ASSET_TYPE_LOCALIZE, key)) + { + mapname = game::UI_SafeTranslateString(key); + } if (game::environment::is_mp()) { + static char clean_gametype[0x80] = {0}; const auto gametype = game::UI_GetGameTypeDisplayName( game::Dvar_FindVar("g_gametype")->current.string); - strcpy_s(details, 0x80, utils::string::va("%s on %s", gametype, mapname)); + utils::string::strip(gametype, + clean_gametype, sizeof(clean_gametype)); + strcpy_s(details, 0x80, utils::string::va("%s on %s", clean_gametype, mapname)); static char clean_hostname[0x80] = {0}; utils::string::strip(game::Dvar_FindVar("sv_hostname")->current.string, clean_hostname, sizeof(clean_hostname)); auto max_clients = party::server_client_count(); - // When true, we are in Private Match if (game::SV_Loaded()) { strcpy_s(clean_hostname, "Private Match"); @@ -218,12 +225,17 @@ namespace discord handlers.joinGame = join_game; handlers.joinRequest = join_request; } + else + { + handlers.joinGame = nullptr; + handlers.joinRequest = nullptr; + } Discord_Initialize("947125042930667530", &handlers, 1, nullptr); scheduler::once(download_default_avatar, scheduler::pipeline::async); - scheduler::once([]() + scheduler::once([] { scheduler::once(update_discord, scheduler::pipeline::async); scheduler::loop(update_discord, scheduler::pipeline::async, 5s); @@ -261,15 +273,12 @@ namespace discord static void join_game(const char* join_secret) { - console::info("Discord: Join game called with join secret: %s\n", join_secret); - - std::string secret = join_secret; - scheduler::once([=]() + scheduler::once([=] { game::netadr_s target{}; - if (game::NET_StringToAdr(secret.data(), &target)) + if (game::NET_StringToAdr(join_secret, &target)) { - console::info("Discord: Connecting to server: %s\n", secret.data()); + console::info("Discord: Connecting to server: %s\n", join_secret); party::connect(target); } }, scheduler::pipeline::main); @@ -277,7 +286,7 @@ namespace discord static void join_request(const DiscordUser* request) { - console::info("Discord: join_request from %s (%s)\n", request->username, request->userId); + console::info("Discord: Join request from %s (%s)\n", request->username, request->userId); if (game::Com_InFrontend() || !ui_scripting::lui_running()) { @@ -290,7 +299,7 @@ namespace discord std::string discriminator = request->discriminator; std::string username = request->username; - scheduler::once([=]() + scheduler::once([=] { const ui_scripting::table request_table{}; request_table.set("avatar", avatar); diff --git a/src/client/component/download.cpp b/src/client/component/download.cpp new file mode 100644 index 00000000..9d4ce938 --- /dev/null +++ b/src/client/component/download.cpp @@ -0,0 +1,231 @@ +#include +#include "loader/component_loader.hpp" + +#include "download.hpp" +#include "console.hpp" +#include "scheduler.hpp" +#include "party.hpp" + +#include "game/ui_scripting/execution.hpp" + +#include +#include +#include +#include + +namespace download +{ + namespace + { + struct globals_t + { + bool abort{}; + bool active{}; + }; + + utils::concurrency::container globals; + + bool download_aborted() + { + return globals.access([](globals_t& globals_) + { + return globals_.abort; + }); + } + + void mark_unactive() + { + globals.access([](globals_t& globals_) + { + globals_.active = false; + }); + } + + void mark_active() + { + globals.access([](globals_t& globals_) + { + globals_.active = true; + }); + } + + bool download_active() + { + return globals.access([](globals_t& globals_) + { + return globals_.active; + }); + } + + auto last_update = std::chrono::high_resolution_clock::now(); + int progress_callback(size_t total, size_t progress) + { + const auto now = std::chrono::high_resolution_clock::now(); + if (now - last_update > 20ms) + { + last_update = std::chrono::high_resolution_clock::now(); + auto fraction = 0.f; + if (total > 0) + { + fraction = static_cast(static_cast(progress) / + static_cast(total)); + } + + scheduler::once([=] + { + ui_scripting::notify("mod_download_progress", + { + {"fraction", fraction}, + }); + }, scheduler::pipeline::lui); + } + + console::debug("Download progress: %lli/%lli\n", progress, total); + if (download_aborted()) + { + return -1; + } + + return 0; + } + + void menu_error(const std::string& error) + { + scheduler::once([=] + { + party::menu_error(error); + }, scheduler::pipeline::lui); + } + } + + void start_download(const game::netadr_s& target, const utils::info_string& info, const std::vector& files) + { + if (download_active()) + { + scheduler::schedule([=] + { + if (!download_active()) + { + start_download(target, info, files); + return scheduler::cond_end; + } + + return scheduler::cond_continue; + }, scheduler::pipeline::main); + + return; + } + + globals.access([&](globals_t& globals_) + { + globals_ = {}; + }); + + const auto base = info.get("sv_wwwBaseUrl"); + if (base.empty()) + { + menu_error("Download failed: Server doesn't have 'sv_wwwBaseUrl' dvar set."); + return; + } + + scheduler::once([] + { + ui_scripting::notify("mod_download_start", {}); + }, scheduler::pipeline::lui); + + scheduler::once([=] + { + { + const auto _0 = gsl::finally(&mark_unactive); + mark_active(); + + if (download_aborted()) + { + return; + } + + for (const auto& file : files) + { + scheduler::once([=] + { + const ui_scripting::table data_table{}; + data_table.set("name", file.name.data()); + + ui_scripting::notify("mod_download_set_file", + { + {"request", data_table} + }); + }, scheduler::pipeline::lui); + + const auto url = utils::string::va("%s/%s", base.data(), file.name.data()); + console::debug("Downloading %s from %s: %s\n", file.name.data(), base.data(), url); + const auto data = utils::http::get_data(url, {}, {}, &progress_callback); + if (!data.has_value()) + { + menu_error("Download failed: An unknown error occurred, please try again."); + return; + } + + if (download_aborted()) + { + return; + } + + const auto& result = data.value(); + if (result.code != CURLE_OK) + { + menu_error(utils::string::va("Download failed: %s (%i)\n", + curl_easy_strerror(result.code), result.code)); + return; + } + + if (result.response_code >= 400) + { + menu_error(utils::string::va("Download failed: Server returned bad response code %i\n", + result.response_code)); + return; + } + + const auto hash = utils::cryptography::sha1::compute(result.buffer, true); + if (hash != file.hash) + { + menu_error(utils::string::va("Download failed: file hash doesn't match the server's (%s: %s != %s)\n", + file.name.data(), hash.data(), file.hash.data())); + return; + } + + utils::io::write_file(file.name, result.buffer, false); + } + } + + scheduler::once([] + { + ui_scripting::notify("mod_download_done", {}); + }, scheduler::pipeline::lui); + + scheduler::once([target] + { + party::connect(target); + }, scheduler::pipeline::main); + }, scheduler::pipeline::async); + } + + void stop_download() + { + if (!download_active()) + { + return; + } + + globals.access([&](globals_t& globals_) + { + globals_.abort = true; + }); + + scheduler::once([] + { + ui_scripting::notify("mod_download_done", {}); + party::menu_error("Download for server mod has been cancelled."); + }, scheduler::pipeline::lui); + } +} diff --git a/src/client/component/download.hpp b/src/client/component/download.hpp new file mode 100644 index 00000000..ddd95790 --- /dev/null +++ b/src/client/component/download.hpp @@ -0,0 +1,17 @@ +#pragma once + +#include "game/game.hpp" + +#include + +namespace download +{ + struct file_t + { + std::string name; + std::string hash; + }; + + void start_download(const game::netadr_s& target, const utils::info_string& info, const std::vector& files); + void stop_download(); +} diff --git a/src/client/component/dvar_cheats.cpp b/src/client/component/dvar_cheats.cpp index 66c49e6c..edb209a9 100644 --- a/src/client/component/dvar_cheats.cpp +++ b/src/client/component/dvar_cheats.cpp @@ -135,7 +135,7 @@ namespace dvar_cheats utils::hook::nop(0x1861D4_b, 8); // let our stub handle zero-source sets utils::hook::jump(0x1861DF_b, get_dvar_flag_checks_stub(), true); // check extra dvar flags when setting values - scheduler::once([]() + scheduler::once([] { dvars::register_bool("sv_cheats", false, game::DvarFlags::DVAR_FLAG_REPLICATED, "Allow cheat commands and dvars on this server"); diff --git a/src/client/component/dvars.cpp b/src/client/component/dvars.cpp index 3b285f20..3fec1fd4 100644 --- a/src/client/component/dvars.cpp +++ b/src/client/component/dvars.cpp @@ -1,5 +1,6 @@ #include #include "loader/component_loader.hpp" + #include "dvars.hpp" #include "game/game.hpp" @@ -252,6 +253,23 @@ namespace dvars } } + namespace callback + { + static std::unordered_map> new_value_callbacks; + + static std::unordered_map> dvar_on_register_function_map; + + void on_new_value(const std::string& name, const std::function callback) + { + new_value_callbacks[game::generateHashValue(name.data())] = callback; + } + + void on_register(const std::string& name, const std::function& callback) + { + dvar_on_register_function_map[game::generateHashValue(name.data())] = callback; + } + } + utils::hook::detour dvar_register_bool_hook; utils::hook::detour dvar_register_bool_hashed_hook; utils::hook::detour dvar_register_float_hook; @@ -263,12 +281,16 @@ namespace dvars utils::hook::detour dvar_register_vector3_hook; utils::hook::detour dvar_register_enum_hook; + utils::hook::detour dvar_register_new_hook; + utils::hook::detour dvar_set_bool_hook; utils::hook::detour dvar_set_float_hook; utils::hook::detour dvar_set_int_hook; utils::hook::detour dvar_set_string_hook; utils::hook::detour dvar_set_from_string_hook; + utils::hook::detour dvar_set_variant_hook; + game::dvar_t* dvar_register_bool(const int hash, const char* name, bool value, unsigned int flags) { auto* var = find_dvar(override::register_bool_overrides, hash); @@ -407,6 +429,20 @@ namespace dvars return dvar_register_enum_hook.invoke(hash, name, value_list, default_index, flags); } + game::dvar_t* dvar_register_new(const int hash, const char* name, game::dvar_type type, unsigned int flags, + game::dvar_value* value, game::dvar_limits* domain, const char* description) + { + auto* dvar = dvar_register_new_hook.invoke(hash, name, type, flags, value, domain, description); + + if (dvar && callback::dvar_on_register_function_map.find(hash) != callback::dvar_on_register_function_map.end()) + { + callback::dvar_on_register_function_map[hash](); + callback::dvar_on_register_function_map.erase(hash); + } + + return dvar; + } + void dvar_set_bool(game::dvar_t* dvar, bool boolean) { const auto disabled = find_dvar(disable::set_bool_disables, dvar->hash); @@ -492,6 +528,16 @@ namespace dvars return dvar_set_from_string_hook.invoke(dvar, string, source); } + void dvar_set_variant(game::dvar_t* dvar, game::dvar_value* value, game::DvarSetSource source) + { + dvar_set_variant_hook.invoke(dvar, value, source); + + if (callback::new_value_callbacks.find(dvar->hash) != callback::new_value_callbacks.end()) + { + callback::new_value_callbacks[dvar->hash](); + } + } + class component final : public component_interface { public: @@ -505,6 +551,8 @@ namespace dvars dvar_register_vector3_hook.create(SELECT_VALUE(0x419A00_b, 0x182DB0_b), &dvar_register_vector3); dvar_register_enum_hook.create(SELECT_VALUE(0x419500_b, 0x182700_b), &dvar_register_enum); + dvar_register_new_hook.create(SELECT_VALUE(0x41B1D0_b, 0x184DF0_b), &dvar_register_new); + if (!game::environment::is_sp()) { dvar_register_bool_hashed_hook.create(SELECT_VALUE(0x0, 0x182420_b), &dvar_register_bool_hashed); @@ -517,6 +565,8 @@ namespace dvars dvar_set_int_hook.create(SELECT_VALUE(0x41BEE0_b, 0x185D10_b), &dvar_set_int); dvar_set_string_hook.create(SELECT_VALUE(0x41C0F0_b, 0x186080_b), &dvar_set_string); dvar_set_from_string_hook.create(SELECT_VALUE(0x41BE20_b, 0x185C60_b), &dvar_set_from_string); + + dvar_set_variant_hook.create(SELECT_VALUE(0x41C190_b, 0x186120_b), &dvar_set_variant); } }; } diff --git a/src/client/component/dvars.hpp b/src/client/component/dvars.hpp index 419530e2..3c9831c8 100644 --- a/src/client/component/dvars.hpp +++ b/src/client/component/dvars.hpp @@ -26,4 +26,11 @@ namespace dvars void set_string(const std::string& name, const std::string& string); void set_from_string(const std::string& name, const std::string& value); } + + namespace callback + { + void on_new_value(const std::string& name, const std::function callback); + + void on_register(const std::string& name, const std::function& callback); + } } diff --git a/src/client/component/exception.cpp b/src/client/component/exception.cpp index 598473ad..86190fa1 100644 --- a/src/client/component/exception.cpp +++ b/src/client/component/exception.cpp @@ -1,8 +1,11 @@ #include #include "loader/component_loader.hpp" -#include "system_check.hpp" -#include "scheduler.hpp" +#include "scheduler.hpp" +#include "system_check.hpp" +#include "version.hpp" + +#include "game/dvars.hpp" #include "game/game.hpp" #include @@ -13,10 +16,6 @@ #include -#include - -#include "game/dvars.hpp" - namespace exception { namespace @@ -101,7 +100,7 @@ namespace exception utils::thread::suspend_other_threads(); show_mouse_cursor(); - MessageBoxA(nullptr, error_str.data(), "H1-Mod ERROR", MB_ICONERROR); + MSG_BOX_ERROR(error_str.data()); TerminateProcess(GetCurrentProcess(), exception_data.code); } @@ -246,7 +245,7 @@ namespace exception SetUnhandledExceptionFilter(exception_filter); utils::hook::jump(SetUnhandledExceptionFilter, set_unhandled_exception_filter_stub, true); - scheduler::on_game_initialized([]() + scheduler::on_game_initialized([] { is_initialized() = true; }); @@ -255,7 +254,7 @@ namespace exception void post_unpack() override { dvars::cg_legacyCrashHandling = dvars::register_bool("cg_legacyCrashHandling", - false, game::DVAR_FLAG_SAVED, "Disable new crash handling"); + false, game::DVAR_FLAG_SAVED, "Toggle new crash handling"); } }; } diff --git a/src/client/component/fastfiles.cpp b/src/client/component/fastfiles.cpp index 1ff9a8ae..a85bfb8b 100644 --- a/src/client/component/fastfiles.cpp +++ b/src/client/component/fastfiles.cpp @@ -1,11 +1,12 @@ #include #include "loader/component_loader.hpp" -#include "game/dvars.hpp" - -#include "fastfiles.hpp" #include "command.hpp" #include "console.hpp" +#include "fastfiles.hpp" +#include "filesystem.hpp" + +#include "game/dvars.hpp" #include #include @@ -15,6 +16,7 @@ namespace fastfiles { static utils::concurrency::container current_fastfile; + static utils::concurrency::container> current_usermap; namespace { @@ -22,6 +24,8 @@ namespace fastfiles utils::hook::detour db_find_xasset_header_hook; game::dvar_t* g_dump_scripts; + std::vector fastfile_handles; + void db_try_load_x_file_internal(const char* zone_name, const int flags) { console::info("Loading fastfile %s\n", zone_name); @@ -56,7 +60,7 @@ namespace fastfiles game::XAssetHeader db_find_xasset_header_stub(game::XAssetType type, const char* name, int allow_create_default) { const auto start = game::Sys_Milliseconds(); - const auto result = db_find_xasset_header_hook.invoke(type, name, allow_create_default); + auto result = db_find_xasset_header_hook.invoke(type, name, allow_create_default); const auto diff = game::Sys_Milliseconds() - start; if (type == game::XAssetType::ASSET_TYPE_SCRIPTFILE) @@ -64,6 +68,20 @@ namespace fastfiles dump_gsc_script(name, result); } + if (type == game::XAssetType::ASSET_TYPE_RAWFILE) + { + if (result.rawfile) + { + const std::string override_rawfile_name = "override/"s + name; + const auto override_rawfile = db_find_xasset_header_hook.invoke(type, override_rawfile_name.data(), 0); + if (override_rawfile.rawfile) + { + result.rawfile = override_rawfile.rawfile; + console::debug("using override asset for rawfile: \"%s\"\n", name); + } + } + } + if (diff > 100) { console::print( @@ -82,6 +100,350 @@ namespace fastfiles return result; } + + utils::hook::detour db_read_stream_file_hook; + void db_read_stream_file_stub(int a1, int a2) + { + // always use lz4 compressor type when reading stream files + *game::g_compressor = 4; + return db_read_stream_file_hook.invoke(a1, a2); + } + + namespace mp + { + void skip_extra_zones_stub(utils::hook::assembler& a) + { + const auto skip = a.newLabel(); + const auto original = a.newLabel(); + + a.pushad64(); + a.test(esi, game::DB_ZONE_CUSTOM); // allocFlags + a.jnz(skip); + + a.bind(original); + a.popad64(); + a.mov(rdx, 0x8E2F80_b); + a.mov(rcx, rbp); + a.call(0x840A20_b); + a.jmp(0x398070_b); + + a.bind(skip); + a.popad64(); + a.mov(r14d, game::DB_ZONE_CUSTOM); + a.not_(r14d); + a.and_(esi, r14d); + a.jmp(0x39814F_b); + } + } + namespace sp + { + void skip_extra_zones_stub(utils::hook::assembler& a) + { + const auto skip = a.newLabel(); + const auto original = a.newLabel(); + + a.pushad64(); + a.test(ebp, game::DB_ZONE_CUSTOM); // allocFlags + a.jnz(skip); + + a.bind(original); + a.popad64(); + a.mov(r8d, 9); + a.mov(rdx, 0x782210_b); + a.jmp(0x1F4006_b); + + a.bind(skip); + a.popad64(); + a.mov(r15d, game::DB_ZONE_CUSTOM); + a.not_(r15d); + a.and_(ebp, r15d); + a.jmp(0x1F4023_b); + } + } + + bool try_load_zone(std::string name, bool localized, bool game = false) + { + if (localized) + { + const auto language = game::SEH_GetCurrentLanguageCode(); + try_load_zone(language + "_"s + name, false); + if (game::environment::is_mp()) + { + try_load_zone(language + "_"s + name + "_mp"s, false); + } + } + + if (!fastfiles::exists(name)) + { + return false; + } + + game::XZoneInfo info{}; + info.name = name.data(); + info.allocFlags = (game ? game::DB_ZONE_GAME : game::DB_ZONE_COMMON) | game::DB_ZONE_CUSTOM; + info.freeFlags = 0; + game::DB_LoadXAssets(&info, 1u, game::DBSyncMode::DB_LOAD_ASYNC); + return true; + } + + HANDLE find_fastfile(const std::string& filename, bool check_loc_folder) + { + std::string path{}; + std::string loc_folder{}; + + if (check_loc_folder && game::DB_IsLocalized(filename.data())) + { + const auto handle = find_fastfile(filename, false); + if (handle != INVALID_HANDLE_VALUE) + { + return handle; + } + + loc_folder = game::SEH_GetCurrentLanguageName() + "/"s; + } + + if (!filesystem::find_file(loc_folder + filename, &path)) + { + if (!filesystem::find_file("zone/"s + loc_folder + filename, &path)) + { + return INVALID_HANDLE_VALUE; + } + } + + const auto handle = CreateFileA(path.data(), GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, + FILE_FLAG_OVERLAPPED | FILE_FLAG_NO_BUFFERING, nullptr); + if (handle != INVALID_HANDLE_VALUE) + { + fastfile_handles.push_back(handle); + } + + return handle; + } + + HANDLE find_usermap(const std::string& mapname) + { + const auto usermap = fastfiles::get_current_usermap(); + if (!usermap.has_value()) + { + return INVALID_HANDLE_VALUE; + } + + const auto& usermap_value = usermap.value(); + const std::string usermap_file = utils::string::va("%s.ff", usermap_value.data()); + const std::string usermap_load_file = utils::string::va("%s_load.ff", usermap_value.data()); + + if (mapname == usermap_file || mapname == usermap_load_file) + { + const auto path = utils::string::va("usermaps\\%s\\%s", + usermap_value.data(), mapname.data()); + if (utils::io::file_exists(path)) + { + return CreateFileA(path, GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, + FILE_FLAG_OVERLAPPED | FILE_FLAG_NO_BUFFERING, nullptr); + } + } + + return INVALID_HANDLE_VALUE; + } + + utils::hook::detour sys_createfile_hook; + HANDLE sys_create_file(game::Sys_Folder folder, const char* base_filename, bool ignore_usermap) + { + const auto* fs_basepath = game::Dvar_FindVar("fs_basepath"); + const auto* fs_game = game::Dvar_FindVar("fs_game"); + + const std::string dir = fs_basepath ? fs_basepath->current.string : ""; + const std::string mod_dir = fs_game ? fs_game->current.string : ""; + const std::string name = base_filename; + + if (name == "mod.ff") + { + if (!mod_dir.empty()) + { + const auto path = utils::string::va("%s\\%s\\%s", + dir.data(), mod_dir.data(), base_filename); + + if (utils::io::file_exists(path)) + { + return CreateFileA(path, GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, + FILE_FLAG_OVERLAPPED | FILE_FLAG_NO_BUFFERING, nullptr); + } + } + + return INVALID_HANDLE_VALUE; + } + + auto handle = sys_createfile_hook.invoke(folder, base_filename); + if (handle != INVALID_HANDLE_VALUE) + { + return handle; + } + + if (!ignore_usermap) + { + const auto usermap = find_usermap(name); + if (usermap != INVALID_HANDLE_VALUE) + { + return usermap; + } + } + + if (name.ends_with(".ff")) + { + handle = find_fastfile(name, true); + } + + return handle; + } + + HANDLE sys_create_file_stub(game::Sys_Folder folder, const char* base_filename) + { + return sys_create_file(folder, base_filename, false); + } + + utils::hook::detour db_file_exists_hook; + bool db_file_exists_stub(const char* file, int a2) + { + const auto file_exists = db_file_exists_hook.invoke(file, a2); + if (file_exists) + { + return file_exists; + } + + return fastfiles::usermap_exists(file); + } + + template + inline void merge(std::vector* target, T* source, size_t length) + { + if (source) + { + for (size_t i = 0; i < length; ++i) + { + target->push_back(source[i]); + } + } + } + + template + inline void merge(std::vector* target, std::vector source) + { + for (auto& entry : source) + { + target->push_back(entry); + } + } + + void load_pre_gfx_zones(game::XZoneInfo* zoneInfo, unsigned int zoneCount, game::DBSyncMode syncMode) + { + std::vector data; + merge(&data, zoneInfo, zoneCount); + + // code_pre_gfx + + game::DB_LoadXAssets(data.data(), static_cast(data.size()), syncMode); + } + + void load_post_gfx_and_ui_and_common_zones(game::XZoneInfo* zoneInfo, unsigned int zoneCount, game::DBSyncMode syncMode) + { + std::vector data; + merge(&data, zoneInfo, zoneCount); + + // code_post_gfx + // ui + // common + + try_load_zone("h1_mod_common", true); + + game::DB_LoadXAssets(data.data(), static_cast(data.size()), syncMode); + + try_load_zone("mod", true); + } + + void load_ui_zones(game::XZoneInfo* zoneInfo, unsigned int zoneCount, game::DBSyncMode syncMode) + { + std::vector data; + merge(&data, zoneInfo, zoneCount); + + // ui + + game::DB_LoadXAssets(data.data(), static_cast(data.size()), syncMode); + } + + void load_lua_file_asset_stub(void* a1) + { + const auto fastfile = fastfiles::get_current_fastfile(); + if (fastfile == "mod") + { + console::error("Mod tried to load a lua file!\n"); + return; + } + + const auto usermap = fastfiles::get_current_usermap(); + if (usermap.has_value()) + { + const auto& usermap_value = usermap.value(); + const auto usermap_load = usermap_value + "_load"; + + if (fastfile == usermap_value || fastfile == usermap_load) + { + console::error("Usermap tried to load a lua file!\n"); + return; + } + } + + utils::hook::invoke(0x39CA90_b, a1); + } + + void db_level_load_add_zone_stub(void* load, const char* name, const unsigned int alloc_flags, + const size_t size_est) + { + auto is_builtin_map = false; + for (auto map = &game::maps[0]; map->unk; ++map) + { + if (!std::strcmp(map->name, name)) + { + is_builtin_map = true; + break; + } + } + + if (is_builtin_map) + { + game::DB_LevelLoadAddZone(load, name, alloc_flags, size_est); + } + else + { + game::DB_LevelLoadAddZone(load, name, alloc_flags | game::DB_ZONE_CUSTOM, size_est); + } + } + + void db_find_aipaths_stub(game::XAssetType type, const char* name, int allow_create_default) + { + if (game::DB_XAssetExists(type, name)) + { + game::DB_FindXAssetHeader(type, name, allow_create_default); + } + else + { + console::warn("No aipaths found for this map\n"); + } + } + } + + bool exists(const std::string& zone, bool ignore_usermap) + { + const auto is_localized = game::DB_IsLocalized(zone.data()); + const auto handle = sys_create_file((is_localized ? game::SF_ZONE_LOC : game::SF_ZONE), + utils::string::va("%s.ff", zone.data()), ignore_usermap); + + if (handle != INVALID_HANDLE_VALUE) + { + CloseHandle(handle); + return true; + } + + return false; } std::string get_current_fastfile() @@ -92,7 +454,7 @@ namespace fastfiles }); } - void enum_assets(const game::XAssetType type, + void enum_assets(const game::XAssetType type, const std::function& callback, const bool includeOverride) { game::DB_EnumXAssets_Internal(type, static_cast([](game::XAssetHeader header, void* data) @@ -102,6 +464,54 @@ namespace fastfiles }), &callback, includeOverride); } + void close_fastfile_handles() + { + for (const auto& handle : fastfile_handles) + { + CloseHandle(handle); + } + } + + void set_usermap(const std::string& usermap) + { + current_usermap.access([&](std::optional& current_usermap_) + { + current_usermap_ = usermap; + }); + } + + void clear_usermap() + { + current_usermap.access([&](std::optional& current_usermap_) + { + current_usermap_.reset(); + }); + } + + std::optional get_current_usermap() + { + return current_usermap.access>([&]( + std::optional& current_usermap_) + { + return current_usermap_; + }); + } + + bool usermap_exists(const std::string& name) + { + if (is_stock_map(name)) + { + return false; + } + + return utils::io::file_exists(utils::string::va("usermaps\\%s\\%s.ff", name.data(), name.data())); + } + + bool is_stock_map(const std::string& name) + { + return fastfiles::exists(name, true); + } + class component final : public component_interface { public: @@ -109,10 +519,82 @@ namespace fastfiles { db_try_load_x_file_internal_hook.create( SELECT_VALUE(0x1F5700_b, 0x39A620_b), &db_try_load_x_file_internal); - db_find_xasset_header_hook.create(game::DB_FindXAssetHeader, db_find_xasset_header_stub); g_dump_scripts = dvars::register_bool("g_dumpScripts", false, game::DVAR_FLAG_NONE, "Dump GSC scripts"); + + // Allow loading of unsigned fastfiles + if (!game::environment::is_sp()) + { + utils::hook::nop(0x368153_b, 2); // DB_InflateInit + } + + if (game::environment::is_sp()) + { + // Allow loading mp maps + utils::hook::set(0x40AF90_b, 0xC300B0); + // Don't sys_error if aipaths are missing + utils::hook::call(0x2F8EE9_b, db_find_aipaths_stub); + } + + // Allow loading of mixed compressor types + utils::hook::nop(SELECT_VALUE(0x1C4BE7_b, 0x3687A7_b), 2); + + // Fix compressor type on streamed file load + db_read_stream_file_hook.create(SELECT_VALUE(0x1FB9D0_b, 0x3A1BF0_b), db_read_stream_file_stub); + + // Add custom zone paths + sys_createfile_hook.create(game::Sys_CreateFile, sys_create_file_stub); + if (!game::environment::is_sp()) + { + db_file_exists_hook.create(0x394DC0_b, db_file_exists_stub); + } + + // load our custom pre_gfx zones + utils::hook::call(SELECT_VALUE(0x3862ED_b, 0x15C3FD_b), load_pre_gfx_zones); + utils::hook::call(SELECT_VALUE(0x3865E7_b, 0x15C75D_b), load_pre_gfx_zones); + + // load our custom ui and common zones + utils::hook::call(SELECT_VALUE(0x5634AA_b, 0x686421_b), load_post_gfx_and_ui_and_common_zones); + + // load our custom ui zones + utils::hook::call(SELECT_VALUE(0x3A5676_b, 0x17C6D2_b), load_ui_zones); + + // Don't load extra zones with loadzone + if (game::environment::is_sp()) + { + utils::hook::nop(0x1F3FF9_b, 13); + utils::hook::jump(0x1F3FF9_b, utils::hook::assemble(sp::skip_extra_zones_stub), true); + } + else + { + utils::hook::nop(0x398061_b, 15); + utils::hook::jump(0x398061_b, utils::hook::assemble(mp::skip_extra_zones_stub), true); + + // dont load localized zone for custom maps + utils::hook::call(0x394A99_b, db_level_load_add_zone_stub); + } + + // prevent mod.ff from loading lua files + if (game::environment::is_mp()) + { + utils::hook::call(0x3757B4_b, load_lua_file_asset_stub); + } + + command::add("loadzone", [](const command::params& params) + { + if (params.size() < 2) + { + console::info("usage: loadzone \n"); + return; + } + + const auto name = params.get(1); + if (!try_load_zone(name, false)) + { + console::warn("loadzone: zone \"%s\" could not be found!\n", name); + } + }); } }; } diff --git a/src/client/component/fastfiles.hpp b/src/client/component/fastfiles.hpp index 4f4108a0..319d3e25 100644 --- a/src/client/component/fastfiles.hpp +++ b/src/client/component/fastfiles.hpp @@ -4,8 +4,18 @@ namespace fastfiles { + bool exists(const std::string& zone, bool ignore_usermap = false); + std::string get_current_fastfile(); void enum_assets(const game::XAssetType type, const std::function& callback, const bool includeOverride); + + void close_fastfile_handles(); + + void set_usermap(const std::string& usermap); + void clear_usermap(); + std::optional get_current_usermap(); + bool usermap_exists(const std::string& name); + bool is_stock_map(const std::string& name); } diff --git a/src/client/component/filesystem.cpp b/src/client/component/filesystem.cpp index 15d11826..98029d0e 100644 --- a/src/client/component/filesystem.cpp +++ b/src/client/component/filesystem.cpp @@ -1,72 +1,155 @@ #include #include "loader/component_loader.hpp" + +#include "command.hpp" +#include "console.hpp" #include "filesystem.hpp" -#include "game_module.hpp" +#include "localized_strings.hpp" +#include "updater.hpp" #include "game/game.hpp" -#include "dvars.hpp" -#include -#include #include +#include +#include +#include + +#define LANGUAGE_FILE "players2/default/language" namespace filesystem { - file::file(std::string name) - : name_(std::move(name)) + namespace { - char* buffer{}; - const auto size = game::FS_ReadFile(this->name_.data(), &buffer); + utils::hook::detour fs_startup_hook; - if (size >= 0 && buffer) + bool initialized = false; + + std::deque& get_search_paths_internal() { - this->valid_ = true; - this->buffer_.append(buffer, size); - game::FS_FreeFile(buffer); + static std::deque search_paths{}; + return search_paths; } - } - bool file::exists() const - { - return this->valid_; - } + bool is_fallback_lang() + { + static const auto* loc_language = game::Dvar_FindVar("loc_language"); + const auto id = loc_language->current.integer; + return id == 5 || id == 6 || id == 8 || id == 9 || id == 10 || id == 11 || id == 13 || id == 15; + } - const std::string& file::get_buffer() const - { - return this->buffer_; - } + void fs_startup_stub(const char* name) + { + console::debug("[FS] Startup\n"); - const std::string& file::get_name() const - { - return this->name_; - } + initialized = true; - std::unordered_set& get_search_paths() - { - static std::unordered_set search_paths{}; - return search_paths; + // hardcoded paths + filesystem::register_path(utils::properties::get_appdata_path() / CLIENT_DATA_FOLDER); + filesystem::register_path(L"."); + filesystem::register_path(L"h1-mod"); + + fs_startup_hook.invoke(name); + + command::register_fs_game_path(); + } + + std::vector get_paths(const std::filesystem::path& path) + { + std::vector paths{}; + + const auto code = game::SEH_GetCurrentLanguageName(); + + if (!::utils::io::file_exists(LANGUAGE_FILE) or ::utils::io::file_size(LANGUAGE_FILE) == 0) + { + ::utils::io::write_file(LANGUAGE_FILE, code); + } + + paths.push_back(path); + + if (is_fallback_lang()) + { + paths.push_back(path / "fallback"); + } + + paths.push_back(path / code); + + return paths; + } + + bool can_insert_path(const std::filesystem::path& path) + { + for (const auto& path_ : get_search_paths_internal()) + { + if (path_ == path) + { + return false; + } + } + + return true; + } + + const char* sys_default_install_path_stub() + { + static auto current_path = std::filesystem::current_path().string(); + return current_path.data(); + } } std::string read_file(const std::string& path) { - for (const auto& search_path : get_search_paths()) + for (const auto& search_path : get_search_paths_internal()) { - const auto path_ = search_path + "/" + path; - if (utils::io::file_exists(path_)) + const auto path_ = search_path / path; + if (utils::io::file_exists(path_.generic_string())) { - return utils::io::read_file(path_); + return utils::io::read_file(path_.generic_string()); } } return {}; } - bool read_file(const std::string& path, std::string* data) + bool read_file(const std::string& path, std::string* data, std::string* real_path) { - for (const auto& search_path : get_search_paths()) + for (const auto& search_path : get_search_paths_internal()) { - const auto path_ = search_path + "/" + path; - if (utils::io::read_file(path_, data)) + const auto path_ = search_path / path; + if (utils::io::read_file(path_.generic_string(), data)) + { + if (real_path != nullptr) + { + *real_path = path_.generic_string(); + } + + return true; + } + } + + return false; + } + + bool find_file(const std::string& path, std::string* real_path) + { + for (const auto& search_path : get_search_paths_internal()) + { + const auto path_ = search_path / path; + if (utils::io::file_exists(path_.generic_string())) + { + *real_path = path_.generic_string(); + return true; + } + } + + return false; + } + + bool exists(const std::string& path) + { + for (const auto& search_path : get_search_paths_internal()) + { + const auto path_ = search_path / path; + if (utils::io::file_exists(path_.generic_string())) { return true; } @@ -75,16 +158,88 @@ namespace filesystem return false; } + void register_path(const std::filesystem::path& path) + { + if (!initialized) + { + return; + } + + const auto paths = get_paths(path); + for (const auto& path_ : paths) + { + if (can_insert_path(path_)) + { + console::debug("[FS] Registering path '%s'\n", path_.generic_string().data()); + get_search_paths_internal().push_front(path_); + } + } + } + + void unregister_path(const std::filesystem::path& path) + { + if (!initialized) + { + return; + } + + const auto paths = get_paths(path); + for (const auto& path_ : paths) + { + auto& search_paths = get_search_paths_internal(); + for (auto i = search_paths.begin(); i != search_paths.end();) + { + if (*i == path_) + { + console::debug("[FS] Unregistering path '%s'\n", path_.generic_string().data()); + i = search_paths.erase(i); + } + else + { + ++i; + } + } + } + } + + std::vector get_search_paths() + { + std::vector paths{}; + + for (const auto& path : get_search_paths_internal()) + { + paths.push_back(path.generic_string()); + } + + return paths; + } + + std::vector get_search_paths_rev() + { + std::vector paths{}; + const auto& search_paths = get_search_paths_internal(); + + for (auto i = search_paths.rbegin(); i != search_paths.rend(); ++i) + { + paths.push_back(i->generic_string()); + } + + return paths; + } + class component final : public component_interface { public: void post_unpack() override { - get_search_paths().insert("."); - get_search_paths().insert("h1-mod"); - get_search_paths().insert("data"); + fs_startup_hook.create(SELECT_VALUE(0x40D890_b, 0x189A40_b), fs_startup_stub); + + utils::hook::jump(SELECT_VALUE(0x42CE00_b, 0x5B3440_b), sys_default_install_path_stub); + + // fs_game flags + utils::hook::set(SELECT_VALUE(0x40D2A5_b, 0x189275_b), 0); } }; } -REGISTER_COMPONENT(filesystem::component) \ No newline at end of file +REGISTER_COMPONENT(filesystem::component) diff --git a/src/client/component/filesystem.hpp b/src/client/component/filesystem.hpp index c3d36dc6..67d1d236 100644 --- a/src/client/component/filesystem.hpp +++ b/src/client/component/filesystem.hpp @@ -2,22 +2,14 @@ namespace filesystem { - class file - { - public: - file(std::string name); - - bool exists() const; - const std::string& get_buffer() const; - const std::string& get_name() const; - - private: - bool valid_ = false; - std::string name_; - std::string buffer_; - }; - - std::unordered_set& get_search_paths(); std::string read_file(const std::string& path); - bool read_file(const std::string& path, std::string* data); -} \ No newline at end of file + bool read_file(const std::string& path, std::string* data, std::string* real_path = nullptr); + bool find_file(const std::string& path, std::string* real_path); + bool exists(const std::string& path); + + void register_path(const std::filesystem::path& path); + void unregister_path(const std::filesystem::path& path); + + std::vector get_search_paths(); + std::vector get_search_paths_rev(); +} diff --git a/src/client/component/fonts.cpp b/src/client/component/fonts.cpp index 07f447ca..e38c2ff6 100644 --- a/src/client/component/fonts.cpp +++ b/src/client/component/fonts.cpp @@ -1,8 +1,8 @@ #include #include "loader/component_loader.hpp" -#include "fonts.hpp" #include "console.hpp" +#include "fonts.hpp" #include "filesystem.hpp" #include "game/game.hpp" diff --git a/src/client/component/fps.cpp b/src/client/component/fps.cpp index dc721c3b..056398cf 100644 --- a/src/client/component/fps.cpp +++ b/src/client/component/fps.cpp @@ -1,15 +1,15 @@ #include #include "loader/component_loader.hpp" +#include "dvars.hpp" #include "fps.hpp" +#include "scheduler.hpp" #include "game/game.hpp" #include "game/dvars.hpp" -#include "dvars.hpp" #include #include -#include namespace fps { diff --git a/src/client/component/gameplay.cpp b/src/client/component/gameplay.cpp index 891ea57e..accb63e2 100644 --- a/src/client/component/gameplay.cpp +++ b/src/client/component/gameplay.cpp @@ -82,19 +82,19 @@ namespace gameplay a.mov(rax, qword_ptr(reinterpret_cast(&dvars::pm_bouncing))); a.mov(al, byte_ptr(rax, 0x10)); - a.cmp(byte_ptr(rbp, -0x7D), al); + a.cmp(byte_ptr(rbp, SELECT_VALUE(-0x5D, -0x7D)), al); a.pop(rax); a.jz(no_bounce); - a.jmp(0x2D39C0_b); + a.jmp(SELECT_VALUE(0x4A2E81_b, 0x2D39C0_b)); a.bind(no_bounce); a.cmp(dword_ptr(rsp, 0x44), 0); a.jnz(loc_2D395D); - a.jmp(0x2D39B1_b); + a.jmp(SELECT_VALUE(0x4A2E6F_b, 0x2D39B1_b)); a.bind(loc_2D395D); - a.jmp(0x2D395D_b); + a.jmp(SELECT_VALUE(0x4A2F18_b, 0x2D395D_b)); }); } @@ -330,6 +330,10 @@ namespace gameplay utils::hook::jump(SELECT_VALUE(0x499617_b, 0x2C9F90_b), utils::hook::assemble(pm_trace_stub), true); dvars::g_enableElevators = dvars::register_bool("g_enableElevators", false, game::DVAR_FLAG_REPLICATED, "Enables Elevators"); + dvars::pm_bouncing = dvars::register_bool("pm_bouncing", false, + game::DVAR_FLAG_REPLICATED, "Enable bouncing"); + utils::hook::jump(SELECT_VALUE(0x4A2E5E_b, 0x2D39A4_b), pm_bouncing_stub_mp(), true); + if (game::environment::is_sp()) { return; @@ -340,10 +344,6 @@ namespace gameplay dvars::g_speed = dvars::register_int("g_speed", 190, 0, 1000, game::DVAR_FLAG_REPLICATED, "changes the speed of the player"); - dvars::pm_bouncing = dvars::register_bool("pm_bouncing", false, - game::DVAR_FLAG_REPLICATED, "Enable bouncing"); - utils::hook::jump(0x2D39A4_b, pm_bouncing_stub_mp(), true); - dvars::pm_bouncingAllAngles = dvars::register_bool("pm_bouncingAllAngles", false, game::DvarFlags::DVAR_FLAG_REPLICATED, "Enable bouncing from all angles"); utils::hook::call(0x2D3A74_b, pm_project_velocity_stub); diff --git a/src/client/component/gsc/script_error.cpp b/src/client/component/gsc/script_error.cpp new file mode 100644 index 00000000..20af68f2 --- /dev/null +++ b/src/client/component/gsc/script_error.cpp @@ -0,0 +1,123 @@ +#include +#include "loader/component_loader.hpp" +#include "game/game.hpp" + +#include "script_error.hpp" + +#include "component/scripting.hpp" + +#include + +namespace gsc +{ + namespace + { + utils::hook::detour scr_emit_function_hook; + + std::uint32_t current_filename = 0; + + std::string unknown_function_error; + + void scr_emit_function_stub(std::uint32_t filename, std::uint32_t thread_name, char* code_pos) + { + current_filename = filename; + scr_emit_function_hook.invoke(filename, thread_name, code_pos); + } + + std::string get_filename_name() + { + const auto filename_str = game::SL_ConvertToString(static_cast(current_filename)); + const auto id = std::atoi(filename_str); + if (!id) + { + return filename_str; + } + + return scripting::get_token(id); + } + + void get_unknown_function_error(const char* code_pos) + { + const auto function = find_function(code_pos); + if (function.has_value()) + { + const auto& pos = function.value(); + unknown_function_error = std::format( + "while processing function '{}' in script '{}':\nunknown script '{}'", + pos.first, pos.second, scripting::current_file + ); + } + else + { + unknown_function_error = std::format("unknown script '{}'", scripting::current_file); + } + } + + void get_unknown_function_error(std::uint32_t thread_name) + { + const auto filename = get_filename_name(); + const auto name = scripting::get_token(thread_name); + + unknown_function_error = std::format( + "while processing script '{}':\nunknown function '{}::{}'", + scripting::current_file, filename, name + ); + } + + void unknown_function_stub(const char* code_pos) + { + get_unknown_function_error(code_pos); + game::Com_Error(game::ERR_DROP, "script link error\n%s", + unknown_function_error.data()); + } + + std::uint32_t find_variable_stub(std::uint32_t parent_id, std::uint32_t thread_name) + { + const auto res = game::FindVariable(parent_id, thread_name); + if (!res) + { + get_unknown_function_error(thread_name); + game::Com_Error(game::ERR_DROP, "script link error\n%s", + unknown_function_error.data()); + } + return res; + } + } + + std::optional> find_function(const char* pos) + { + for (const auto& file : scripting::script_function_table_sort) + { + for (auto i = file.second.begin(); i != file.second.end() && std::next(i) != file.second.end(); ++i) + { + const auto next = std::next(i); + if (pos >= i->second && pos < next->second) + { + return {std::make_pair(i->first, file.first)}; + } + } + } + + return {}; + } + + class error final : public component_interface + { + public: + void post_unpack() override + { + scr_emit_function_hook.create(SELECT_VALUE(0x3BD680_b, 0x504660_b), &scr_emit_function_stub); + + utils::hook::call(SELECT_VALUE(0x3BD626_b, 0x504606_b), unknown_function_stub); // CompileError (LinkFile) + utils::hook::call(SELECT_VALUE(0x3BD672_b, 0x504652_b), unknown_function_stub); // ^ + utils::hook::call(SELECT_VALUE(0x3BD75A_b, 0x50473A_b), find_variable_stub); // Scr_EmitFunction + } + + void pre_destroy() override + { + scr_emit_function_hook.clear(); + } + }; +} + +REGISTER_COMPONENT(gsc::error) diff --git a/src/client/component/gsc/script_error.hpp b/src/client/component/gsc/script_error.hpp new file mode 100644 index 00000000..e8742026 --- /dev/null +++ b/src/client/component/gsc/script_error.hpp @@ -0,0 +1,7 @@ + +#pragma once + +namespace gsc +{ + std::optional> find_function(const char* pos); +} \ No newline at end of file diff --git a/src/client/component/gsc/script_extension.cpp b/src/client/component/gsc/script_extension.cpp new file mode 100644 index 00000000..9afcfd0b --- /dev/null +++ b/src/client/component/gsc/script_extension.cpp @@ -0,0 +1,501 @@ +#include +#include "loader/component_loader.hpp" + +#include "game/dvars.hpp" +#include "game/game.hpp" +#include "game/scripting/execution.hpp" +#include "game/scripting/function.hpp" +#include "game/scripting/functions.hpp" +#include "game/scripting/lua/error.hpp" + +#include + +#include "component/command.hpp" +#include "component/console.hpp" +#include "component/scripting.hpp" +#include "component/logfile.hpp" + +#include +#include + +#include "script_extension.hpp" +#include "script_error.hpp" + +namespace gsc +{ + std::uint16_t function_id_start = 0x30A; + std::uint16_t method_id_start = 0x8586; + + builtin_function func_table[0x1000]; + builtin_method meth_table[0x1000]; + + const game::dvar_t* developer_script = nullptr; + + namespace + { + std::unordered_map functions; + std::unordered_map methods; + + bool force_error_print = false; + std::optional gsc_error_msg; + game::scr_entref_t saved_ent_ref; + + function_args get_arguments() + { + std::vector args; + + for (auto i = 0; static_cast(i) < game::scr_VmPub->outparamcount; ++i) + { + const auto value = game::scr_VmPub->top[-i]; + args.push_back(value); + } + + return args; + } + + void return_value(const scripting::script_value& value) + { + if (game::scr_VmPub->outparamcount) + { + game::Scr_ClearOutParams(); + } + + scripting::push_value(value); + } + + std::uint16_t get_function_id() + { + const auto pos = game::scr_function_stack->pos; + return *reinterpret_cast( + reinterpret_cast(pos - 2)); + } + + void execute_custom_function(const std::uint16_t id) + { + auto error = false; + + try + { + const auto& function = functions[id]; + const auto result = function(get_arguments()); + const auto type = result.get_raw().type; + + if (type) + { + return_value(result); + } + } + catch (const std::exception& e) + { + error = true; + force_error_print = true; + gsc_error_msg = e.what(); + } + + if (error) + { + game::Scr_ErrorInternal(); + } + } + + void execute_custom_method(const std::uint16_t id) + { + auto error = false; + + try + { + const auto& method = methods[id]; + const auto result = method(saved_ent_ref, get_arguments()); + const auto type = result.get_raw().type; + + if (type) + { + return_value(result); + } + } + catch (const std::exception& e) + { + error = true; + force_error_print = true; + gsc_error_msg = e.what(); + } + + if (error) + { + game::Scr_ErrorInternal(); + } + } + + void vm_call_builtin_function_stub(builtin_function function) + { + const auto function_id = get_function_id(); + + if (!functions.contains(function_id)) + { + function(); + } + else + { + execute_custom_function(function_id); + } + } + + game::scr_entref_t get_entity_id_stub(std::uint32_t ent_id) + { + const auto ref = game::Scr_GetEntityIdRef(ent_id); + saved_ent_ref = ref; + return ref; + } + + void vm_call_builtin_method_stub(builtin_method method) + { + const auto function_id = get_function_id(); + + if (!methods.contains(function_id)) + { + method(saved_ent_ref); + } + else + { + execute_custom_method(function_id); + } + } + + void builtin_call_error(const std::string& error) + { + const auto function_id = get_function_id(); + + if (function_id > 0x1000) + { + console::warn("in call to builtin method \"%s\"%s", + xsk::gsc::h1::resolver::method_name(function_id).data(), error.data()); + } + else + { + console::warn("in call to builtin function \"%s\"%s", + xsk::gsc::h1::resolver::function_name(function_id).data(), error.data()); + } + } + + std::optional get_opcode_name(const std::uint8_t opcode) + { + try + { + return {xsk::gsc::h1::resolver::opcode_name(opcode)}; + } + catch (...) + { + return {}; + } + } + + void print_callstack() + { + for (auto frame = game::scr_VmPub->function_frame; frame != game::scr_VmPub->function_frame_start; --frame) + { + const auto pos = frame == game::scr_VmPub->function_frame ? game::scr_function_stack->pos : frame->fs.pos; + const auto function = find_function(frame->fs.pos); + + if (function.has_value()) + { + console::warn("\tat function \"%s\" in file \"%s.gsc\"\n", function.value().first.data(), function.value().second.data()); + } + else + { + console::warn("\tat unknown location %p\n", pos); + } + } + } + + void vm_error_stub(int mark_pos) + { + if (!developer_script->current.enabled && !force_error_print) + { + utils::hook::invoke(SELECT_VALUE(0x415C90_b, 0x59DDA0_b), mark_pos); + return; + } + + console::warn("*********** script runtime error *************\n"); + + const auto opcode_id = *reinterpret_cast(SELECT_VALUE(0xC4015E8_b, 0xB7B8968_b)); + const std::string error_str = gsc_error_msg.has_value() + ? utils::string::va(": %s", gsc_error_msg.value().data()) + : ""; + + if ((opcode_id >= 0x1A && opcode_id <= 0x20) || (opcode_id >= 0xA9 && opcode_id <= 0xAF)) + { + builtin_call_error(error_str); + } + else + { + const auto opcode = get_opcode_name(opcode_id); + if (opcode.has_value()) + { + console::warn("while processing instruction %s%s\n", opcode.value().data(), error_str.data()); + } + else + { + console::warn("while processing instruction 0x%X%s\n", opcode_id, error_str.data()); + } + } + + force_error_print = false; + gsc_error_msg = {}; + + print_callstack(); + console::warn("**********************************************\n"); + utils::hook::invoke(SELECT_VALUE(0x415C90_b, 0x59DDA0_b), mark_pos); + } + + void print(const function_args& args) + { + std::string buffer{}; + + for (auto i = 0u; i < args.size(); ++i) + { + const auto str = args[i].to_string(); + buffer.append(str); + buffer.append("\t"); + } + console::info("%s\n", buffer.data()); + } + + scripting::script_value typeof(const function_args& args) + { + return args[0].type_name(); + } + } + + namespace function + { + void add(const std::string& name, script_function function) + { + if (xsk::gsc::h1::resolver::find_function(name)) + { + const auto id = xsk::gsc::h1::resolver::function_id(name); + functions[id] = function; + } + else + { + const auto id = ++function_id_start; + xsk::gsc::h1::resolver::add_function(name, static_cast(id)); + functions[id] = function; + } + } + } + + namespace method + { + void add(const std::string& name, script_method method) + { + if (xsk::gsc::h1::resolver::find_method(name)) + { + const auto id = xsk::gsc::h1::resolver::method_id(name); + methods[id] = method; + } + else + { + const auto id = ++method_id_start; + xsk::gsc::h1::resolver::add_method(name, static_cast(id)); + methods[id] = method; + } + } + } + + function_args::function_args(std::vector values) + : values_(values) + { + } + + std::uint32_t function_args::size() const + { + return static_cast(this->values_.size()); + } + + std::vector function_args::get_raw() const + { + return this->values_; + } + + scripting::value_wrap function_args::get(const int index) const + { + if (index >= this->values_.size()) + { + throw std::runtime_error(utils::string::va("parameter %d does not exist", index)); + } + + return {this->values_[index], index}; + } + + class extension final : public component_interface + { + public: + void post_unpack() override + { + utils::hook::set(SELECT_VALUE(0x3BD86C_b, 0x50484C_b), 0x1000); // change builtin func count + + utils::hook::set(SELECT_VALUE(0x3BD872_b, 0x504852_b) + 4, + static_cast(reverse_b((&func_table)))); + utils::hook::set(SELECT_VALUE(0x3CB718_b, 0x512778_b) + 4, + static_cast(reverse_b((&func_table)))); + utils::hook::inject(SELECT_VALUE(0x3BDC28_b, 0x504C58_b) + 3, &func_table); + utils::hook::set(SELECT_VALUE(0x3BDC1E_b, 0x504C4E_b), sizeof(func_table)); + + utils::hook::set(SELECT_VALUE(0x3BD882_b, 0x504862_b) + 4, + static_cast(reverse_b((&meth_table)))); + utils::hook::set(SELECT_VALUE(0x3CBA3B_b, 0x512A9B_b) + 4, + static_cast(reverse_b(&meth_table))); + utils::hook::inject(SELECT_VALUE(0x3BDC36_b, 0x504C66_b) + 3, &meth_table); + utils::hook::set(SELECT_VALUE(0x3BDC3F_b, 0x504C6F_b), sizeof(meth_table)); + + developer_script = dvars::register_bool("developer_script", false, 0, "Enable developer script comments"); + + utils::hook::nop(SELECT_VALUE(0x3CB723_b, 0x512783_b), 8); + utils::hook::call(SELECT_VALUE(0x3CB723_b, 0x512783_b), vm_call_builtin_function_stub); + + utils::hook::call(SELECT_VALUE(0x3CBA12_b, 0x512A72_b), get_entity_id_stub); + utils::hook::nop(SELECT_VALUE(0x3CBA46_b, 0x512AA6_b), 6); + utils::hook::nop(SELECT_VALUE(0x3CBA4E_b, 0x512AAE_b), 2); + utils::hook::call(SELECT_VALUE(0x3CBA46_b, 0x512AA6_b), vm_call_builtin_method_stub); + + utils::hook::call(SELECT_VALUE(0x3CC9F3_b, 0x513A53_b), vm_error_stub); + + if (game::environment::is_dedi()) + { + function::add("isusingmatchrulesdata", [](const function_args& args) + { + // return 0 so the game doesn't override the cfg + return 0; + }); + } + + function::add("print", [](const function_args& args) + { + print(args); + return scripting::script_value{}; + }); + + function::add("println", [](const function_args& args) + { + print(args); + return scripting::script_value{}; + }); + + function::add("assert", [](const function_args& args) + { + const auto expr = args[0].as(); + if (!expr) + { + throw std::runtime_error("assert fail"); + } + + return scripting::script_value{}; + }); + + function::add("assertex", [](const function_args& args) + { + const auto expr = args[0].as(); + if (!expr) + { + const auto error = args[1].as(); + throw std::runtime_error(error); + } + + return scripting::script_value{}; + }); + + function::add("getfunction", [](const function_args& args) + { + const auto filename = args[0].as(); + const auto function = args[1].as(); + + if (!scripting::script_function_table[filename].contains(function)) + { + throw std::runtime_error("function not found"); + } + + return scripting::function{scripting::script_function_table[filename][function]}; + }); + + function::add("replacefunc", [](const function_args& args) + { + const auto what = args[0].get_raw(); + const auto with = args[1].get_raw(); + + if (what.type != game::VAR_FUNCTION || with.type != game::VAR_FUNCTION) + { + throw std::runtime_error("replaceFunc: parameter 1 must be a function"); + } + + logfile::set_gsc_hook(what.u.codePosValue, with.u.codePosValue); + + return scripting::script_value{}; + }); + + function::add("toupper", [](const function_args& args) + { + const auto string = args[0].as(); + return utils::string::to_upper(string); + }); + + function::add("logprint", [](const function_args& args) + { + std::string buffer{}; + + for (auto i = 0u; i < args.size(); ++i) + { + const auto string = args[i].as(); + buffer.append(string); + } + + game::G_LogPrintf("%s", buffer.data()); + + return scripting::script_value{}; + }); + + function::add("executecommand", [](const function_args& args) + { + const auto cmd = args[0].as(); + command::execute(cmd, true); + + return scripting::script_value{}; + }); + + function::add("typeof", typeof); + function::add("type", typeof); + + if (!game::environment::is_sp()) + { + function::add("say", [](const function_args& args) + { + const auto message = args[0].as(); + game::SV_GameSendServerCommand(-1, game::SV_CMD_CAN_IGNORE, utils::string::va("%c \"%s\"", 84, message.data())); + + return scripting::script_value{}; + }); + + method::add("tell", [](const game::scr_entref_t ent, const function_args& args) + { + if (ent.classnum != 0) + { + throw std::runtime_error("Invalid entity"); + } + + const auto client = ent.entnum; + + if (game::mp::g_entities[client].client == nullptr) + { + throw std::runtime_error("Not a player entity"); + } + + const auto message = args[0].as(); + game::SV_GameSendServerCommand(client, game::SV_CMD_CAN_IGNORE, utils::string::va("%c \"%s\"", 84, message.data())); + + return scripting::script_value{}; + }); + } + } + }; +} + +REGISTER_COMPONENT(gsc::extension) \ No newline at end of file diff --git a/src/client/component/gsc/script_extension.hpp b/src/client/component/gsc/script_extension.hpp new file mode 100644 index 00000000..2aae4a2e --- /dev/null +++ b/src/client/component/gsc/script_extension.hpp @@ -0,0 +1,46 @@ +#pragma once + +#include "game/scripting/array.hpp" +#include "game/scripting/execution.hpp" +#include "game/scripting/function.hpp" + +namespace gsc +{ + class function_args + { + public: + function_args(std::vector); + + unsigned int size() const; + std::vector get_raw() const; + scripting::value_wrap get(const int index) const; + + scripting::value_wrap operator[](const int index) const + { + return this->get(index); + } + private: + std::vector values_; + }; + + using builtin_function = void(*)(); + using builtin_method = void(*)(game::scr_entref_t); + + using script_function = std::function; + using script_method = std::function; + + extern builtin_function func_table[0x1000]; + extern builtin_method meth_table[0x1000]; + + extern const game::dvar_t* developer_script; + + namespace function + { + void add(const std::string& name, script_function function); + } + + namespace method + { + void add(const std::string& name, script_method function); + } +} \ No newline at end of file diff --git a/src/client/component/gsc/script_loading.cpp b/src/client/component/gsc/script_loading.cpp new file mode 100644 index 00000000..5d9c2cf5 --- /dev/null +++ b/src/client/component/gsc/script_loading.cpp @@ -0,0 +1,432 @@ +#include +#include "loader/component_loader.hpp" + +#include "component/console.hpp" +#include "component/fastfiles.hpp" +#include "component/filesystem.hpp" +#include "component/logfile.hpp" +#include "component/scripting.hpp" +#include "component/gsc/script_loading.hpp" + +#include "game/dvars.hpp" + +#include "game/scripting/array.hpp" +#include "game/scripting/execution.hpp" +#include "game/scripting/function.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +namespace gsc +{ + namespace + { + auto compiler = ::gsc::compiler(); + auto decompiler = ::gsc::decompiler(); + auto assembler = ::gsc::assembler(); + auto disassembler = ::gsc::disassembler(); + + std::unordered_map main_handles; + std::unordered_map init_handles; + + utils::memory::allocator scriptfile_allocator; + std::unordered_map loaded_scripts; + + struct + { + char* buf = nullptr; + char* pos = nullptr; + unsigned int size = 0x1000000; + } script_memory; + + char* allocate_buffer(size_t size) + { + if (script_memory.buf == nullptr) + { + script_memory.buf = game::PMem_AllocFromSource_NoDebug(script_memory.size, 4, 1, game::PMEM_SOURCE_SCRIPT); + script_memory.pos = script_memory.buf; + } + + if (script_memory.pos + size > script_memory.buf + script_memory.size) + { + game::Com_Error(game::ERR_FATAL, "Out of custom script memory"); + } + + const auto pos = script_memory.pos; + script_memory.pos += size; + return pos; + } + + void free_script_memory() + { + game::PMem_PopFromSource_NoDebug(script_memory.buf, script_memory.size, 4, 1, game::PMEM_SOURCE_SCRIPT); + script_memory.buf = nullptr; + script_memory.pos = nullptr; + } + + void clear() + { + main_handles.clear(); + init_handles.clear(); + loaded_scripts.clear(); + scriptfile_allocator.clear(); + free_script_memory(); + } + + bool read_script_file(const std::string& name, std::string* data) + { + if (filesystem::read_file(name, data)) + { + return true; + } + + const auto name_str = name.data(); + + if (game::DB_XAssetExists(game::ASSET_TYPE_RAWFILE, name_str) && + !game::DB_IsXAssetDefault(game::ASSET_TYPE_RAWFILE, name_str)) + { + const auto asset = game::DB_FindXAssetHeader(game::ASSET_TYPE_RAWFILE, name_str, false); + const auto len = game::DB_GetRawFileLen(asset.rawfile); + data->resize(len); + game::DB_GetRawBuffer(asset.rawfile, data->data(), len); + if (len > 0) + { + data->pop_back(); + } + + return true; + } + + return false; + } + + game::ScriptFile* load_custom_script(const char* file_name, const std::string& real_name) + { + if (game::VirtualLobby_Loaded()) + { + return nullptr; + } + + if (const auto itr = loaded_scripts.find(real_name); itr != loaded_scripts.end()) + { + return itr->second; + } + + std::string source_buffer; + if (!read_script_file(real_name + ".gsc", &source_buffer) || source_buffer.empty()) + { + return nullptr; + } + + if (game::DB_XAssetExists(game::ASSET_TYPE_SCRIPTFILE, file_name) && + !game::DB_IsXAssetDefault(game::ASSET_TYPE_SCRIPTFILE, file_name)) + { + // filter out gsc rawfiles that contain developer code (has ScriptFile counterparts for ship, won't compile either) + if ((real_name.starts_with("maps/createfx") || real_name.starts_with("maps/createart") || real_name.starts_with("maps/mp")) + && (real_name.ends_with("_fx") || real_name.ends_with("_fog") || real_name.ends_with("_hdr"))) + { + return game::DB_FindXAssetHeader(game::ASSET_TYPE_SCRIPTFILE, file_name, false).scriptfile; + } + } + + std::vector data; + data.assign(source_buffer.begin(), source_buffer.end()); + + try + { + compiler->compile(real_name, data); + } + catch (const std::exception& e) + { + console::error("*********** script compile error *************\n"); + console::error("failed to compile '%s':\n%s", real_name.data(), e.what()); + console::error("**********************************************\n"); + return nullptr; + } + + auto assembly = compiler->output(); + + try + { + assembler->assemble(real_name, assembly); + } + catch (const std::exception& e) + { + console::error("*********** script compile error *************\n"); + console::error("failed to assemble '%s':\n%s", real_name.data(), e.what()); + console::error("**********************************************\n"); + return nullptr; + } + + const auto script_file_ptr = scriptfile_allocator.allocate(); + script_file_ptr->name = file_name; + + const auto stack = assembler->output_stack(); + script_file_ptr->len = static_cast(stack.size()); + + const auto script = assembler->output_script(); + script_file_ptr->bytecodeLen = static_cast(script.size()); + + script_file_ptr->buffer = game::Hunk_AllocateTempMemoryHigh(stack.size() + 1); + std::memcpy(script_file_ptr->buffer, stack.data(), stack.size()); + + script_file_ptr->bytecode = allocate_buffer(script.size() + 1); + std::memcpy(script_file_ptr->bytecode, script.data(), script.size()); + + script_file_ptr->compressedLen = 0; + + loaded_scripts[real_name] = script_file_ptr; + + return script_file_ptr; + } + + std::string get_script_file_name(const std::string& name) + { + const auto id = xsk::gsc::h1::resolver::token_id(name); + if (!id) + { + return name; + } + + return std::to_string(id); + } + + std::vector decompile_script_file(const std::string& name, const std::string& real_name) + { + const auto* script_file = game::DB_FindXAssetHeader(game::ASSET_TYPE_SCRIPTFILE, name.data(), false).scriptfile; + if (!script_file) + { + throw std::runtime_error(std::format("Could not load scriptfile '{}'", real_name)); + } + + console::info("Decompiling scriptfile '%s'\n", real_name.data()); + + std::vector stack{script_file->buffer, script_file->buffer + script_file->len}; + std::vector bytecode{script_file->bytecode, script_file->bytecode + script_file->bytecodeLen}; + + auto decompressed_stack = xsk::utils::zlib::decompress(stack, static_cast(stack.size())); + + disassembler->disassemble(name, bytecode, decompressed_stack); + auto output = disassembler->output(); + + decompiler->decompile(name, output); + + return decompiler->output(); + } + + void load_script(const std::string& name) + { + if (!game::Scr_LoadScript(name.data())) + { + return; + } + + const auto main_handle = game::Scr_GetFunctionHandle(name.data(), xsk::gsc::h1::resolver::token_id("main")); + const auto init_handle = game::Scr_GetFunctionHandle(name.data(), xsk::gsc::h1::resolver::token_id("init")); + + if (main_handle) + { + console::info("Loaded '%s::main'\n", name.data()); + main_handles[name] = main_handle; + } + + if (init_handle) + { + console::info("Loaded '%s::init'\n", name.data()); + init_handles[name] = init_handle; + } + } + + void load_scripts(const std::filesystem::path& root_dir, const std::filesystem::path& script_dir) + { + std::filesystem::path script_dir_path = root_dir / script_dir; + if (!utils::io::directory_exists(script_dir_path.generic_string())) + { + return; + } + + const auto scripts = utils::io::list_files(script_dir_path.generic_string()); + for (const auto& script : scripts) + { + if (!script.ends_with(".gsc")) + { + continue; + } + + std::filesystem::path path(script); + const auto relative = path.lexically_relative(root_dir).generic_string(); + const auto base_name = relative.substr(0, relative.size() - 4); + + load_script(base_name); + } + } + + int db_is_x_asset_default(game::XAssetType type, const char* name) + { + if (loaded_scripts.contains(name)) + { + return 0; + } + + return game::DB_IsXAssetDefault(type, name); + } + + void gscr_load_gametype_script_stub(void* a1, void* a2) + { + utils::hook::invoke(SELECT_VALUE(0x2B9DA0_b, 0x18BC00_b), a1, a2); + + if (game::VirtualLobby_Loaded()) + { + return; + } + + for (const auto& path : filesystem::get_search_paths()) + { + load_scripts(path, "scripts/"); + if (game::environment::is_sp()) + { + load_scripts(path, "scripts/sp/"); + } + else + { + load_scripts(path, "scripts/mp/"); + } + } + } + + void db_get_raw_buffer_stub(const game::RawFile* rawfile, char* buf, const int size) + { + if (rawfile->len > 0 && rawfile->compressedLen == 0) + { + std::memset(buf, 0, size); + std::memcpy(buf, rawfile->buffer, std::min(rawfile->len, size)); + return; + } + + utils::hook::invoke(SELECT_VALUE(0x1F1E00_b, 0x396080_b), rawfile, buf, size); + } + + void pmem_init_stub() + { + utils::hook::invoke(SELECT_VALUE(0x420260_b, 0x5A5590_b)); + + const auto type_0 = &game::g_scriptmem[0]; + const auto type_1 = &game::g_scriptmem[1]; + + const auto size_0 = 0x100000; // default size + const auto size_1 = 0x100000 + script_memory.size; + + const auto block = reinterpret_cast(VirtualAlloc(NULL, size_0 + size_1, MEM_RESERVE, PAGE_READWRITE)); + + type_0->buf = block; + type_0->size = size_0; + + type_1->buf = block + size_0; + type_1->size = size_1; + + utils::hook::set(SELECT_VALUE(0x420252_b, 0x5A5582_b), size_0 + size_1); + } + } + + void load_main_handles() + { + for (auto& function_handle : main_handles) + { + console::info("Executing '%s::main'\n", function_handle.first.data()); + game::RemoveRefToObject(game::Scr_ExecThread(function_handle.second, 0)); + } + } + + void load_init_handles() + { + for (auto& function_handle : init_handles) + { + console::info("Executing '%s::init'\n", function_handle.first.data()); + game::RemoveRefToObject(game::Scr_ExecThread(function_handle.second, 0)); + } + } + + game::ScriptFile* find_script(game::XAssetType type, const char* name, int allow_create_default) + { + std::string real_name = name; + const auto id = static_cast(std::atoi(name)); + if (id) + { + real_name = xsk::gsc::h1::resolver::token_name(id); + } + + auto* script = load_custom_script(name, real_name); + if (script) + { + return script; + } + + return game::DB_FindXAssetHeader(type, name, allow_create_default).scriptfile; + } + + class loading final : public component_interface + { + public: + void post_unpack() override + { + // allow custom scripts to include other custom scripts + xsk::gsc::h1::resolver::init([](const auto& include_name) + { + const auto real_name = include_name + ".gsc"; + + std::string file_buffer; + if (!read_script_file(real_name, &file_buffer) || file_buffer.empty()) + { + const auto name = get_script_file_name(include_name); + if (game::DB_XAssetExists(game::ASSET_TYPE_SCRIPTFILE, name.data())) + { + return decompile_script_file(name, real_name); + } + else + { + throw std::runtime_error(std::format("Could not load gsc file '{}'", real_name)); + } + } + + std::vector result; + result.assign(file_buffer.begin(), file_buffer.end()); + + return result; + }); + + // hook xasset functions to return our own custom scripts + utils::hook::call(SELECT_VALUE(0x3C7217_b, 0x50E357_b), find_script); + utils::hook::call(SELECT_VALUE(0x3C7227_b, 0x50E367_b), db_is_x_asset_default); + + // GScr_LoadScripts + utils::hook::call(SELECT_VALUE(0x2BA152_b, 0x18C325_b), gscr_load_gametype_script_stub); + + // loads scripts with an uncompressed stack + utils::hook::call(SELECT_VALUE(0x3C7280_b, 0x50E3C0_b), db_get_raw_buffer_stub); + + // Increase script memory + utils::hook::call(SELECT_VALUE(0x38639C_b, 0x15C4D6_b), pmem_init_stub); + + scripting::on_shutdown([](bool free_scripts, bool post_shutdown) + { + if (free_scripts && post_shutdown) + { + xsk::gsc::h1::resolver::cleanup(); + clear(); + } + }); + } + }; +} + +REGISTER_COMPONENT(gsc::loading) \ No newline at end of file diff --git a/src/client/component/gsc/script_loading.hpp b/src/client/component/gsc/script_loading.hpp new file mode 100644 index 00000000..e42a7e45 --- /dev/null +++ b/src/client/component/gsc/script_loading.hpp @@ -0,0 +1,8 @@ +#pragma once + +namespace gsc +{ + void load_main_handles(); + void load_init_handles(); + game::ScriptFile* find_script(game::XAssetType type, const char* name, int allow_create_default); +} diff --git a/src/client/component/io.cpp b/src/client/component/io.cpp new file mode 100644 index 00000000..64373558 --- /dev/null +++ b/src/client/component/io.cpp @@ -0,0 +1,173 @@ +#include +#include "loader/component_loader.hpp" + +#include "component/console.hpp" +#include "component/logfile.hpp" +#include "component/scheduler.hpp" +#include "component/gsc/script_extension.hpp" + +#include "game/dvars.hpp" +#include "game/game.hpp" + +#include "game/scripting/execution.hpp" + +#include +#include +#include +#include + +namespace io +{ + namespace + { + bool allow_root_io = false; + + void check_path(const std::filesystem::path& path) + { + if (path.generic_string().find("..") != std::string::npos) + { + throw std::runtime_error("directory traversal is not allowed"); + } + } + + std::string convert_path(const std::filesystem::path& path) + { + check_path(path); + + if (allow_root_io) + { + static const auto fs_base_game = game::Dvar_FindVar("fs_basepath"); + const std::filesystem::path fs_base_game_path(fs_base_game->current.string); + return (fs_base_game_path / path).generic_string(); + } + + static const auto fs_game = game::Dvar_FindVar("fs_game"); + if (fs_game->current.string && fs_game->current.string != ""s) + { + const std::filesystem::path fs_game_path(fs_game->current.string); + return (fs_game_path / path).generic_string(); + } + + throw std::runtime_error("fs_game is not properly defined"); + } + + void replace(std::string& str, const std::string& from, const std::string& to) + { + const auto start_pos = str.find(from); + + if (start_pos == std::string::npos) + { + return; + } + + str.replace(start_pos, from.length(), to); + } + } + + class component final : public component_interface + { + public: + void post_unpack() override + { + allow_root_io = utils::flags::has_flag("allow_root_io"); + if (allow_root_io) + { + console::warn("GSC has access to your game folder. Remove the '-allow_root_io' launch parameter to disable this feature."); + } + + gsc::function::add("fileexists", [](const gsc::function_args& args) + { + const auto path = convert_path(args[0].as()); + return utils::io::file_exists(path); + }); + + gsc::function::add("writefile", [](const gsc::function_args& args) + { + const auto path = convert_path(args[0].as()); + const auto data = args[1].as(); + + auto append = false; + if (args.size() > 2u) + { + append = args[2].as(); + } + + return utils::io::write_file(path, data, append); + }); + + gsc::function::add("readfile", [](const gsc::function_args& args) + { + const auto path = convert_path(args[0].as()); + return utils::io::read_file(path); + }); + + gsc::function::add("filesize", [](const gsc::function_args& args) + { + const auto path = convert_path(args[0].as()); + return static_cast(utils::io::file_size(path)); + }); + + gsc::function::add("createdirectory", [](const gsc::function_args& args) + { + const auto path = convert_path(args[0].as()); + return utils::io::create_directory(path); + }); + + gsc::function::add("directoryexists", [](const gsc::function_args& args) + { + const auto path = convert_path(args[0].as()); + return utils::io::directory_exists(path); + }); + + gsc::function::add("directoryisempty", [](const gsc::function_args& args) + { + const auto path = convert_path(args[0].as()); + return utils::io::directory_is_empty(path); + }); + + gsc::function::add("listfiles", [](const gsc::function_args& args) + { + const auto path = convert_path(args[0].as()); + const auto files = utils::io::list_files(path); + + scripting::array array{}; + for (const auto& file : files) + { + array.push(file); + } + + return array; + }); + + gsc::function::add("copyfolder", [](const gsc::function_args& args) + { + const auto source = convert_path(args[0].as()); + const auto target = convert_path(args[1].as()); + utils::io::copy_folder(source, target); + + return scripting::script_value{}; + }); + + gsc::function::add("removefile", [](const gsc::function_args& args) + { + const auto path = convert_path(args[0].as()); + return utils::io::remove_file(path); + }); + + gsc::function::add("va", [](const gsc::function_args& args) + { + auto fmt = args[0].as(); + + for (auto i = 1u; i < args.size(); i++) + { + const auto arg = args[i].to_string(); + replace(fmt, "%s", arg); + } + + return fmt; + }); + } + }; +} + +REGISTER_COMPONENT(io::component) diff --git a/src/client/component/json.cpp b/src/client/component/json.cpp new file mode 100644 index 00000000..7144652b --- /dev/null +++ b/src/client/component/json.cpp @@ -0,0 +1,213 @@ +#include +#include "loader/component_loader.hpp" + +#include "component/scheduler.hpp" +#include "component/gsc/script_extension.hpp" + +#include "game/scripting/array.hpp" +#include "game/scripting/execution.hpp" +#include "game/scripting/function.hpp" + +#include + +namespace json +{ + namespace + { + nlohmann::json gsc_to_json(scripting::script_value _value); + + nlohmann::json entity_to_array(unsigned int id) + { + scripting::array array(id); + nlohmann::json obj; + + auto string_indexed = -1; + const auto keys = array.get_keys(); + for (auto i = 0; i < keys.size(); i++) + { + const auto is_int = keys[i].is(); + const auto is_string = keys[i].is(); + + if (string_indexed == -1) + { + string_indexed = is_string; + } + + if (!string_indexed && is_int) + { + const auto index = keys[i].as(); + obj[index] = gsc_to_json(array[index]); + } + else if (string_indexed && is_string) + { + const auto key = keys[i].as(); + obj.emplace(key, gsc_to_json(array[key])); + } + } + + return obj; + } + + nlohmann::json vector_to_array(const float* value) + { + nlohmann::json obj; + obj.push_back(value[0]); + obj.push_back(value[1]); + obj.push_back(value[2]); + + return obj; + } + + nlohmann::json gsc_to_json(scripting::script_value _value) + { + const auto variable = _value.get_raw(); + const auto value = variable.u; + const auto type = variable.type; + + switch (type) + { + case (game::VAR_UNDEFINED): + return {}; + case (game::VAR_INTEGER): + return value.intValue; + case (game::VAR_FLOAT): + return value.floatValue; + case (game::VAR_STRING): + case (game::VAR_ISTRING): + return game::SL_ConvertToString(static_cast(value.stringValue)); + case (game::VAR_VECTOR): + return vector_to_array(value.vectorValue); + case (game::VAR_POINTER): + { + const auto object_type = game::scr_VarGlob->objectVariableValue[value.uintValue].w.type; + + switch (object_type) + { + case (game::VAR_OBJECT): + return "[struct]"; + case (game::VAR_ARRAY): + return entity_to_array(value.uintValue); + default: + return "[entity]"; + } + } + case (game::VAR_FUNCTION): + return _value.as().get_name(); + default: + return utils::string::va("[%s]", _value.type_name().data()); + }; + } + + scripting::script_value json_to_gsc(nlohmann::json obj) + { + const auto type = obj.type(); + + switch (type) + { + case (nlohmann::detail::value_t::number_integer): + case (nlohmann::detail::value_t::number_unsigned): + return obj.get(); + case (nlohmann::detail::value_t::number_float): + return obj.get(); + case (nlohmann::detail::value_t::string): + return obj.get(); + case (nlohmann::detail::value_t::array): + { + scripting::array array; + + for (const auto& [key, value] : obj.items()) + { + array.push(json_to_gsc(value)); + } + + return array.get_raw(); + } + case (nlohmann::detail::value_t::object): + { + scripting::array array; + + for (const auto& [key, value] : obj.items()) + { + array[key] = json_to_gsc(value); + } + + return array.get_raw(); + } + } + + return {}; + } + } + + std::string gsc_to_string(const scripting::script_value& value) + { + return gsc_to_json(value).dump(); + } + + class component final : public component_interface + { + public: + void post_unpack() override + { + gsc::function::add("array", [](const gsc::function_args& args) + { + scripting::array array(args.get_raw()); + return array.get_raw(); + }); + + gsc::function::add("map", [](const gsc::function_args& args) + { + scripting::array array; + + for (auto i = 0u; i < args.size(); i += 2) + { + if (i >= args.size() - 1) + { + continue; + } + + const auto key = args[i].as(); + array[key] = args[i + 1]; + } + + return array; + }); + + gsc::function::add("jsonparse", [](const gsc::function_args& args) + { + const auto json = args[0].as(); + const auto obj = nlohmann::json::parse(json); + return json_to_gsc(obj); + }); + + gsc::function::add("jsonserialize", [](const gsc::function_args& args) + { + const auto value = args[0]; + auto indent = -1; + + if (args.size() > 1) + { + indent = args[1].as(); + } + + return gsc_to_json(value).dump(indent); + }); + + gsc::function::add("jsonprint", [](const gsc::function_args& args) -> scripting::script_value + { + std::string buffer; + + for (const auto arg : args.get_raw()) + { + buffer.append(gsc_to_string(arg)); + buffer.append("\t"); + } + + printf("%s\n", buffer.data()); + return {}; + }); + } + }; +} + +REGISTER_COMPONENT(json::component) diff --git a/src/client/component/localized_strings.cpp b/src/client/component/localized_strings.cpp index 1b72bea9..231a765c 100644 --- a/src/client/component/localized_strings.cpp +++ b/src/client/component/localized_strings.cpp @@ -1,10 +1,16 @@ #include #include "loader/component_loader.hpp" + +#include "console.hpp" +#include "filesystem.hpp" #include "localized_strings.hpp" + +#include "game/game.hpp" + #include #include #include -#include "game/game.hpp" +#include namespace localized_strings { diff --git a/src/client/component/logfile.cpp b/src/client/component/logfile.cpp index 64600ffe..ef1efb2e 100644 --- a/src/client/component/logfile.cpp +++ b/src/client/component/logfile.cpp @@ -1,29 +1,48 @@ #include #include "loader/component_loader.hpp" -#include "scheduler.hpp" -#include "logfile.hpp" +#include "component/logfile.hpp" +#include "component/scripting.hpp" +#include "component/scheduler.hpp" +#include "component/gsc/script_extension.hpp" + +#include "game/dvars.hpp" #include +#include namespace logfile { - std::unordered_map vm_execute_hooks; + bool hook_enabled = true; namespace { + struct gsc_hook_t + { + bool is_lua_hook{}; + const char* target_pos{}; + sol::protected_function lua_function; + }; + + std::unordered_map vm_execute_hooks; utils::hook::detour scr_player_killed_hook; utils::hook::detour scr_player_damage_hook; utils::hook::detour client_command_hook; - utils::hook::detour g_shutdown_game_hook; + + utils::hook::detour g_log_printf_hook; std::vector player_killed_callbacks; std::vector player_damage_callbacks; + std::vector say_callbacks; + + game::dvar_t* logfile; + game::dvar_t* g_log; + utils::hook::detour vm_execute_hook; char empty_function[2] = {0x32, 0x34}; // CHECK_CLEAR_PARAMS, END - bool hook_enabled = true; + const char* target_function = nullptr; sol::lua_value convert_entity(lua_State* state, const game::mp::gentity_s* ent) { @@ -166,21 +185,30 @@ namespace logfile } const auto& hook = vm_execute_hooks[pos]; - const auto state = hook.lua_state(); - - const scripting::entity self = local_id_to_entity(game::scr_VmPub->function_frame->fs.localId); - - std::vector args; - - const auto top = game::scr_function_stack->top; - - for (auto* value = top; value->type != game::SCRIPT_END; --value) + if (hook.is_lua_hook) { - args.push_back(scripting::lua::convert(state, *value)); - } + const auto& function = hook.lua_function; + const auto state = function.lua_state(); - const auto result = hook(self, sol::as_args(args)); - scripting::lua::handle_error(result); + const scripting::entity self = local_id_to_entity(game::scr_VmPub->function_frame->fs.localId); + + std::vector args; + + const auto top = game::scr_function_stack->top; + + for (auto* value = top; value->type != game::VAR_PRECODEPOS; --value) + { + args.push_back(scripting::lua::convert(state, *value)); + } + + const auto result = function(self, sol::as_args(args)); + scripting::lua::handle_error(result); + target_function = empty_function; + } + else + { + target_function = hook.target_pos; + } return true; } @@ -212,9 +240,35 @@ namespace logfile a.bind(replace); a.popad64(); - a.mov(r14, reinterpret_cast(empty_function)); + a.mov(rax, qword_ptr(reinterpret_cast(&target_function))); + a.mov(r14, rax); a.jmp(end); } + + void g_log_printf_stub(const char* fmt, ...) + { + if (!logfile->current.enabled) + { + return; + } + + char va_buffer[0x400] = {0}; + + va_list ap; + va_start(ap, fmt); + vsprintf_s(va_buffer, fmt, ap); + va_end(ap); + + const auto file = g_log->current.string; + const auto time = *game::level_time / 1000; + + utils::io::write_file(file, utils::string::va("%3i:%i%i %s", + time / 60, + time % 60 / 10, + time % 60 % 10, + va_buffer + ), true); + } } void add_player_damage_callback(const sol::protected_function& callback) @@ -251,21 +305,37 @@ namespace logfile game::SV_Cmd_ArgvBuffer(0, cmd, 1024); + auto hidden = false; if (cmd == "say"s || cmd == "say_team"s) { - auto hidden = false; std::string message(game::ConcatArgs(1)); + message.erase(0, 1); - hidden = message[1] == '/'; - message.erase(0, hidden ? 2 : 1); + for (const auto& callback : say_callbacks) + { + const auto entity_id = game::Scr_GetEntityId(client_num, 0); + const auto result = callback(entity_id, {message, cmd == "say_team"s}); + + if (result.is() && !hidden) + { + hidden = result.as() == 0; + } + } scheduler::once([cmd, message, self, hidden]() { const scripting::entity level{*game::levelEntityId}; const scripting::entity player{game::Scr_GetEntityId(self->s.number, 0)}; - scripting::notify(level, cmd, {player, message, hidden}); - scripting::notify(player, cmd, {message, hidden}); + notify(level, cmd, {player, message, hidden}); + notify(player, cmd, {message, hidden}); + + game::G_LogPrintf("%s;%s;%i;%s;%s\n", + cmd, + player.call("getguid").as(), + player.call("getentitynumber").as(), + player.get("name").as(), + message.data()); }, scheduler::pipeline::server); if (hidden) @@ -277,6 +347,32 @@ namespace logfile return true; } + void set_lua_hook(const char* pos, const sol::protected_function& callback) + { + gsc_hook_t hook; + hook.is_lua_hook = true; + hook.lua_function = callback; + vm_execute_hooks[pos] = hook; + } + + void set_gsc_hook(const char* source, const char* target) + { + gsc_hook_t hook; + hook.is_lua_hook = false; + hook.target_pos = target; + vm_execute_hooks[source] = hook; + } + + void clear_hook(const char* pos) + { + vm_execute_hooks.erase(pos); + } + + size_t get_hook_count() + { + return vm_execute_hooks.size(); + } + class component final : public component_interface { public: @@ -291,6 +387,29 @@ namespace logfile scr_player_damage_hook.create(0x1CE780_b, scr_player_damage_stub); scr_player_killed_hook.create(0x1CEA60_b, scr_player_killed_stub); + + // Reimplement game log + scheduler::once([]() + { + logfile = dvars::register_bool("logfile", true, game::DVAR_FLAG_NONE, "Enable game logging"); + g_log = dvars::register_string("g_log", "h1-mod\\logs\\games_mp.log", game::DVAR_FLAG_NONE, "Log file path"); + }, scheduler::pipeline::main); + g_log_printf_hook.create(game::G_LogPrintf, g_log_printf_stub); + + gsc::function::add("onplayersay", [](const gsc::function_args& args) + { + const auto function = args[0].as(); + say_callbacks.push_back(function); + return scripting::script_value{}; + }); + + scripting::on_shutdown([](bool /*free_scripts*/, bool post_shutdown) + { + if (!post_shutdown) + { + say_callbacks.clear(); + } + }); } }; } diff --git a/src/client/component/logfile.hpp b/src/client/component/logfile.hpp index 4ab67966..6618fb05 100644 --- a/src/client/component/logfile.hpp +++ b/src/client/component/logfile.hpp @@ -7,7 +7,12 @@ namespace logfile { - extern std::unordered_map vm_execute_hooks; + extern bool hook_enabled; + + void set_lua_hook(const char* pos, const sol::protected_function&); + void set_gsc_hook(const char* source, const char* target); + void clear_hook(const char* pos); + size_t get_hook_count(); void add_player_damage_callback(const sol::protected_function& callback); void add_player_killed_callback(const sol::protected_function& callback); diff --git a/src/client/component/logger.cpp b/src/client/component/logger.cpp index 776313e1..80c59025 100644 --- a/src/client/component/logger.cpp +++ b/src/client/component/logger.cpp @@ -1,12 +1,11 @@ #include #include "loader/component_loader.hpp" +#include "game/game.hpp" +#include "game/dvars.hpp" #include "party.hpp" #include "console.hpp" -#include "game/game.hpp" -#include "game/dvars.hpp" - #include namespace logger @@ -15,48 +14,42 @@ namespace logger { utils::hook::detour com_error_hook; + game::dvar_t* logger_dev = nullptr; + void print_error(const char* msg, ...) { - char buffer[2048]; - + char buffer[2048]{}; va_list ap; + va_start(ap, msg); - - vsnprintf_s(buffer, sizeof(buffer), _TRUNCATE, msg, ap); - + vsnprintf_s(buffer, _TRUNCATE, msg, ap); va_end(ap); - console::error(buffer); + console::error("%s", buffer); } void print_com_error(int, const char* msg, ...) { - char buffer[2048]; - + char buffer[2048]{}; va_list ap; + va_start(ap, msg); - - vsnprintf_s(buffer, sizeof(buffer), _TRUNCATE, msg, ap); - + vsnprintf_s(buffer, _TRUNCATE, msg, ap); va_end(ap); - console::error(buffer); + console::error("%s", buffer); } void com_error_stub(const int error, const char* msg, ...) { - char buffer[2048]; + char buffer[2048]{}; + va_list ap; - { - va_list ap; - va_start(ap, msg); + va_start(ap, msg); + vsnprintf_s(buffer, _TRUNCATE, msg, ap); + va_end(ap); - vsnprintf_s(buffer, sizeof(buffer), _TRUNCATE, msg, ap); - - va_end(ap); - - console::error("Error: %s\n", buffer); - } + console::error("Error: %s\n", buffer); party::clear_sv_motd(); // clear sv_motd on error if it exists @@ -65,50 +58,43 @@ namespace logger void print_warning(const char* msg, ...) { - char buffer[2048]; - + char buffer[2048]{}; va_list ap; + va_start(ap, msg); - - vsnprintf_s(buffer, sizeof(buffer), _TRUNCATE, msg, ap); - + vsnprintf_s(buffer, _TRUNCATE, msg, ap); va_end(ap); - console::warn(buffer); + console::warn("%s", buffer); } void print(const char* msg, ...) { - char buffer[2048]; - + char buffer[2048]{}; va_list ap; + va_start(ap, msg); - - vsnprintf_s(buffer, sizeof(buffer), _TRUNCATE, msg, ap); - + vsnprintf_s(buffer, _TRUNCATE, msg, ap); va_end(ap); - console::info(buffer); + console::info("%s", buffer); } void print_dev(const char* msg, ...) { - static auto* enabled = dvars::register_bool("logger_dev", false, game::DVAR_FLAG_SAVED, "Print dev stuff"); - if (!enabled->current.enabled) + if (!logger_dev->current.enabled) { return; } - char buffer[2048]; - + char buffer[2048]{}; va_list ap; + va_start(ap, msg); - - vsnprintf_s(buffer, sizeof(buffer), _TRUNCATE, msg, ap); - + vsnprintf_s(buffer, _TRUNCATE, msg, ap); va_end(ap); - console::info(buffer); + console::info("%s", buffer); } } @@ -126,6 +112,8 @@ namespace logger } com_error_hook.create(game::Com_Error, com_error_stub); + + logger_dev = dvars::register_bool("logger_dev", false, game::DVAR_FLAG_SAVED, "Print dev stuff"); } }; } diff --git a/src/client/component/lui.cpp b/src/client/component/lui.cpp index c99766c1..4fd77f6d 100644 --- a/src/client/component/lui.cpp +++ b/src/client/component/lui.cpp @@ -95,6 +95,17 @@ namespace lui game::LUI_OpenMenu(0, params[1], 0, 0, 0); }); + command::add("lui_close", [](const command::params& params) + { + if (params.size() <= 1) + { + console::info("usage: lui_close \n"); + return; + } + + game::LUI_LeaveMenuByName(0, params[1], 0, *game::hks::lua_state); + }); + command::add("lui_open_popup", [](const command::params& params) { if (params.size() <= 1) diff --git a/src/client/component/map_patches.cpp b/src/client/component/map_patches.cpp new file mode 100644 index 00000000..7c59b80c --- /dev/null +++ b/src/client/component/map_patches.cpp @@ -0,0 +1,27 @@ +#include +#include "loader/component_loader.hpp" + +#include "game/game.hpp" + +#include + +namespace map_patches +{ + class component final : public component_interface + { + public: + void post_unpack() override + { + if (game::environment::is_sp()) + { + return; + } + + // skip fx name prefix checks + utils::hook::set(0x2F377D_b, 0xEB); // createfx parse + utils::hook::set(0x4444E0_b, 0xEB); // scr_loadfx + } + }; +} + +REGISTER_COMPONENT(map_patches::component) \ No newline at end of file diff --git a/src/client/component/map_rotation.cpp b/src/client/component/map_rotation.cpp index 6f749304..7b80bebf 100644 --- a/src/client/component/map_rotation.cpp +++ b/src/client/component/map_rotation.cpp @@ -1,11 +1,15 @@ #include #include "loader/component_loader.hpp" + #include "command.hpp" +#include "console.hpp" #include "scheduler.hpp" + +#include "game/game.hpp" +#include "game/dvars.hpp" + #include #include -#include "game/game.hpp" -#include namespace map_rotation { @@ -82,14 +86,12 @@ namespace map_rotation auto* const dvar = game::Dvar_FindVar("sv_autoPriority"); if (dvar && dvar->current.enabled) { - scheduler::on_game_initialized([]() + scheduler::on_game_initialized([] { - //printf("=======================setting OLD priority=======================\n"); SetPriorityClass(GetCurrentProcess(), previous_priority); }, scheduler::pipeline::main, 1s); previous_priority = GetPriorityClass(GetCurrentProcess()); - //printf("=======================setting NEW priority=======================\n"); SetPriorityClass(GetCurrentProcess(), NORMAL_PRIORITY_CLASS); } } @@ -119,7 +121,7 @@ namespace map_rotation change_process_priority(); if (!game::SV_MapExists(value.data())) { - printf("map_rotation: '%s' map doesn't exist!\n", value.data()); + console::info("map_rotation: '%s' map doesn't exist!\n", value.data()); launch_default_map(); return; } @@ -128,7 +130,7 @@ namespace map_rotation } else { - printf("Invalid map rotation key: %s\n", key.data()); + console::info("Invalid map rotation key: %s\n", key.data()); } } @@ -137,7 +139,7 @@ namespace map_rotation void trigger_map_rotation() { - scheduler::schedule([]() + scheduler::schedule([] { if (game::CL_IsCgameInitialized()) { @@ -160,7 +162,7 @@ namespace map_rotation return; } - scheduler::once([]() + scheduler::once([] { dvars::register_string("sv_mapRotation", "", game::DVAR_FLAG_NONE, ""); dvars::register_string("sv_mapRotationCurrent", "", game::DVAR_FLAG_NONE, ""); diff --git a/src/client/component/mapents.cpp b/src/client/component/mapents.cpp new file mode 100644 index 00000000..eee4297f --- /dev/null +++ b/src/client/component/mapents.cpp @@ -0,0 +1,183 @@ +#include +#include "loader/component_loader.hpp" + +#include "game/game.hpp" +#include "console.hpp" + +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace mapents +{ + namespace + { + std::optional parse_mapents(const std::string& source) + { + std::string out_buffer{}; + + const auto lines = utils::string::split(source, '\n'); + auto in_map_ent = false; + auto empty = false; + auto in_comment = false; + + for (auto i = 0; i < lines.size(); i++) + { + auto line = lines[i]; + if (line.ends_with('\r')) + { + line.pop_back(); + } + + if (line.starts_with("/*")) + { + in_comment = true; + continue; + } + + if (line.ends_with("*/")) + { + in_comment = false; + continue; + } + + if (in_comment) + { + continue; + } + + if (line.starts_with("//")) + { + continue; + } + + if (line[0] == '{' && !in_map_ent) + { + in_map_ent = true; + out_buffer.append("{\n"); + continue; + } + + if (line[0] == '{' && in_map_ent) + { + console::error("[map_ents parser] Unexpected '{' on line %i\n", i); + return {}; + } + + if (line[0] == '}' && in_map_ent) + { + if (empty) + { + out_buffer.append("\n}\n"); + } + else if (i < static_cast(lines.size()) - 1) + { + out_buffer.append("}\n"); + } + else + { + out_buffer.append("}\0"); + } + + in_map_ent = false; + continue; + } + + if (line[0] == '}' && !in_map_ent) + { + console::error("[map_ents parser] Unexpected '}' on line %i\n", i); + return {}; + } + + std::regex expr(R"~((.+) "(.*)")~"); + std::smatch match{}; + if (!std::regex_search(line, match, expr)) + { + console::warn("[map_ents parser] Failed to parse line %i (%s)\n", i, line.data()); + continue; + } + + auto key = utils::string::to_lower(match[1].str()); + const auto value = match[2].str(); + + if (key.size() <= 0) + { + console::warn("[map_ents parser] Invalid key ('%s') on line %i (%s)\n", key.data(), i, line.data()); + continue; + } + + if (value.size() <= 0) + { + continue; + } + + empty = false; + + if (utils::string::is_numeric(key) || key.size() < 3 || !key.starts_with("\"") || !key.ends_with("\"")) + { + out_buffer.append(line); + out_buffer.append("\n"); + continue; + } + + const auto key_ = key.substr(1, key.size() - 2); + const auto id = xsk::gsc::h1::resolver::token_id(key_); + if (id == 0) + { + console::warn("[map_ents parser] Key '%s' not found, on line %i (%s)\n", key_.data(), i, line.data()); + continue; + } + + out_buffer.append(utils::string::va("%i \"%s\"\n", id, value.data())); + } + + return {out_buffer}; + } + + std::string entity_string; + const char* cm_entity_string_stub() + { + if (!entity_string.empty()) + { + return entity_string.data(); + } + + const auto original = utils::hook::invoke(SELECT_VALUE(0x3685C0_b, 0x4CD140_b)); + const auto parsed = parse_mapents(original); + if (parsed.has_value()) + { + entity_string = parsed.value(); + return entity_string.data(); + } + else + { + return original; + } + } + + void cm_unload_stub(void* clip_map) + { + entity_string.clear(); + utils::hook::invoke(SELECT_VALUE(0x368560_b, 0x4CD0E0_b), clip_map); + } + } + + class component final : public component_interface + { + public: + void post_unpack() override + { + utils::hook::call(SELECT_VALUE(0x2A1154_b, 0x41F594_b), cm_entity_string_stub); + utils::hook::call(SELECT_VALUE(0x1F4E74_b, 0x399814_b), cm_unload_stub); + } + }; +} + +REGISTER_COMPONENT(mapents::component) diff --git a/src/client/component/menus.cpp b/src/client/component/menus.cpp new file mode 100644 index 00000000..f610d35a --- /dev/null +++ b/src/client/component/menus.cpp @@ -0,0 +1,197 @@ +#include +#include "loader/component_loader.hpp" + +#include "menus.hpp" + +#include "game/game.hpp" + +#include "console.hpp" +#include "command.hpp" + +#include "utils/hook.hpp" +#include "utils/string.hpp" + +namespace menus +{ + namespace + { + std::string script_main_menu; + + bool keys_bypass_menu() + { + const auto* cl_bypass_mouse_input = game::Dvar_FindVar("cl_bypassMouseInput"); + if (cl_bypass_mouse_input && cl_bypass_mouse_input->current.enabled) + { + return true; + } + + return false; + } + + game::XAssetHeader load_script_menu_internal(const char* menu) + { + const char* menu_file = utils::string::va("ui_mp/scriptmenus/%s.menu", menu); + return game::DB_FindXAssetHeader(game::ASSET_TYPE_MENUFILE, menu_file, 1); + } + + bool load_script_menu(int client_num, const char* menu) + { + game::XAssetHeader asset = load_script_menu_internal(menu); + if (asset.data != nullptr) + { + game::UI_AddMenuList(game::ui_info_array, asset.data, 1); + return true; + } + + return false; + } + + void precache_script_menu(int client_num, int config_string_index) + { + const char* menu = game::CL_GetConfigString(config_string_index); + if (menu) + { + if (!load_script_menu(client_num, menu)) + { + game::Com_Error(game::ERR_DROP, "Could not load script menu file %s", menu); + } + } + } + + utils::hook::detour cg_set_config_values_hook; + void cg_set_config_values_stub(int client_num) + { + cg_set_config_values_hook.invoke(client_num); + + auto nesting = game::R_PopRemoteScreenUpdate(); + for (auto i = 3432; i < (3432 + 50); i++) + { + precache_script_menu(client_num, i); + } + + game::R_PushRemoteScreenUpdate(nesting); + } + + void ui_mouse_event(int client_num, int x, int y) + { + const auto scr_place = game::ScrPlace_GetViewPlacement(); + + const auto v_x = x / (game::ScrPlace_HiResGetScaleX() * scr_place->scaleVirtualToFull[0]); + const auto v_y = y / (game::ScrPlace_HiResGetScaleY() * scr_place->scaleVirtualToFull[1]); + + game::ui_info_array->cursor_x = v_x; + game::ui_info_array->cursor_x = v_y; + + const auto cursor_visible = v_x >= 0.0 && v_x <= 640.0 && v_y >= 0.0 && v_y <= 480.0; + if (!cursor_visible) + { + return; + } + + const auto menu_count = *reinterpret_cast(0x352F9B8_b); + if (menu_count > 0) + { + game::ui_info_array->cursor_x = v_x; + game::ui_info_array->cursor_y = v_y; + game::ui_info_array->cursor_time = game::Sys_Milliseconds() + 200; + game::ui_info_array->ingame_cursor_visible = cursor_visible; + game::Display_MouseMove(game::ui_info_array); + } + } + + int ui_mouse_fix(int cx_, int cy_, int dx_, int dy_) + { + if ((*game::keyCatchers & 0x10) != 0 && !keys_bypass_menu()) + { + tagPOINT cursor{}; + + game::CL_ShowSystemCursor(0); + game::CL_GetCursorPos(&cursor); + + ui_mouse_event(0, cursor.x, cursor.y); + return 0; + } + + return utils::hook::invoke(0x1384C0_b, cx_, cy_, dx_, dy_); + } + + bool open_script_main_menu() + { + if (!script_main_menu.empty()) + { + void* menu = game::Menus_FindByName(game::ui_info_array, script_main_menu.data()); + if (menu) + { + game::Menus_Open(game::ui_info_array, menu, 0); + return true; + } + } + + return false; + } + + void lui_toggle_menu_stub(int controller_index, void* context) + { + if (!game::VirtualLobby_Loaded()) + { + if (!script_main_menu.empty()) + { + if (game::Menu_IsMenuOpenAndVisible(0, script_main_menu.data())) + { + game::UI_SetActiveMenu(0, 0); + return; + } + else if (open_script_main_menu()) + { + *game::keyCatchers = *game::keyCatchers & 1 | 0x10; + return; + } + } + } + + // LUI_ToggleMenu + return utils::hook::invoke(0x270A90_b, controller_index, context); + } + } + + void set_script_main_menu(const std::string& menu) + { + script_main_menu = menu; + } + + class component final : public component_interface + { + public: + void post_unpack() override + { + if (!game::environment::is_mp()) + { + return; + } + + // add back legacy menu precache + cg_set_config_values_hook.create(0x11AC50_b, cg_set_config_values_stub); + + // add legacy menu mouse fix + utils::hook::call(0x5BA535_b, ui_mouse_fix); + + // add script main menu + utils::hook::call(0x1E5143_b, lui_toggle_menu_stub); // (CL_ExecBinding) + utils::hook::call(0x131377_b, lui_toggle_menu_stub); // (UI_SetActiveMenu) + + command::add("openmenu", [](const command::params& params) + { + if (params.size() != 2) + { + console::info("usage: openmenu \n"); + return; + } + + *game::keyCatchers = *game::keyCatchers & 1 | 0x10; + game::Menus_OpenByName(0, params.get(1)); + }); + } + }; +} + +REGISTER_COMPONENT(menus::component) \ No newline at end of file diff --git a/src/client/component/menus.hpp b/src/client/component/menus.hpp new file mode 100644 index 00000000..d817f790 --- /dev/null +++ b/src/client/component/menus.hpp @@ -0,0 +1,6 @@ +#pragma once + +namespace menus +{ + void set_script_main_menu(const std::string& menu); +} \ No newline at end of file diff --git a/src/client/component/mods.cpp b/src/client/component/mods.cpp index 9e0552ee..aad8c6ac 100644 --- a/src/client/component/mods.cpp +++ b/src/client/component/mods.cpp @@ -2,22 +2,22 @@ #include "loader/component_loader.hpp" #include "game/game.hpp" -#include "game/dvars.hpp" #include "command.hpp" #include "console.hpp" -#include "scheduler.hpp" #include "filesystem.hpp" -#include "materials.hpp" #include "fonts.hpp" +#include "localized_strings.hpp" +#include "materials.hpp" #include "mods.hpp" +#include "scheduler.hpp" #include #include namespace mods { - std::string mod_path{}; + std::optional mod_path; namespace { @@ -40,10 +40,66 @@ namespace mods scheduler::once([]() { release_assets = true; + const auto _0 = gsl::finally([]() + { + release_assets = false; + }); + game::Com_Shutdown(""); - release_assets = false; }, scheduler::pipeline::main); } + + void full_restart(const std::string& arg) + { + if (game::environment::is_mp()) + { + // vid_restart works on multiplayer, but not on singleplayer + command::execute("vid_restart"); + return; + } + + auto mode = game::environment::is_mp() ? " -multiplayer "s : " -singleplayer "s; + + utils::nt::relaunch_self(mode.append(arg), true); + utils::nt::terminate(); + } + + bool mod_requires_restart(const std::string& path) + { + return utils::io::file_exists(path + "/mod.ff") || utils::io::file_exists(path + "/zone/mod.ff"); + } + + void set_filesystem_data(const std::string& path) + { + if (mod_path.has_value()) + { + filesystem::unregister_path(mod_path.value()); + } + + if (!game::environment::is_sp()) + { + // modify fs_game on mp/dedi because its not set when we obviously vid_restart (sp does a full relaunch with command line arguments) + game::Dvar_SetFromStringByNameFromSource("fs_game", path.data(), + game::DVAR_SOURCE_INTERNAL); + } + } + } + + void set_mod(const std::string& path) + { + set_filesystem_data(path); + mod_path = path; + } + + void clear_mod() + { + set_filesystem_data(""); + mod_path.reset(); + } + + std::optional get_mod() + { + return mod_path; } class component final : public component_interface @@ -51,11 +107,6 @@ namespace mods public: void post_unpack() override { - if (!game::environment::is_sp()) - { - return; - } - if (!utils::io::directory_exists("mods")) { utils::io::create_directory("mods"); @@ -71,10 +122,10 @@ namespace mods return; } - if (!game::Com_InFrontend()) + if (!game::Com_InFrontend() && (game::environment::is_mp() && !game::VirtualLobby_Loaded())) { console::info("Cannot load mod while in-game!\n"); - game::CG_GameMessage(0, "^1Cannot unload mod while in-game!"); + game::CG_GameMessage(0, "^1Cannot load mod while in-game!"); return; } @@ -86,30 +137,57 @@ namespace mods } console::info("Loading mod %s\n", path); - filesystem::get_search_paths().erase(mod_path); - filesystem::get_search_paths().insert(path); - mod_path = path; - restart(); + set_mod(path); + + if ((mod_path.has_value() && mod_requires_restart(mod_path.value())) || + mod_requires_restart(path)) + { + console::info("Restarting...\n"); + full_restart("+set fs_game \""s + path + "\""); + } + else + { + restart(); + } }); command::add("unloadmod", [](const command::params& params) { - if (mod_path.empty()) + if (!mod_path.has_value()) { console::info("No mod loaded\n"); return; } - if (!game::Com_InFrontend()) + if (!game::Com_InFrontend() && (game::environment::is_mp() && !game::VirtualLobby_Loaded())) { console::info("Cannot unload mod while in-game!\n"); game::CG_GameMessage(0, "^1Cannot unload mod while in-game!"); return; } - console::info("Unloading mod %s\n", mod_path.data()); - filesystem::get_search_paths().erase(mod_path); - mod_path.clear(); + console::info("Unloading mod %s\n", mod_path.value().data()); + + if (mod_requires_restart(mod_path.value())) + { + console::info("Restarting...\n"); + clear_mod(); + full_restart(""); + } + else + { + clear_mod(); + restart(); + } + }); + + command::add("com_restart", []() + { + if (!game::Com_InFrontend() && (game::environment::is_mp() && !game::VirtualLobby_Loaded())) + { + return; + } + restart(); }); } diff --git a/src/client/component/mods.hpp b/src/client/component/mods.hpp index 364a2f11..6359a7ab 100644 --- a/src/client/component/mods.hpp +++ b/src/client/component/mods.hpp @@ -2,5 +2,7 @@ namespace mods { - extern std::string mod_path; + void set_mod(const std::string& path); + void clear_mod(); + std::optional get_mod(); } \ No newline at end of file diff --git a/src/client/component/network.cpp b/src/client/component/network.cpp index 336de37f..b9ad6039 100644 --- a/src/client/component/network.cpp +++ b/src/client/component/network.cpp @@ -7,6 +7,7 @@ #include "dvars.hpp" #include "game/dvars.hpp" +#include "game/game.hpp" #include #include @@ -33,7 +34,7 @@ namespace network return false; } - const std::string_view data(message->data + offset, message->cursize - offset); + const std::string data(message->data + offset, message->cursize - offset); handler->second(*address, data); #ifdef DEBUG @@ -302,11 +303,10 @@ namespace network utils::hook::set(0x4F1E25_b, max_packet_size); // ignore built in "print" oob command and add in our own - utils::hook::set(0x12F817_b, 0xEB); - on("print", [](const game::netadr_s&, const std::string_view& data) + utils::hook::set(0x12F817_b, 0xEB); + on("print", [](const game::netadr_s&, const std::string& data) { - const std::string message{data}; - console::info(message.data()); + console::info("%s\n", data.data()); }); // Use our own socket since the game's socket doesn't work with non localhost addresses diff --git a/src/client/component/network.hpp b/src/client/component/network.hpp index c8c9d7fc..a412b691 100644 --- a/src/client/component/network.hpp +++ b/src/client/component/network.hpp @@ -3,7 +3,7 @@ namespace network { - using callback = std::function; + using callback = std::function; void on(const std::string& command, const callback& callback); void send(const game::netadr_s& address, const std::string& command, const std::string& data = {}, char separator = ' '); diff --git a/src/client/component/party.cpp b/src/client/component/party.cpp index da192a27..a136f8b6 100644 --- a/src/client/component/party.cpp +++ b/src/client/component/party.cpp @@ -7,13 +7,21 @@ #include "network.hpp" #include "scheduler.hpp" #include "server_list.hpp" +#include "download.hpp" +#include "fastfiles.hpp" +#include "mods.hpp" + +#include "game/game.hpp" +#include "game/ui_scripting/execution.hpp" #include "steam/steam.hpp" +#include #include #include #include #include +#include namespace party { @@ -29,6 +37,26 @@ namespace party std::string sv_motd; int sv_maxclients; + struct usermap_file + { + std::string extension; + std::string name; + bool optional; + }; + + std::vector usermap_files = + { + {".ff", "usermaphash", false}, + {"_load.ff", "usermaploadhash", true}, + {".arena", "usermaparenahash", true}, + }; + + struct + { + game::netadr_s host{}; + utils::info_string info_string{}; + } saved_info_response; + void perform_game_initialization() { command::execute("onlinegame 1", true); @@ -70,8 +98,16 @@ namespace party perform_game_initialization(); - // exit from virtuallobby - utils::hook::invoke(0x13C9C0_b, 1); + if (game::VirtualLobby_Loaded()) + { + // exit from virtuallobby + utils::hook::invoke(0x13C9C0_b, 1); + } + + if (!fastfiles::is_stock_map(mapname)) + { + fastfiles::set_usermap(mapname); + } // CL_ConnectFromParty char session_info[0x100] = {}; @@ -138,17 +174,385 @@ namespace party utils::hook::detour cl_disconnect_hook; - void cl_disconnect_stub(int showMainMenu) // possibly bool + void cl_disconnect_stub(int show_main_menu) // possibly bool { party::clear_sv_motd(); - cl_disconnect_hook.invoke(showMainMenu); + if (!game::VirtualLobby_Loaded()) + { + fastfiles::clear_usermap(); + } + cl_disconnect_hook.invoke(show_main_menu); } - void menu_error(const std::string& error) + std::unordered_map hash_cache; + + std::string get_file_hash(const std::string& file) { - utils::hook::invoke(0x17D770_b, error.data(), "MENU_NOTICE"); - utils::hook::set(0x2ED2F78_b, 1); + if (!utils::io::file_exists(file)) + { + return {}; + } + + const auto iter = hash_cache.find(file); + if (iter != hash_cache.end()) + { + return iter->second; + } + + const auto data = utils::io::read_file(file); + const auto sha = utils::cryptography::sha1::compute(data, true); + hash_cache[file] = sha; + return sha; } + + std::string get_usermap_file_path(const std::string& mapname, const std::string& extension) + { + return utils::string::va("usermaps\\%s\\%s%s", mapname.data(), mapname.data(), extension.data()); + } + + void check_download_map(const utils::info_string& info, std::vector& files) + { + const auto mapname = info.get("mapname"); + if (fastfiles::is_stock_map(mapname)) + { + return; + } + + if (mapname.contains('.') || mapname.contains("::")) + { + throw std::runtime_error(utils::string::va("Invalid server mapname value %s\n", mapname.data())); + } + + const auto check_file = [&](const std::string& ext, const std::string& name, bool optional) + { + const std::string filename = utils::string::va("usermaps/%s/%s%s", mapname.data(), mapname.data(), ext.data()); + const auto source_hash = info.get(name); + if (source_hash.empty()) + { + if (!optional) + { + throw std::runtime_error(utils::string::va("Server %s is empty", name.data())); + } + + return; + } + + const auto hash = get_file_hash(filename); + if (hash != source_hash) + { + files.emplace_back(filename, source_hash); + return; + } + }; + + for (const auto& [ext, name, opt] : usermap_files) + { + check_file(ext, name, opt); + } + } + + bool check_download_mod(const utils::info_string& info, std::vector& files) + { + static const auto fs_game = game::Dvar_FindVar("fs_game"); + const auto client_fs_game = utils::string::to_lower(fs_game->current.string); + const auto server_fs_game = utils::string::to_lower(info.get("fs_game")); + + if (server_fs_game.empty() && client_fs_game.empty()) + { + return false; + } + + if (server_fs_game.empty() && !client_fs_game.empty()) + { + mods::clear_mod(); + return true; + } + + if (!server_fs_game.starts_with("mods/") || server_fs_game.contains('.') || server_fs_game.contains("::")) + { + throw std::runtime_error(utils::string::va("Invalid server fs_game value %s\n", server_fs_game.data())); + } + + const auto source_hash = info.get("modHash"); + if (source_hash.empty()) + { + throw std::runtime_error("Connection failed: Server mod hash is empty."); + } + + const auto mod_path = server_fs_game + "/mod.ff"; + auto has_to_download = !utils::io::file_exists(mod_path); + + if (!has_to_download) + { + const auto data = utils::io::read_file(mod_path); + const auto hash = utils::cryptography::sha1::compute(data, true); + + has_to_download = source_hash != hash; + } + + if (has_to_download) + { + files.emplace_back(mod_path, source_hash); + return false; + } + else if (client_fs_game != server_fs_game) + { + mods::set_mod(server_fs_game); + return true; + } + + return false; + } + + void close_joining_popups() + { + if (game::Menu_IsMenuOpenAndVisible(0, "popup_acceptinginvite")) + { + command::execute("lui_close popup_acceptinginvite", false); + } + if (game::Menu_IsMenuOpenAndVisible(0, "generic_waiting_popup_")) + { + command::execute("lui_close generic_waiting_popup_", false); + } + } + + std::string get_whitelist_json_path() + { + return (utils::properties::get_appdata_path() / "whitelist.json").generic_string(); + } + + nlohmann::json get_whitelist_json_object() + { + std::string data; + if (!utils::io::read_file(get_whitelist_json_path(), &data)) + { + return nullptr; + } + + nlohmann::json obj; + try + { + obj = nlohmann::json::parse(data.data()); + } + catch (const nlohmann::json::parse_error& ex) + { + menu_error(utils::string::va("%s\n", ex.what())); + return nullptr; + } + + return obj; + } + + std::string target_ip_to_string(const game::netadr_s& target) + { + return utils::string::va("%i.%i.%i.%i", + static_cast(saved_info_response.host.ip[0]), + static_cast(saved_info_response.host.ip[1]), + static_cast(saved_info_response.host.ip[2]), + static_cast(saved_info_response.host.ip[3])); + } + + bool download_files(const game::netadr_s& target, const utils::info_string& info, bool allow_download); + + bool should_user_confirm(const game::netadr_s& target) + { + nlohmann::json obj = get_whitelist_json_object(); + if (obj != nullptr) + { + const auto target_ip = target_ip_to_string(target); + for (const auto& [key, value] : obj.items()) + { + if (value.is_string() && value.get() == target_ip) + { + return false; + } + } + } + + close_joining_popups(); + command::execute("lui_open_popup popup_confirmdownload", false); + + return true; + } + + bool needs_vid_restart = false; + + bool download_files(const game::netadr_s& target, const utils::info_string& info, bool allow_download) + { + try + { + std::vector files{}; + + const auto needs_restart = check_download_mod(info, files); + needs_vid_restart = needs_vid_restart || needs_restart; + check_download_map(info, files); + + if (files.size() > 0) + { + if (!allow_download && should_user_confirm(target)) + { + return true; + } + + download::stop_download(); + download::start_download(target, info, files); + return true; + } + else if (needs_restart || needs_vid_restart) + { + command::execute("vid_restart"); + needs_vid_restart = false; + scheduler::once([=]() + { + connect(target); + }, scheduler::pipeline::main); + return true; + } + } + catch (const std::exception& e) + { + menu_error(e.what()); + return true; + } + + return false; + } + + void set_new_map(const char* mapname, const char* gametype, game::msg_t* msg) + { + if (game::SV_Loaded() || fastfiles::is_stock_map(mapname)) + { + utils::hook::invoke(0x13AAD0_b, mapname, gametype); + return; + } + + fastfiles::set_usermap(mapname); + + for (const auto& [ext, key, opt] : usermap_files) + { + char buffer[0x100] = {0}; + const std::string source_hash = game::MSG_ReadStringLine(msg, + buffer, static_cast(sizeof(buffer))); + + const auto path = get_usermap_file_path(mapname, ext); + const auto hash = get_file_hash(path); + + if ((!source_hash.empty() && hash != source_hash) || (source_hash.empty() && !opt)) + { + command::execute("disconnect"); + scheduler::once([] + { + connect(connect_state.host); + }, scheduler::pipeline::main); + return; + } + } + + utils::hook::invoke(0x13AAD0_b, mapname, gametype); + } + + void loading_new_map_cl_stub(utils::hook::assembler& a) + { + a.pushad64(); + a.mov(r8, rdi); + a.call_aligned(set_new_map); + a.popad64(); + + a.mov(al, 1); + a.jmp(0x12FCAA_b); + } + + std::string current_sv_mapname; + + void sv_spawn_server_stub(const char* map, void* a2, void* a3, void* a4, void* a5) + { + if (!fastfiles::is_stock_map(map)) + { + fastfiles::set_usermap(map); + } + + hash_cache.clear(); + current_sv_mapname = map; + utils::hook::invoke(0x54BBB0_b, map, a2, a3, a4, a5); + } + + utils::hook::detour net_out_of_band_print_hook; + void net_out_of_band_print_stub(game::netsrc_t sock, game::netadr_s* addr, const char* data) + { + if (!std::strstr(data, "loadingnewmap")) + { + return net_out_of_band_print_hook.invoke(sock, addr, data); + } + + std::string buffer{}; + const auto line = [&](const std::string& data_) + { + buffer.append(data_); + buffer.append("\n"); + }; + + const auto* sv_gametype = game::Dvar_FindVar("g_gametype"); + line("loadingnewmap"); + line(current_sv_mapname); + line(sv_gametype->current.string); + + const auto add_hash = [&](const std::string extension) + { + const auto filename = get_usermap_file_path(current_sv_mapname, extension); + const auto hash = get_file_hash(filename); + line(hash); + }; + + const auto is_usermap = fastfiles::usermap_exists(current_sv_mapname); + for (const auto& [ext, key, opt] : usermap_files) + { + if (is_usermap) + { + add_hash(ext); + } + else + { + line(""); + } + } + + net_out_of_band_print_hook.invoke(sock, addr, buffer.data()); + } + } + + std::string get_www_url() + { + return saved_info_response.info_string.get("sv_wwwBaseUrl"); + } + + void user_download_response(bool response) + { + if (!response) + { + return; + } + + nlohmann::json obj = get_whitelist_json_object(); + if (obj == nullptr) + { + obj = {}; + } + + obj.push_back(target_ip_to_string(saved_info_response.host)); + + utils::io::write_file(get_whitelist_json_path(), obj.dump(4)); + + download_files(saved_info_response.host, saved_info_response.info_string, true); + } + + void menu_error(const std::string& error) + { + console::error("%s\n", error.data()); + + close_joining_popups(); + + utils::hook::invoke(0x17D770_b, error.data(), "MENU_NOTICE"); // Com_SetLocalizedErrorMessage + *reinterpret_cast(0x2ED2F78_b) = 1; } void clear_sv_motd() @@ -242,18 +646,13 @@ namespace party return connect_state.host; } - std::string get_state_challenge() - { - return connect_state.challenge; - } - - void start_map(const std::string& mapname) + void start_map(const std::string& mapname, bool dev) { if (game::Live_SyncOnlineDataFlags(0) > 32) { scheduler::once([=]() { - command::execute("map " + mapname, false); + start_map(mapname, dev); }, scheduler::pipeline::main, 1s); } else @@ -299,6 +698,8 @@ namespace party command::execute(utils::string::va("party_maxplayers %i", maxclients->current.integer), true); }*/ + command::execute((dev ? "sv_cheats 1" : "sv_cheats 0"), true); + const auto* args = "StartServer"; game::UI_RunMenuScript(0, &args); } @@ -342,6 +743,11 @@ namespace party // allow custom didyouknow based on sv_motd utils::hook::call(0x1A8A3A_b, get_didyouknow_stub); + // add usermaphash to loadingnewmap command + utils::hook::jump(0x12FA68_b, utils::hook::assemble(loading_new_map_cl_stub), true); + utils::hook::call(0x54CC98_b, sv_spawn_server_stub); + net_out_of_band_print_hook.create(game::NET_OutOfBandPrint, net_out_of_band_print_stub); + command::add("map", [](const command::params& argument) { if (argument.size() != 2) @@ -349,7 +755,17 @@ namespace party return; } - start_map(argument[1]); + start_map(argument[1], false); + }); + + command::add("devmap", [](const command::params& argument) + { + if (argument.size() != 2) + { + return; + } + + party::start_map(argument[1], true); }); command::add("map_restart", []() @@ -509,7 +925,7 @@ namespace party game::SV_GameSendServerCommand(client_num, game::SV_CMD_CAN_IGNORE, utils::string::va("%c \"%s: %s\"", 84, name, message.data())); - printf("%s -> %i: %s\n", name, client_num, message.data()); + console::info("%s -> %i: %s\n", name, client_num, message.data()); }); command::add("tellraw", [](const command::params& params) @@ -524,7 +940,7 @@ namespace party game::SV_GameSendServerCommand(client_num, game::SV_CMD_CAN_IGNORE, utils::string::va("%c \"%s\"", 84, message.data())); - printf("%i: %s\n", client_num, message.data()); + console::info("%i: %s\n", client_num, message.data()); }); command::add("say", [](const command::params& params) @@ -539,7 +955,7 @@ namespace party game::SV_GameSendServerCommand( -1, game::SV_CMD_CAN_IGNORE, utils::string::va("%c \"%s: %s\"", 84, name, message.data())); - printf("%s: %s\n", name, message.data()); + console::info("%s: %s\n", name, message.data()); }); command::add("sayraw", [](const command::params& params) @@ -553,19 +969,21 @@ namespace party game::SV_GameSendServerCommand(-1, game::SV_CMD_CAN_IGNORE, utils::string::va("%c \"%s\"", 84, message.data())); - printf("%s\n", message.data()); + console::info("%s\n", message.data()); }); - network::on("getInfo", [](const game::netadr_s& target, const std::string_view& data) + network::on("getInfo", [](const game::netadr_s& target, const std::string& data) { - utils::info_string info{}; - info.set("challenge", std::string{data}); + const auto mapname = get_dvar_string("mapname"); + + utils::info_string info; + info.set("challenge", data); info.set("gamename", "H1"); info.set("hostname", get_dvar_string("sv_hostname")); info.set("gametype", get_dvar_string("g_gametype")); info.set("sv_motd", get_dvar_string("sv_motd")); info.set("xuid", utils::string::va("%llX", steam::SteamUser()->GetSteamID().bits)); - info.set("mapname", get_dvar_string("mapname")); + info.set("mapname", mapname); info.set("isPrivate", get_dvar_string("g_password").empty() ? "0" : "1"); info.set("clients", utils::string::va("%i", get_client_count())); info.set("bots", utils::string::va("%i", get_bot_count())); @@ -574,13 +992,38 @@ namespace party info.set("playmode", utils::string::va("%i", game::Com_GetCurrentCoDPlayMode())); info.set("sv_running", utils::string::va("%i", get_dvar_bool("sv_running") && !game::VirtualLobby_Loaded())); info.set("dedicated", utils::string::va("%i", get_dvar_bool("dedicated"))); + info.set("sv_wwwBaseUrl", get_dvar_string("sv_wwwBaseUrl")); + + if (!fastfiles::is_stock_map(mapname)) + { + const auto add_hash = [&](const std::string& extension, const std::string& name) + { + const auto path = get_usermap_file_path(mapname, extension); + const auto hash = get_file_hash(path); + info.set(name, hash); + }; + + for (const auto& [ext, name, opt] : usermap_files) + { + add_hash(ext, name); + } + } + + const auto fs_game = get_dvar_string("fs_game"); + info.set("fs_game", fs_game); + + if (!fs_game.empty()) + { + const auto hash = get_file_hash(utils::string::va("%s/mod.ff", fs_game.data())); + info.set("modHash", hash); + } network::send(target, "infoResponse", info.build(), '\n'); }); - network::on("infoResponse", [](const game::netadr_s& target, const std::string_view& data) + network::on("infoResponse", [](const game::netadr_s& target, const std::string& data) { - const utils::info_string info{data}; + const utils::info_string info(data); server_list::handle_info_response(target, info); if (connect_state.host != target) @@ -588,56 +1031,53 @@ namespace party return; } + saved_info_response = {}; + saved_info_response.host = target; + saved_info_response.info_string = info; + if (info.get("challenge") != connect_state.challenge) { - const auto str = "Invalid challenge."; - printf("%s\n", str); - menu_error(str); + menu_error("Connection failed: Invalid challenge."); return; } const auto gamename = info.get("gamename"); if (gamename != "H1"s) { - const auto str = "Invalid gamename."; - printf("%s\n", str); - menu_error(str); + menu_error("Connection failed: Invalid gamename."); return; } const auto playmode = info.get("playmode"); if (game::CodPlayMode(std::atoi(playmode.data())) != game::Com_GetCurrentCoDPlayMode()) { - const auto str = "Invalid playmode."; - printf("%s\n", str); - menu_error(str); + menu_error("Connection failed: Invalid playmode."); return; } const auto sv_running = info.get("sv_running"); if (!std::atoi(sv_running.data())) { - const auto str = "Server not running."; - printf("%s\n", str); - menu_error(str); + menu_error("Connection failed: Server not running."); return; } const auto mapname = info.get("mapname"); if (mapname.empty()) { - const auto str = "Invalid map."; - printf("%s\n", str); - menu_error(str); + menu_error("Connection failed: Invalid map."); return; } const auto gametype = info.get("gametype"); if (gametype.empty()) { - const auto str = "Invalid gametype."; - printf("%s\n", str); - menu_error(str); + menu_error("Connection failed: Invalid gametype."); + return; + } + + if (download_files(target, info, false)) + { return; } diff --git a/src/client/component/party.hpp b/src/client/component/party.hpp index 13990aea..ca4852b2 100644 --- a/src/client/component/party.hpp +++ b/src/client/component/party.hpp @@ -3,14 +3,18 @@ namespace party { + std::string get_www_url(); + void user_download_response(bool response); + + void menu_error(const std::string& error); + void reset_connect_state(); void connect(const game::netadr_s& target); - void start_map(const std::string& mapname); + void start_map(const std::string& mapname, bool dev = false); void clear_sv_motd(); game::netadr_s get_state_host(); - std::string get_state_challenge(); int server_client_count(); int get_client_num_by_name(const std::string& name); diff --git a/src/client/component/patches.cpp b/src/client/component/patches.cpp index e5b0b822..1dfd1b8f 100644 --- a/src/client/component/patches.cpp +++ b/src/client/component/patches.cpp @@ -9,6 +9,7 @@ #include "network.hpp" #include "scheduler.hpp" #include "filesystem.hpp" +#include "menus.hpp" #include "game/game.hpp" #include "game/dvars.hpp" @@ -63,17 +64,66 @@ namespace patches return com_register_dvars_hook.invoke(); } - utils::hook::detour set_client_dvar_from_server_hook; + utils::hook::detour cg_set_client_dvar_from_server_hook; - void set_client_dvar_from_server_stub(void* clientNum, void* cgameGlob, const char* dvar, const char* value) + void cg_set_client_dvar_from_server_stub(void* clientNum, void* cgameGlob, const char* dvar_hash, const char* value) { - const auto dvar_lowercase = utils::string::to_lower(dvar); - if (dvar_lowercase == "cg_fov"s || dvar_lowercase == "cg_fovMin"s) + int hash = atoi(dvar_hash); + auto* dvar = game::Dvar_FindMalleableVar(hash); + + if (hash == game::generateHashValue("cg_fov") || + hash == game::generateHashValue("cg_fovMin") || + hash == game::generateHashValue("cg_fovScale")) { return; } - set_client_dvar_from_server_hook.invoke(0x11AA90_b, clientNum, cgameGlob, dvar, value); + if (hash == game::generateHashValue("g_scriptMainMenu")) + { + menus::set_script_main_menu(value); + } + + // register new dvar + if (!dvar) + { + game::Dvar_RegisterString(hash, "", value, game::DVAR_FLAG_EXTERNAL); + return; + } + + // only set if dvar has no flags or has cheat flag or has external flag + if (dvar->flags == game::DVAR_FLAG_NONE || + (dvar->flags & game::DVAR_FLAG_CHEAT) != 0 || + (dvar->flags & game::DVAR_FLAG_EXTERNAL) != 0) + { + game::Dvar_SetFromStringFromSource(dvar, value, game::DvarSetSource::DVAR_SOURCE_EXTERNAL); + } + + // original code + int index = 0; + auto result = utils::hook::invoke(0x4745E0_b, dvar, &index); // NetConstStrings_SV_GetNetworkDvarIndex + if (result) + { + std::string index_str = std::to_string(index); + return cg_set_client_dvar_from_server_hook.invoke(clientNum, cgameGlob, index_str.data(), value); + } + } + + game::dvar_t* get_client_dvar(const char* name) + { + game::dvar_t* dvar = game::Dvar_FindVar(name); + if (!dvar) + { + static game::dvar_t dummy{0}; + dummy.hash = game::generateHashValue(name); + return &dummy; + } + return dvar; + } + + bool get_client_dvar_hash(game::dvar_t* dvar, int* hash) + { + *hash = dvar->hash; + return true; } const char* db_read_raw_file_stub(const char* filename, char* buf, const int size) @@ -84,10 +134,10 @@ namespace patches file_name.append(".cfg"); } - const auto file = filesystem::file(file_name); - if (file.exists()) + std::string buffer{}; + if (filesystem::read_file(file_name, &buffer)) { - snprintf(buf, size, "%s\n", file.get_buffer().data()); + snprintf(buf, size, "%s\n", buffer.data()); return buf; } @@ -386,11 +436,19 @@ namespace patches utils::hook::inject(0x54DCE5_b, VERSION); // prevent servers overriding our fov - set_client_dvar_from_server_hook.create(0x11AA90_b, set_client_dvar_from_server_stub); utils::hook::nop(0x17DA96_b, 0x16); utils::hook::nop(0xE00BE_b, 0x17); utils::hook::set(0x307F39_b, 0xEB); + // make setclientdvar behave like older games + cg_set_client_dvar_from_server_hook.create(0x11AA90_b, cg_set_client_dvar_from_server_stub); + utils::hook::call(0x407EC5_b, get_client_dvar_hash); // setclientdvar + utils::hook::call(0x4087C1_b, get_client_dvar_hash); // setclientdvars + utils::hook::call(0x407E8E_b, get_client_dvar); // setclientdvar + utils::hook::call(0x40878A_b, get_client_dvar); // setclientdvars + utils::hook::set(0x407EB6_b, 0xEB); // setclientdvar + utils::hook::set(0x4087B2_b, 0xEB); // setclientdvars + // some [data validation] anti tamper thing that kills performance dvars::override::register_int("dvl", 0, 0, 0, game::DVAR_FLAG_READ); @@ -439,4 +497,4 @@ namespace patches }; } -REGISTER_COMPONENT(patches::component) +REGISTER_COMPONENT(patches::component) \ No newline at end of file diff --git a/src/client/component/scripting.cpp b/src/client/component/scripting.cpp index 9bcd2bc9..3f9e9ad9 100644 --- a/src/client/component/scripting.cpp +++ b/src/client/component/scripting.cpp @@ -1,32 +1,39 @@ #include #include "loader/component_loader.hpp" +#include "component/gsc/script_extension.hpp" +#include "component/gsc/script_loading.hpp" +#include "component/scheduler.hpp" +#include "component/scripting.hpp" + +#include "console.hpp" + #include "game/game.hpp" -#include "game/dvars.hpp" -#include "game/scripting/entity.hpp" -#include "game/scripting/functions.hpp" #include "game/scripting/event.hpp" -#include "game/scripting/lua/engine.hpp" #include "game/scripting/execution.hpp" - -#include "scheduler.hpp" -#include "scripting.hpp" +#include "game/scripting/functions.hpp" +#include "game/scripting/lua/engine.hpp" #include -#include -#include namespace scripting { std::unordered_map> fields_table; + std::unordered_map> script_function_table; + std::unordered_map>> script_function_table_sort; + std::unordered_map> script_function_table_rev; + utils::concurrency::container shared_table; + std::string current_file; + namespace { utils::hook::detour vm_notify_hook; utils::hook::detour vm_execute_hook; + utils::hook::detour g_load_structs_hook; utils::hook::detour scr_load_level_hook; utils::hook::detour g_shutdown_game_hook; @@ -39,12 +46,14 @@ namespace scripting utils::hook::detour db_find_xasset_header_hook; - std::string current_file; + std::string current_script_file; unsigned int current_file_id{}; game::dvar_t* g_dump_scripts; - std::vector> shutdown_callbacks; + std::vector> shutdown_callbacks; + + std::unordered_map canonical_string_table; void vm_notify_stub(const unsigned int notify_list_owner_id, const game::scr_string_t string_value, game::VariableValue* top) @@ -58,7 +67,7 @@ namespace scripting e.name = string; e.entity = notify_list_owner_id; - for (auto* value = top; value->type != game::SCRIPT_END; --value) + for (auto* value = top; value->type != game::VAR_PRECODEPOS; --value) { e.arguments.emplace_back(*value); } @@ -80,30 +89,58 @@ namespace scripting return vm_execute_hook.invoke(); } - void scr_load_level_stub() + void g_load_structs_stub() { - scr_load_level_hook.invoke(); if (!game::VirtualLobby_Loaded()) { + game::G_LogPrintf("------------------------------------------------------------\n"); + game::G_LogPrintf("InitGame\n"); + lua::engine::start(); + + gsc::load_main_handles(); } + + g_load_structs_hook.invoke(); + } + + void scr_load_level_stub() + { + if (!game::VirtualLobby_Loaded()) + { + gsc::load_init_handles(); + } + + scr_load_level_hook.invoke(); } void g_shutdown_game_stub(const int free_scripts) { if (free_scripts) { + script_function_table_sort.clear(); script_function_table.clear(); + script_function_table_rev.clear(); + canonical_string_table.clear(); } for (const auto& callback : shutdown_callbacks) { - callback(); + callback(free_scripts, false); } scripting::notify(*game::levelEntityId, "shutdownGame_called", {1}); lua::engine::stop(); - return g_shutdown_game_hook.invoke(free_scripts); + + game::G_LogPrintf("ShutdownGame:\n"); + game::G_LogPrintf("------------------------------------------------------------\n"); + + g_shutdown_game_hook.invoke(free_scripts); + + for (const auto& callback : shutdown_callbacks) + { + callback(free_scripts, true); + } } void scr_add_class_field_stub(unsigned int classnum, game::scr_string_t name, unsigned int canonical_string, unsigned int offset) @@ -120,10 +157,12 @@ namespace scripting void process_script_stub(const char* filename) { + current_script_file = filename; + const auto file_id = atoi(filename); if (file_id) { - current_file_id = file_id; + current_file_id = static_cast(file_id); } else { @@ -134,24 +173,44 @@ namespace scripting process_script_hook.invoke(filename); } + void add_function_sort(unsigned int id, const char* pos) + { + std::string filename = current_file; + if (current_file_id) + { + filename = scripting::get_token(current_file_id); + } + + if (!script_function_table_sort.contains(filename)) + { + const auto script = gsc::find_script(game::ASSET_TYPE_SCRIPTFILE, current_script_file.data(), false); + if (script) + { + const auto end = &script->bytecode[script->bytecodeLen]; + script_function_table_sort[filename].emplace_back("__end__", end); + } + } + + const auto name = scripting::get_token(id); + auto& itr = script_function_table_sort[filename]; + itr.insert(itr.end() - 1, {name, pos}); + } + void add_function(const std::string& file, unsigned int id, const char* pos) { - const auto function_names = scripting::find_token(id); - for (const auto& name : function_names) - { - script_function_table[file][name] = pos; - } + const auto name = get_token(id); + script_function_table[file][name] = pos; + script_function_table_rev[pos] = {file, name}; } void scr_set_thread_position_stub(unsigned int thread_name, const char* code_pos) { + add_function_sort(thread_name, code_pos); + if (current_file_id) { - const auto names = scripting::find_token(current_file_id); - for (const auto& name : names) - { - add_function(name, thread_name, code_pos); - } + const auto name = get_token(current_file_id); + add_function(name, thread_name, code_pos); } else { @@ -164,16 +223,47 @@ namespace scripting unsigned int sl_get_canonical_string_stub(const char* str) { const auto result = sl_get_canonical_string_hook.invoke(str); - scripting::token_map[str] = result; + canonical_string_table[result] = str; return result; } + + void* get_spawn_point_stub() + { + const auto spawn_point = utils::hook::invoke(0x28BD50_b); + if (spawn_point == nullptr) + { + console::warn("No spawnpoint found for this map, using (0, 0, 0)\n"); + return &game::sp::g_entities[0]; + } + return spawn_point; + } } - void on_shutdown(const std::function& callback) + std::string get_token(unsigned int id) + { + if (canonical_string_table.find(id) != canonical_string_table.end()) + { + return canonical_string_table[id]; + } + + return scripting::find_token(id); + } + + void on_shutdown(const std::function& callback) { shutdown_callbacks.push_back(callback); } + std::optional get_canonical_string(const unsigned int id) + { + if (canonical_string_table.find(id) == canonical_string_table.end()) + { + return {}; + } + + return {canonical_string_table[id]}; + } + class component final : public component_interface { public: @@ -187,18 +277,21 @@ namespace scripting process_script_hook.create(SELECT_VALUE(0x3C7200_b, 0x50E340_b), process_script_stub); sl_get_canonical_string_hook.create(game::SL_GetCanonicalString, sl_get_canonical_string_stub); - if (!game::environment::is_sp()) - { - scr_load_level_hook.create(0x450FC0_b, scr_load_level_stub); - } - else + g_load_structs_hook.create(SELECT_VALUE(0x2E7970_b, 0x458520_b), g_load_structs_stub); + scr_load_level_hook.create(SELECT_VALUE(0x2D4CD0_b, 0x450FC0_b), scr_load_level_stub); + if (game::environment::is_sp()) { vm_execute_hook.create(0x3CA080_b, vm_execute_stub); } g_shutdown_game_hook.create(SELECT_VALUE(0x2A5130_b, 0x422F30_b), g_shutdown_game_stub); - scheduler::loop([]() + if (game::environment::is_sp()) + { + utils::hook::call(0x28AE82_b, get_spawn_point_stub); + } + + scheduler::loop([] { lua::engine::run_frame(); }, scheduler::pipeline::server); diff --git a/src/client/component/scripting.hpp b/src/client/component/scripting.hpp index 226b275c..12598b9c 100644 --- a/src/client/component/scripting.hpp +++ b/src/client/component/scripting.hpp @@ -7,7 +7,14 @@ namespace scripting extern std::unordered_map> fields_table; extern std::unordered_map> script_function_table; + extern std::unordered_map>> script_function_table_sort; + extern std::unordered_map> script_function_table_rev; + extern utils::concurrency::container shared_table; - void on_shutdown(const std::function& callback); + extern std::string current_file; + + void on_shutdown(const std::function& callback); + std::optional get_canonical_string(const unsigned int id); + std::string get_token(unsigned int id); } \ No newline at end of file diff --git a/src/client/component/server_list.cpp b/src/client/component/server_list.cpp index 9eb7fded..d9b2d668 100644 --- a/src/client/component/server_list.cpp +++ b/src/client/component/server_list.cpp @@ -491,7 +491,7 @@ namespace server_list scheduler::loop(do_frame_work, scheduler::pipeline::main); scheduler::loop(check_refresh, scheduler::pipeline::lui, 10ms); - network::on("getServersResponse", [](const game::netadr_s& target, const std::string_view& data) + network::on("getServersResponse", [](const game::netadr_s& target, const std::string& data) { { std::lock_guard _(mutex); @@ -503,7 +503,7 @@ namespace server_list master_state.requesting = false; std::optional start{}; - for (size_t i = 0; i + 6 < data.size(); ++i) + for (std::size_t i = 0; i + 6 < data.size(); ++i) { if (data[i + 6] == '\\') { @@ -527,8 +527,8 @@ namespace server_list game::netadr_s address{}; address.type = game::NA_IP; address.localNetID = game::NS_CLIENT1; - memcpy(&address.ip[0], data.data() + i + 0, 4); - memcpy(&address.port, data.data() + i + 4, 2); + std::memcpy(&address.ip[0], data.data() + i + 0, 4); + std::memcpy(&address.port, data.data() + i + 4, 2); master_state.queued_servers[address] = 0; } diff --git a/src/client/component/slowmotion.cpp b/src/client/component/slowmotion.cpp index f7ed223a..e70911fb 100644 --- a/src/client/component/slowmotion.cpp +++ b/src/client/component/slowmotion.cpp @@ -1,5 +1,8 @@ #include #include "loader/component_loader.hpp" + +#include "gsc/script_extension.hpp" + #include "game/game.hpp" #include @@ -7,34 +10,6 @@ namespace slowmotion { - namespace - { - void scr_cmd_set_slow_motion() - { - if (game::Scr_GetNumParam() < 1) - { - return; - } - - int duration = 1000; - float end = 1.0f; - const float start = game::Scr_GetFloat(0); - - if (game::Scr_GetNumParam() >= 2) - { - end = game::Scr_GetFloat(1u); - } - - if (game::Scr_GetNumParam() >= 3) - { - duration = static_cast(game::Scr_GetFloat(2u) * 1000.0f); - } - - game::SV_SetConfigstring(10, utils::string::va("%i %i %g %g", *game::mp::gameTime, duration, start, end)); - game::Com_SetSlowMotion(start, end, duration); - } - } - class component final : public component_interface { public: @@ -45,7 +20,22 @@ namespace slowmotion return; } - utils::hook::jump(0x43D2E0_b, scr_cmd_set_slow_motion); + gsc::function::add("setslowmotion", [](const gsc::function_args& args) + { + if (args.size() == 0) + { + return scripting::script_value{}; + } + + const auto start = args[0].as(); + const auto end = (args.size() > 0 ? args[1].as() : 1.0f); + const auto duration = (args.size() > 1 ? args[2].as() : 1) * 1000; + + game::SV_SetConfigstring(10, utils::string::va("%i %i %g %g", *game::mp::gameTime, duration, start, end)); + game::Com_SetSlowMotion(start, end, duration); + + return scripting::script_value{}; + }); } }; } diff --git a/src/client/component/steam_proxy.cpp b/src/client/component/steam_proxy.cpp index 117cab4f..1c840647 100644 --- a/src/client/component/steam_proxy.cpp +++ b/src/client/component/steam_proxy.cpp @@ -3,16 +3,18 @@ #include "steam_proxy.hpp" #include "scheduler.hpp" -#include -#include -#include -#include +#include "arxan.hpp" #include "game/game.hpp" #include "steam/interface.hpp" #include "steam/steam.hpp" +#include +#include +#include +#include + namespace steam_proxy { namespace @@ -31,12 +33,7 @@ namespace steam_proxy public: void post_load() override { - if (game::environment::is_dedi() || is_disabled()) - { - return; - } - - if (!FindWindowA(0, "Steam")) + if (game::environment::is_dedi() || is_disabled() || !FindWindowA(0, "Steam")) { return; } @@ -107,7 +104,10 @@ namespace steam_proxy void load_client() { const std::filesystem::path steam_path = steam::SteamAPI_GetSteamInstallPath(); - if (steam_path.empty()) return; + if (steam_path.empty()) + { + return; + } utils::nt::library::load(steam_path / "tier0_s64.dll"); utils::nt::library::load(steam_path / "vstdlib_s64.dll"); diff --git a/src/client/component/system_check.cpp b/src/client/component/system_check.cpp index 794bf375..da1dc452 100644 --- a/src/client/component/system_check.cpp +++ b/src/client/component/system_check.cpp @@ -1,9 +1,11 @@ #include #include "loader/component_loader.hpp" + #include "system_check.hpp" #include "game/game.hpp" +#include #include #include @@ -64,12 +66,16 @@ namespace system_check return verify_hashes(mp_zone_hashes) && (game::environment::is_dedi() || verify_hashes(sp_zone_hashes)); } - // need to update these values void verify_binary_version() { const auto value = *reinterpret_cast(0x1337_b); - if (value != 0x60202B6A && value != 0xBC0E9FE) + if (!utils::nt::is_wine()) { + if (value == 0x60202B6A || value == 0xBC0E9FE) + { + return; + } + throw std::runtime_error("Unsupported Call of Duty: Modern Warfare Remastered version (1.15)"); } } @@ -90,9 +96,8 @@ namespace system_check if (!is_valid()) { - MessageBoxA(nullptr, "Your game files are outdated or unsupported.\n" - "Please get the latest officially supported Call of Duty: Modern Warfare Remastered files, or you will get random crashes and issues.", - "Invalid game files!", MB_ICONINFORMATION); + MSG_BOX_INFO("Your game files are outdated or unsupported.\n" + "Please get the latest officially supported Call of Duty: Modern Warfare Remastered files, or you will get random crashes and issues."); } } }; diff --git a/src/client/component/ui_scripting.cpp b/src/client/component/ui_scripting.cpp index 7c2e8ea8..1fcfb1d1 100644 --- a/src/client/component/ui_scripting.cpp +++ b/src/client/component/ui_scripting.cpp @@ -9,6 +9,7 @@ #include "localized_strings.hpp" #include "console.hpp" +#include "download.hpp" #include "game_module.hpp" #include "fps.hpp" #include "server_list.hpp" @@ -18,6 +19,7 @@ #include "scripting.hpp" #include "updater.hpp" #include "server_list.hpp" +#include "party.hpp" #include "game/ui_scripting/execution.hpp" #include "game/scripting/execution.hpp" @@ -29,6 +31,8 @@ #include #include +#include "steam/steam.hpp" + namespace ui_scripting { namespace @@ -45,16 +49,10 @@ namespace ui_scripting const auto lui_updater = utils::nt::load_resource(LUI_UPDATER); const auto lua_json = utils::nt::load_resource(LUA_JSON); - struct script - { - std::string name; - std::string root; - }; - struct globals_t { std::string in_require_script; - std::vector