diff --git a/SSV2/includes/backend.lua b/SSV2/includes/backend.lua index 3b8244e..e644111 100644 --- a/SSV2/includes/backend.lua +++ b/SSV2/includes/backend.lua @@ -74,9 +74,9 @@ local Backend = { ---@type table MaxAllowedEntities = { - [Enums.eEntityType.Ped] = 50, - [Enums.eEntityType.Vehicle] = 25, - [Enums.eEntityType.Object] = 75, + [Enums.eEntityType.Ped] = 25, + [Enums.eEntityType.Vehicle] = 15, + [Enums.eEntityType.Object] = 35, }, } Backend.__index = Backend @@ -456,7 +456,6 @@ function Backend:OnPlayerSwitch() end self.is_in_player_transition = true - ThreadManager:Run(function() self:TriggerEventCallbacks(Enums.eBackendEvent.PLAYER_SWITCH) @@ -469,36 +468,39 @@ function Backend:OnPlayerSwitch() end function Backend:RegisterHandlers() - self.debug_mode = self:IsMockEnv() or GVars.backend.debug_mode or false + local mockEnv = self:IsMockEnv() + self.debug_mode = mockEnv or GVars.backend.debug_mode or false - if (self:IsMockEnv()) then - return - end + if (mockEnv) then return end ThreadManager:RegisterLooped("SS_CTRLS", function() if (self.disable_input) then PAD.DISABLE_ALL_CONTROL_ACTIONS(0) - end - - if ((gui.is_open() or GUI:IsOpen()) and not self.disable_input) then - self:DisableAttackInput() - end + else + if ((gui.is_open() or GUI:IsOpen())) then + self:DisableAttackInput() + end - for _, control in pairs(self.ControlsToDisable) do - PAD.DISABLE_CONTROL_ACTION(0, control, true) + for _, control in pairs(self.ControlsToDisable) do + PAD.DISABLE_CONTROL_ACTION(0, control, true) + end end end) ThreadManager:RegisterLooped("SS_BACKEND", function() self:OnPlayerSwitch() self:OnSessionSwitch() + PreviewService:Update() Decorator:CollectGarbage() + Translator:OnTick() + yield() end) ThreadManager:RegisterLooped("SS_POOLMGR", function() self:PoolMgr() + yield() end) event.register_handler(menu_event.MenuUnloaded, function() self:Cleanup() end) diff --git a/SSV2/includes/classes/Mutex.lua b/SSV2/includes/classes/Mutex.lua index 7ac7d84..2062bd8 100644 --- a/SSV2/includes/classes/Mutex.lua +++ b/SSV2/includes/classes/Mutex.lua @@ -10,7 +10,7 @@ -------------------------------------- -- Class: Mutex -------------------------------------- --- Simple mutual exclusion. +-- Simple mutual exclusivity. ---@class Mutex ---@field protected m_locked boolean ---@overload fun(): Mutex @@ -42,9 +42,10 @@ function Mutex:Release() end -- Scoped lock. ----@param func function +---@generic R1, R2, R3, R4, R5 +---@param func fun(...?: any): R1?, R2?, R3?, R4?, R5?, ...? ---@param ... any ----@return ... +---@return boolean success, R1?, R2?, R3?, R4?, R5?, ...? function Mutex:WithLock(func, ...) self:Acquire() local ret = { xpcall(func, function(msg) diff --git a/SSV2/includes/classes/gta/CPed.lua b/SSV2/includes/classes/gta/CPed.lua index a684563..c5dc23f 100644 --- a/SSV2/includes/classes/gta/CPed.lua +++ b/SSV2/includes/classes/gta/CPed.lua @@ -27,7 +27,7 @@ local CPlayerInfo = require("includes.classes.gta.CPlayerInfo") ---@field m_ped_weapon_mgr pointer_ref ---@field m_player_info CPlayerInfo ---@field m_velocity pointer ----@field m_ped_type pointer +---@field m_ped_type pointer ---@field m_ped_task_flag pointer ---@field m_seatbelt pointer ---@field m_armor pointer @@ -64,14 +64,14 @@ end ---@return boolean function CPed:CanRagdoll() return self:__safecall(false, function() - return (self.m_ped_type & 0x20) ~= 0 + return (self.m_ped_type:get_dword() & 0x20) ~= 0 end) end ---@return boolean function CPed:HasSeatbelt() return self:__safecall(false, function() - return (self.m_seatbelt & 0x3) ~= 0 + return (self.m_seatbelt:get_byte() & 0x3) ~= 0 end) end @@ -85,7 +85,7 @@ end ---@return ePedType function CPed:GetPedType() return self:__safecall(-1, function() - return (self.m_ped_type:get_word() << 11 >> 25) + return (self.m_ped_type:get_dword() << 11 >> 25) end) end diff --git a/SSV2/includes/data/config.lua b/SSV2/includes/data/config.lua index 28bc584..1099721 100644 --- a/SSV2/includes/data/config.lua +++ b/SSV2/includes/data/config.lua @@ -14,7 +14,8 @@ local Config = { auto_cleanup_entities = false, language_index = 1, language_code = "en-US", - language_name = "English" + language_name = "English", + use_game_language = false }, ui = { disable_tooltips = false, diff --git a/SSV2/includes/data/enums/__init__.lua b/SSV2/includes/data/enums/__init__.lua index 59cc9b3..c39ef03 100644 --- a/SSV2/includes/data/enums/__init__.lua +++ b/SSV2/includes/data/enums/__init__.lua @@ -8,27 +8,28 @@ local Enums = { + eActionType = require("includes.data.enums.action_type"), + eAnimFlags = require("includes.data.enums.anim_flags"), + eDrivingFlags = require("includes.data.enums.driving_flags"), eGameState = require("includes.data.enums.game_state"), - eModelType = require("includes.data.enums.model_type"), - eRagdollBlockingFlags = require("includes.data.enums.ragdoll_blocking_flags"), - eVehicleClasses = require("includes.data.enums.vehicle_classes"), + eGameLanguage = require("includes.data.enums.game_language"), eHandlingType = require("includes.data.enums.handling_type"), - eDrivingFlags = require("includes.data.enums.driving_flags"), - eVehicleHandlingFlags = require("includes.data.enums.handling_flags"), - eVehicleModelFlags = require("includes.data.enums.vehicle_model_flags"), - eVehicleModelInfoFlags = require("includes.data.enums.vehicle_model_info_flags"), - eVehicleAdvancedFlags = require("includes.data.enums.vehicle_advanced_flags"), - ePedType = require("includes.data.enums.ped_type"), - ePedGender = require("includes.data.enums.ped_gender"), + eLandingGearState = require("includes.data.enums.landing_gear_state"), + eModelType = require("includes.data.enums.model_type"), + ePedCombatAttributes = require("includes.data.enums.ped_combat_attributes"), ePedComponents = require("includes.data.enums.ped_components"), ePedConfigFlags = require("includes.data.enums.ped_config_flags"), + ePedGender = require("includes.data.enums.ped_gender"), ePedResetFlags = require("includes.data.enums.ped_reset_flags"), - ePedCombatAttributes = require("includes.data.enums.ped_combat_attributes"), ePedTaskIndex = require("includes.data.enums.ped_task_index"), - eAnimFlags = require("includes.data.enums.anim_flags"), - eActionType = require("includes.data.enums.action_type"), + ePedType = require("includes.data.enums.ped_type"), + eRagdollBlockingFlags = require("includes.data.enums.ragdoll_blocking_flags"), + eVehicleAdvancedFlags = require("includes.data.enums.vehicle_advanced_flags"), + eVehicleClasses = require("includes.data.enums.vehicle_classes"), + eVehicleHandlingFlags = require("includes.data.enums.vehicle_handling_flags"), + eVehicleModelFlags = require("includes.data.enums.vehicle_model_flags"), + eVehicleModelInfoFlags = require("includes.data.enums.vehicle_model_info_flags"), eVehicleTask = require("includes.data.enums.vehicle_task"), - eLandingGearState = require("includes.data.enums.landing_gear_state"), } return Enums diff --git a/SSV2/includes/data/enums/game_language.lua b/SSV2/includes/data/enums/game_language.lua new file mode 100644 index 0000000..340941d --- /dev/null +++ b/SSV2/includes/data/enums/game_language.lua @@ -0,0 +1,27 @@ +-- Copyright (C) 2026 SAMURAI (xesdoog) & Contributors. +-- This file is part of Samurai's Scripts. +-- +-- Permission is hereby granted to copy, modify, and redistribute +-- this code as long as you respect these conditions: +-- * Credit the owner and contributors. +-- * Provide a copy of or a link to the original license (GPL-3.0 or later); see LICENSE.md or . + + +---@enum eGameLanguage +local eGameLanguage = { + ENGLISH = 0, + FRENCH = 1, + GERMAN = 2, + ITALIAN = 3, + SPANISH = 4, + PORTUGUESE_BRASIL = 5, + POLISH = 6, + RUSSIAN = 7, + KOREAN = 8, + CHINESE_TRADITIONAL = 9, + JAPANESE = 10, + SPANISH_MEXICAN = 11, + CHINESE_SIMPLIFIED = 12, +} + +return eGameLanguage diff --git a/SSV2/includes/data/enums/handling_flags.lua b/SSV2/includes/data/enums/vehicle_handling_flags.lua similarity index 100% rename from SSV2/includes/data/enums/handling_flags.lua rename to SSV2/includes/data/enums/vehicle_handling_flags.lua diff --git a/SSV2/includes/data/pointers.lua b/SSV2/includes/data/pointers.lua index 4ca9389..7b542a3 100644 --- a/SSV2/includes/data/pointers.lua +++ b/SSV2/includes/data/pointers.lua @@ -21,7 +21,7 @@ PatternScanner = require("includes.services.PatternScanner") -- -- **NOTE:** Please make sure no modules/files try to use a pointer before the scan is complete. -- --- You can call `PatternScanner:IsDone()` to double check. +-- You can call `PatternScanner:IsDone()` to double check.- ---@class GPointers ---@field ScriptGlobals pointer ---@field GameState pointer @@ -66,12 +66,10 @@ local mem_batches = { GPointers.ScriptGlobals = ptr:add(0x3):rip() end), MemoryBatch.new("GameVersion", "8B C3 33 D2 C6 44 24 20", function(ptr) - local pGameBuild = ptr:add(0x24):rip() - local pOnlineVersion = pGameBuild:add(0x20) - GPointers.GameVersion = { - build = pGameBuild:get_string(), - online = pOnlineVersion:get_string() - } + local pGameBuild = ptr:add(0x24):rip() + local pOnlineVersion = pGameBuild:add(0x20) + GPointers.GameVersion.build = pGameBuild:get_string() + GPointers.GameVersion.online = pOnlineVersion:get_string() end), MemoryBatch.new("GameState", "81 39 5D 6D FF AF 75 20", function(ptr) GPointers.GameState = ptr:add(0xA):rip():add(0x1) @@ -80,10 +78,8 @@ local mem_batches = { GPointers.GameTime = ptr:add(0x2):rip() end), MemoryBatch.new("ScreenResolution", "66 0F 6E 0D ? ? ? ? 0F B7 3D", function(ptr) - GPointers.ScreenResolution = vec2:new( - ptr:sub(0x4):rip():get_word(), - ptr:add(0x4):rip():get_word() - ) + GPointers.ScreenResolution.x = ptr:sub(0x4):rip():get_word() + GPointers.ScreenResolution.y = ptr:add(0x4):rip():get_word() end), -- TODO: enable once dynamic calls become stable. For now either the JIT compiler is broken or I'm just outright stupid. @@ -103,19 +99,15 @@ local mem_batches = { GPointers.ScriptGlobals = ptr:add(0x7):add(0x3):rip() end), MemoryBatch.new("GameVersion", "4C 8D 0D ? ? ? ? 48 8D 5C 24 ? 48 89 D9 48 89 FA", function(ptr) - GPointers.GameVersion = { - build = ptr:add(0x3):rip():get_string(), - online = ptr:add(0x47):add(0x3):rip():get_string() - } + GPointers.GameVersion.build = ptr:add(0x3):rip():get_string() + GPointers.GameVersion.online = ptr:add(0x47):add(0x3):rip():get_string() end), MemoryBatch.new("GameState", "83 3D ? ? ? ? ? 0F 85 ? ? ? ? BA ? 00", function(ptr) GPointers.GameState = ptr:add(0x2):rip():add(0x1) end), MemoryBatch.new("ScreenResolution", "75 39 0F 57 C0 F3 0F 2A 05", function(ptr) - GPointers.ScreenResolution = vec2:new( - ptr:add(0x5):add(0x4):rip():get_word(), - ptr:add(0x1E):add(0x4):rip():get_word() - ) + GPointers.ScreenResolution.x = ptr:add(0x5):add(0x4):rip():get_word() + GPointers.ScreenResolution.y = ptr:add(0x1E):add(0x4):rip():get_word() end), }, [Enums.eAPIVersion.L54] = { --[[dummy]] }, diff --git a/SSV2/includes/data/refs.lua b/SSV2/includes/data/refs.lua index a963396..b19474c 100644 --- a/SSV2/includes/data/refs.lua +++ b/SSV2/includes/data/refs.lua @@ -252,19 +252,19 @@ return { }, locales = { - { name = "English", id = 0, iso = "en-US" }, - { name = "French", id = 1, iso = "fr-FR" }, - { name = "German", id = 2, iso = "de-DE" }, - { name = "Italian", id = 3, iso = "it-IT" }, - { name = "Spanish, Spain", id = 4, iso = "es-ES" }, - { name = "Portugese", id = 5, iso = "pt-BR" }, - { name = "Polish", id = 6, iso = "pl-PL" }, - { name = "Russian", id = 7, iso = "ru-RU" }, - { name = "Korean", id = 8, iso = "ko-KR" }, - { name = "Chinese Traditional", id = 9, iso = "zh-TW" }, - { name = "Japanese", id = 10, iso = "ja-JP" }, - { name = "Spanish, Mexico", id = 11, iso = "es-MX" }, - { name = "Chinese Simplified", id = 12, iso = "zh-CN" }, + [0] = { name = "English", iso = "en-US" }, + [1] = { name = "French", iso = "fr-FR" }, + [2] = { name = "German", iso = "de-DE" }, + [3] = { name = "Italian", iso = "it-IT" }, + [4] = { name = "Spanish, Spain", iso = "es-ES" }, + [5] = { name = "Portugese", iso = "pt-BR" }, + [6] = { name = "Polish", iso = "pl-PL" }, + [7] = { name = "Russian", iso = "ru-RU" }, + [8] = { name = "Korean", iso = "ko-KR" }, + [9] = { name = "Chinese Traditional", iso = "zh-TW" }, + [10] = { name = "Japanese", iso = "ja-JP" }, + [11] = { name = "Spanish, Mexico", iso = "es-MX" }, + [12] = { name = "Chinese Simplified", iso = "zh-CN" }, }, engineSwaps = { diff --git a/SSV2/includes/features/EntityForge.lua b/SSV2/includes/features/EntityForge.lua index 13c98c1..9d9da42 100644 --- a/SSV2/includes/features/EntityForge.lua +++ b/SSV2/includes/features/EntityForge.lua @@ -62,6 +62,7 @@ function EntityForge:init() ThreadManager:RegisterLooped("SS_ENTITY_FORGE", function() if (not instance.EntityGunEnabled or not WEAPON.IS_PED_ARMED(LocalPlayer:GetHandle(), 4)) then + yield() return end diff --git a/SSV2/includes/features/YimActionsV3.lua b/SSV2/includes/features/YimActionsV3.lua index 33ea094..7601f56 100644 --- a/SSV2/includes/features/YimActionsV3.lua +++ b/SSV2/includes/features/YimActionsV3.lua @@ -129,7 +129,6 @@ function YimActions:IsPlayerBusy() or LocalPlayer:IsRagdoll() or Game.IsInNetworkTransition() or Backend:IsPlayerSwitchInProgress() - or Backend:AreControlsDisabled() end ---@param ped? integer @@ -631,6 +630,10 @@ function YimActions:GoofyUnaliveAnim() end function YimActions:OnKeyDown() + if (Backend:AreControlsDisabled()) then + return + end + if (KeyManager:IsKeybindJustPressed("stop_anim")) then ThreadManager:Run(function() local timer = Timer.new(1000) diff --git a/SSV2/includes/features/vehicle/flappy_doors.lua b/SSV2/includes/features/vehicle/flappy_doors.lua index bc99043..612f3a5 100644 --- a/SSV2/includes/features/vehicle/flappy_doors.lua +++ b/SSV2/includes/features/vehicle/flappy_doors.lua @@ -7,14 +7,15 @@ -- * Provide a copy of or a link to the original license (GPL-3.0 or later); see LICENSE.md or . -local FeatureBase = require("includes.modules.FeatureBase") +local FeatureBase = require("includes.modules.FeatureBase") local StateMachine = require("includes.structs.StateMachine") + ---@class FlappyDoors : FeatureBase ---@field private m_entity PlayerVehicle ---@field private m_is_active boolean ---@field private m_state_machine StateMachine -local FlappyDoors = setmetatable({}, FeatureBase) +local FlappyDoors = setmetatable({}, FeatureBase) FlappyDoors.__index = FlappyDoors ---@param pv PlayerVehicle diff --git a/SSV2/includes/features/vehicle/misc_vehicle.lua b/SSV2/includes/features/vehicle/misc_vehicle.lua index 58f4bb0..e82f6d0 100644 --- a/SSV2/includes/features/vehicle/misc_vehicle.lua +++ b/SSV2/includes/features/vehicle/misc_vehicle.lua @@ -132,7 +132,7 @@ function MiscVehicle:UpdateMachineGuns() return end - if (not LocalPlayer:IsUsingAirctaftMG()) then + if (not LocalPlayer:IsUsingAircraftMG()) then return end diff --git a/SSV2/includes/frontend/self/self_ui.lua b/SSV2/includes/frontend/self/self_ui.lua index c209049..8be57da 100644 --- a/SSV2/includes/frontend/self/self_ui.lua +++ b/SSV2/includes/frontend/self/self_ui.lua @@ -30,11 +30,8 @@ local playerAbilitiesWindow = { local function CheckIfRagdollBlocked() ThreadManager:Run(function() - if (LocalPlayer:IsOnFoot() and not PED.CAN_PED_RAGDOLL(LocalPlayer:GetHandle())) then - Notifier:ShowWarning( - "Samurais Scripts", - _T("SELF_RAGDOLL_BLOCK_INFO") - ) + if (LocalPlayer:IsOnFoot() and not LocalPlayer:IsRagdoll() and not LocalPlayer:CanRagdoll()) then + Notifier:ShowWarning("Samurais Scripts", _T("SELF_RAGDOLL_BLOCK_INFO")) end end) end diff --git a/SSV2/includes/frontend/settings/debug_ui.lua b/SSV2/includes/frontend/settings/debug_ui.lua index 00d0ccf..acd2076 100644 --- a/SSV2/includes/frontend/settings/debug_ui.lua +++ b/SSV2/includes/frontend/settings/debug_ui.lua @@ -181,7 +181,8 @@ local function DrawThreads() end elseif (thread_state == eThreadState.DEAD) then if GUI:Button("Start", { size = side_button_size }) then - ThreadManager:StartThread(thread_name) + ---@diagnostic disable-next-line: invisible + ThreadManager:RestartThread(thread_name) end end end @@ -409,8 +410,26 @@ local function DrawTranslatorDebug() ImGui.TextDisabled("You can switch between available languages in Settings -> General.") ImGui.Spacing() - if GUI:Button("Reload Translator") then - Translator:Reload() + ImGui.BulletText(_F("Language Name: %s", GVars.backend.language_name)) + ImGui.BulletText(_F("ISO: %s", GVars.backend.language_code)) + ImGui.BulletText(_F("Index: %d", GVars.backend.language_index)) + + ImGui.Spacing() + + if (GUI:Button("Reload")) then + Translator.wants_reload = true + end + + if (GUI:Button("Dump Labels")) then + print(Translator.labels) + end + + if (GUI:Button("Dump Locales")) then + print(Translator.locales) + end + + if (GUI:Button("Dump Cache")) then + print(Translator:GetCache()) end end @@ -536,6 +555,8 @@ local function DrawMiscTests() local level = math.random(0, 3) Notifier:Add(label, string.random(), level) end + + Notifier:Add("Callable", string.random(), 0, { callback = function() print("notification callback") end }) end if (ImGui.Button("Dump CWeaponInfo")) then @@ -546,8 +567,8 @@ local function DrawMiscTests() return end - local cweaponinfo = cpedweaponmgr.m_weapon_info - if (not cweaponinfo) then + local cweaponinfo = cpedweaponmgr:GetWeaponInfo() + if not (cweaponinfo and cweaponinfo:IsValid()) then print("CWeaponInfo: invalid pointer.") return end diff --git a/SSV2/includes/frontend/settings/settings_ui.lua b/SSV2/includes/frontend/settings/settings_ui.lua index 63a52b5..24d478a 100644 --- a/SSV2/includes/frontend/settings/settings_ui.lua +++ b/SSV2/includes/frontend/settings/settings_ui.lua @@ -7,10 +7,12 @@ -- * Provide a copy of or a link to the original license (GPL-3.0 or later); see LICENSE.md or . -local ThemeManager = require("includes.services.ThemeManager") -local selectedTheme = ThemeManager:GetCurrentTheme() -local newThemeBuff = selectedTheme:Copy() -local cfgReset = { +local ThemeManager = require("includes.services.ThemeManager") +local LOCALES = Translator.locales +local selectedTheme +local newThemeBuff + +local cfgReset = { ---@type Set exceptions = Set.new("backend.debug_mode"), excToggles = { @@ -23,14 +25,13 @@ local cfgReset = { }, open = false, } -local themeEditor = { +local themeEditor = { shouldDraw = false, liveEdit = false, shouldFocusName = false, valid = true, errors = {} } -newThemeBuff.Name = "" local function onConfigReset() for _, v in pairs(cfgReset.excToggles) do @@ -44,29 +45,38 @@ local function drawGeneralSettings() GVars.backend.auto_cleanup_entities = GUI:CustomToggle(_T("SETTINGS_ENTITY_REPLACE"), GVars.backend.auto_cleanup_entities ) - GUI:Tooltip(_T("SETTINGS_ENTITY_REPLACE_TT")) + GUI:HelpMarker(_T("SETTINGS_ENTITY_REPLACE_TT")) - ImGui.Spacing() - ImGui.BulletText(_F("%s: %s (%s)", _T("SETTINGS_LANGUAGE"), GVars.backend.language_name, GVars.backend.language_code)) - ImGui.Spacing() + if (Translator and Translator:IsReady()) then + GUI:HeaderText(_T("SETTINGS_LANGUAGE"), { separator = true, spacing = true }) - if ImGui.BeginCombo("##langs", _F("%s (%s)", - Translator.locales[GVars.backend.language_index].name, - Translator.locales[GVars.backend.language_index].iso - )) then - for i, lang in ipairs(Translator.locales) do - local is_selected = (i == GVars.backend.language_index) - if (ImGui.Selectable(_F("%s (%s)", lang.name, lang.iso), is_selected)) then - GVars.backend.language_index = i - GVars.backend.language_name = lang.name - GVars.backend.language_code = lang.iso + ImGui.BeginDisabled(not Translator:CanReload()) + GVars.backend.use_game_language = GUI:CustomToggle(_T("SETTINGS_GAME_LANGUAGE"), + GVars.backend.use_game_language, + { onClick = function() Translator.wants_reload = true end, } + ) + ImGui.EndDisabled() + GUI:HelpMarker(_T("SETTINGS_GAME_LANGUAGE_TT")) + + ImGui.Spacing() + ImGui.BeginDisabled(GVars.backend.use_game_language) + if (ImGui.BeginCombo("##langs", _F("%s (%s)", GVars.backend.language_name or "English", GVars.backend.language_code or "en-US"))) then + for i, lang in ipairs(LOCALES) do + local idx = GVars.backend.language_index + local is_selected = (i == idx) + if (ImGui.Selectable(_F("%s (%s)", lang.name, lang.iso), is_selected)) then + GVars.backend.language_index = i + GVars.backend.language_name = lang.name + GVars.backend.language_code = lang.iso + end end + ImGui.EndCombo() end - ImGui.EndCombo() + ImGui.EndDisabled() end ImGui.Spacing() - + ImGui.Separator() if ImGui.Button(_T("SETTINGS_CFG_RESET")) then cfgReset.open = true end @@ -105,11 +115,10 @@ local function drawGeneralSettings() end, function() cfgReset.exceptions:Clear() onConfigReset() - end) + end, true) ImGui.End() end end - ImGui.Dummy(1, 10) end local function drawThemeSettings() @@ -119,7 +128,6 @@ local function drawThemeSettings() if (ImGui.Begin("##new_theme", ImGuiWindowFlags.NoTitleBar - | ImGuiWindowFlags.NoMove | ImGuiWindowFlags.NoResize | ImGuiWindowFlags.AlwaysAutoResize )) then @@ -148,7 +156,7 @@ local function drawThemeSettings() newThemeBuff.Name = "" themeEditor.shouldFocusName = false end - newThemeBuff.Name, _ = ImGui.InputText(_T("SETTINGS_NEW_THEME_NAME"), newThemeBuff.Name, 128) + newThemeBuff.Name, _ = ImGui.InputText(_T("SETTINGS_NEW_THEME_NAME"), newThemeBuff.Name, 128) Backend.disable_input = ImGui.IsItemActive() ImGui.Spacing() @@ -205,8 +213,9 @@ local function drawThemeSettings() else themeEditor.valid, themeEditor.errors = newThemeBuff:ValidateVisibility() if (themeEditor.valid) then - ThemeManager:AddNewTheme(newThemeBuff:Copy()) - selectedTheme = newThemeBuff:Copy() + local themeCopy = newThemeBuff:Copy() + ThemeManager:AddNewTheme(themeCopy, themeEditor.liveEdit) + selectedTheme = themeCopy themeEditor.shouldDraw = false newThemeBuff:Clear() else @@ -306,7 +315,8 @@ local function drawGuiSettings() ImGui.PopStyleVar() GUI:ShowWindowHeightLimit() end, - ImGui.CloseCurrentPopup + ImGui.CloseCurrentPopup, + true ) ImGui.EndPopup() end @@ -346,14 +356,18 @@ local function drawGuiSettings() end ImGui.BeginDisabled() - GVars.ui.window_pos.x, _ = ImGui.SliderFloat(_T("SETTINGS_WINDOW_POS_X"), GVars.ui.window_pos.x, 0, resolution.x) - GUI:Tooltip(_T("SETTINGS_WINDOW_POS_TT")) - GVars.ui.window_pos.y, _ = ImGui.SliderFloat(_T("SETTINGS_WINDOW_POS_Y"), GVars.ui.window_pos.y, 0, resolution.y) + GVars.ui.window_pos.x = ImGui.SliderFloat(_T("SETTINGS_WINDOW_POS_X"), GVars.ui.window_pos.x, 0, resolution.x) + ImGui.EndDisabled() GUI:Tooltip(_T("SETTINGS_WINDOW_POS_TT")) + + ImGui.BeginDisabled() + GVars.ui.window_pos.y = ImGui.SliderFloat(_T("SETTINGS_WINDOW_POS_Y"), GVars.ui.window_pos.y, 0, resolution.y) ImGui.EndDisabled() + GUI:Tooltip(_T("SETTINGS_WINDOW_POS_TT")) ImGui.Spacing() GUI:HeaderText(_T("SETTINGS_WINDOW_STYLE"), { separator = true }) + selectedTheme = selectedTheme or ThemeManager:GetCurrentTheme() GVars.ui.style.bg_alpha, _ = ImGui.SliderFloat(_T("SETTINGS_WINDOW_ALPHA"), GVars.ui.style.bg_alpha, 0.01, 1.0) ImGui.SameLine() @@ -363,9 +377,9 @@ local function drawGuiSettings() for _, theme in pairs(ThemeManager:GetAllThemes()) do local name = theme.Name or "" local is_selected = selectedTheme and selectedTheme.Name == name or false - local is_json = theme.JSON or false + local is_json = theme.JSON == true if (is_json) then - name = _F("[*] %s", name) + name = _F("{..} %s", name) end if (ImGui.Selectable(name, is_selected)) then @@ -383,8 +397,13 @@ local function drawGuiSettings() if (ImGui.BeginPopup(name)) then if (ImGui.MenuItem(_T("GENERIC_DELETE"))) then + if (theme.Name == (ThemeManager:GetCurrentTheme()).Name) then + local default = ThemeManager:GetDefaultTheme() + ThemeManager:SetCurrentTheme(default) + selectedTheme = default + end + ThemeManager:RemoveTheme(theme) - selectedTheme = ThemeManager:GetCurrentTheme() end ImGui.EndPopup() end diff --git a/SSV2/includes/frontend/vehicle/stancer_ui.lua b/SSV2/includes/frontend/vehicle/stancer_ui.lua index c3f1e66..7568107 100644 --- a/SSV2/includes/frontend/vehicle/stancer_ui.lua +++ b/SSV2/includes/frontend/vehicle/stancer_ui.lua @@ -320,7 +320,7 @@ return function() end end, function() saved_vehs_window.should_draw = false - end) + end, true) ImGui.End() end diff --git a/SSV2/includes/frontend/vehicle/vehicle_ui.lua b/SSV2/includes/frontend/vehicle/vehicle_ui.lua index eda76c4..c85e26f 100644 --- a/SSV2/includes/frontend/vehicle/vehicle_ui.lua +++ b/SSV2/includes/frontend/vehicle/vehicle_ui.lua @@ -15,7 +15,7 @@ local customPaintsUI = require("includes.frontend.vehicle.custom_paints_ui") local engine_swap_index = 1 local vehicleTab = GUI:RegisterNewTab(Enums.eTabID.TAB_VEHICLE, "SUBTAB_CARS", nil, nil, true) local handlingEditorTab = vehicleTab:RegisterSubtab("SUBTAB_HANDLING_EDITOR", nil, nil, true) -local optionWindowFlgs = ImGuiWindowFlags.NoTitleBar | ImGuiWindowFlags.AlwaysAutoResize | ImGuiWindowFlags.NoMove +local optionWindowFlgs = ImGuiWindowFlags.NoTitleBar | ImGuiWindowFlags.AlwaysAutoResize DriftMinigame = LocalPlayer:GetVehicle():AddFeature(driftMG) ---@type WindowRequest diff --git a/SSV2/includes/init.lua b/SSV2/includes/init.lua index 86bce62..694f664 100644 --- a/SSV2/includes/init.lua +++ b/SSV2/includes/init.lua @@ -24,16 +24,20 @@ local SCRIPT_NAME = "Samurai's Scripts" local SCRIPT_VERSION = require("includes.version") local DEFAULT_CONFIG = require("includes.data.config") + ---@type GAME_VERSION -local GAME_VERSION = { - { build = "3788.0", online = "1.72" }, - { build = "1013.29", online = "1.72" }, +local GAME_VERSION = { + [1] = { build = "3788.0", online = "1.72" }, + [2] = { build = "1013.29", online = "1.72" }, + [99] = { build = "any", online = "any" }, } + -- ### Enums Namespace. -- -- All enums are stored here to avoid polluting the global namespace. -Enums = require("includes.data.enums.__init__") +Enums = require("includes.data.enums.__init__") + -- ### Backend Module -- @@ -42,7 +46,8 @@ Enums = require("includes.data.enums.__init__") -- It handles API/environment detection, cleanup logic, entity and blip tracking, etc. -- -- This is the core system that ensures safe, predictable behavior when switching sessions, reloading scripts, or shutting down. -Backend = require("includes.backend"):init(SCRIPT_NAME, SCRIPT_VERSION, GAME_VERSION) +Backend = require("includes.backend"):init(SCRIPT_NAME, SCRIPT_VERSION, GAME_VERSION) + require("includes.lib.types") require("includes.lib.utils") @@ -63,13 +68,15 @@ require("includes.modules.Accessor") -- -- For temporary or internal state that should not be saved, use `_G` directly. ---@class GVars : Config -GVars = {} +GVars = {} + ---------------------------------------------------------------------------------------------------- -- These services must be loaded before any class that registers with/uses them ------------------- ThreadManager = require("includes.services.ThreadManager"):init() Serializer = require("includes.services.Serializer"):init("ssv2", DEFAULT_CONFIG, GVars) + -- These may look out of place, but they register themselves with Serializer for seamless -- -- object serialization and deserialization. They are also needed in the next batch of @@ -80,14 +87,17 @@ require("includes.classes.Vector3") require("includes.classes.Vector4") require("includes.modules.Color") -GPointers = require("includes.data.pointers") -Memory = require("includes.modules.Memory") -KeyManager = require("includes.services.KeyManager"):init() -GUI = require("includes.services.GUI") -Notifier = require("includes.services.ToastNotifier").new() -CommandExecutor = require("includes.services.CommandExecutor"):init() + +GPointers = require("includes.data.pointers") +Memory = require("includes.modules.Memory") +KeyManager = require("includes.services.KeyManager"):init() +GUI = require("includes.services.GUI") +Notifier = require("includes.services.ToastNotifier").new() +CommandExecutor = require("includes.services.CommandExecutor"):init() +Translator = require("includes.services.Translator") ---------------------------------------------------------------------------------------------------- + ----------------- Big Features (for smaller features, refer to includes/features) ------------------ BillionaireServices = require("includes.features.BillionaireServicesV2"):init() EntityForge = require("includes.features.EntityForge"):init() @@ -95,12 +105,10 @@ YimActions = require("includes.features.YimActionsV3"):init() YRV3 = require("includes.features.YimResupplierV3"):init() ---------------------------------------------------------------------------------------------------- -local base_path = "includes" -local packages = { - "data.refs", - "data.weapons", - "structs.StateMachine", +local base_path = "includes" +local packages = { + "data.refs", "modules.Audio", "modules.Decorator", @@ -112,7 +120,6 @@ local packages = { "modules.LocalPlayer", "services.GridRenderer", - "services.Translator", "frontend.entity_forge_ui", "frontend.bsv2_ui", diff --git a/SSV2/includes/lib/compat.lua b/SSV2/includes/lib/compat.lua index e77c890..e906238 100644 --- a/SSV2/includes/lib/compat.lua +++ b/SSV2/includes/lib/compat.lua @@ -15,7 +15,7 @@ Compat.__index = Compat ---@param version eAPIVersion function Compat.SetupEnv(version) if (version == Enums.eAPIVersion.L54) then - require("includes.lib.mock_env") + require("includes.lib.mock_env").Setup(version) else print = function(...) local out = {} diff --git a/SSV2/includes/lib/imgui_ext.lua b/SSV2/includes/lib/imgui_ext.lua index ff29df6..b5eb38e 100644 --- a/SSV2/includes/lib/imgui_ext.lua +++ b/SSV2/includes/lib/imgui_ext.lua @@ -120,10 +120,10 @@ end ---@param text string ---@param width float ----@return string +---@return string, boolean function ImGui.TrimTextToWidth(text, width) if (ImGui.CalcTextSize(text) < width) then - return text + return text, false end local ellipsis = "." @@ -143,7 +143,7 @@ function ImGui.TrimTextToWidth(text, width) trimmed = trimmed:sub(1, -2) end - return _F("%s%s", trimmed, ellipsis) + return _F("%s%s", trimmed, ellipsis), true end ---@param text string diff --git a/SSV2/includes/lib/mock_env.lua b/SSV2/includes/lib/mock_env.lua index 13205c2..7129d31 100644 --- a/SSV2/includes/lib/mock_env.lua +++ b/SSV2/includes/lib/mock_env.lua @@ -9,149 +9,147 @@ ---@diagnostic disable: duplicate-set-field -if (not Backend or not Backend:IsMockEnv()) then - return -end - -if (not log) then - local logger = require("includes.modules.Logger").new("Samurai's Scripts", { - level = "debug", - use_colors = true, - file = "./cout.log", - max_size = 1024 * 500 - }) +local MockEnv = {} +MockEnv.__index = MockEnv - local levels = { "debug", "info", "warning" } +function MockEnv.Setup(version) + if (version ~= Enums.eAPIVersion.L54) then + return + end - ---@class log - log = {} + if (not io["exists"]) then + io.exists = function(filepath) + local ok, f = pcall(io.open, filepath, "r") + if not ok or not f then + return false + end - for _, level in ipairs(levels) do - log[level] = function(data) - logger:log(level, data) + f:close() + return true end + end - local flevel = "f" .. level - log[flevel] = function(fmt, ...) - logger:logf(level, fmt, ...) - end + local m_os_rename = os.rename + os.rename = function(oldname, newname) + if (io.exists(newname)) then os.remove(newname) end + m_os_rename(oldname, newname) end -end -if (not io["exists"]) then - io.exists = function(filepath) - local ok, f = pcall(io.open, filepath, "r") - if not ok or not f then - return false - end + if (not log) then + local logger = require("includes.modules.Logger").new("SSV2", { + level = "debug", + use_colors = true, + file = "./ssv2.log", + max_size = 1024 * 500 + }) - f:close() - return true - end -end + local levels = { "debug", "info", "warning" } -if (not script) then - script = { - register_looped = function(name, fn) - print("[mock script looped]", name) - fn({ sleep = function() end, yield = function() end }) - end, - run_in_fiber = function(fn) - fn({ sleep = function() end, yield = function() end }) - end, - - is_active = function(scr_name) - print("[mock script active check]", scr_name) - return false - end - } -end + ---@class log + log = {} + + for _, level in ipairs(levels) do + log[level] = function(data) + logger:log(level, data) + end -if (not event) then - event = { - register_handler = function(evt, fn) - print("[mock event]", evt) - return fn + local flevel = "f" .. level + log[flevel] = function(fmt, ...) + logger:logf(level, fmt, ...) + end end - } -end + end -if (not menu_event) then - menu_event = { - playerLeave = 1, - playerJoin = 2, - playerMgrInit = 3, - playerMgrShutdown = 4, - ChatMessageReceived = 5, - ScriptedGameEventReceived = 6, - MenuUnloaded = 7, - ScriptsReloaded = 8, - Wndproc = 9 - } -end + if (not script) then + script = { + register_looped = NOP, + run_in_fiber = NOP, + execute_as_script = NOP, + is_active = function(_) return false end, + } + end -if (not memory) then - memory = { - pointer = { - add = function(self, offset) return memory.pointer end, - sub = function(self, offset) return memory.pointer end, - get_byte = function(self) return 0 end, - get_word = function(self) return 0 end, - get_dword = function(self) return 0 end, - get_qword = function(self) return 0 end, - get_int = function(self) return 0 end, - get_float = function(self) return 0.0 end, - get_vec3 = function(self) return vec3:zero() end, - get_address = function(self) return 0x0 end, - get_disp32 = function(self, offset, adjust) return 0 end, - set_address = function(self, address) end, - is_null = function(self) return true end, - is_valid = function(self) return false end - }, - - scan_pattern = function(ida_ptrn) return memory.pointer end, - allocate = function(size) return memory.pointer end, - free = function(ptr) end, - handle_to_ptr = function(handle) return memory.pointer end, - ptr_to_handle = function(ptr) return 0x0 end, - dynamic_call = function(ret_type, arg_types, ptr) end - } -end + if (not event) then + event = { register_handler = NOP } + end -if (not vec3) then - vec3 = {} - ---@param x float - ---@param y float - ---@param z float - ---@return vec3 - function vec3:new(x, y, z) - return setmetatable( - { - x = x or 0, - y = y or 0, - z = z or 0, + if (not menu_event) then + menu_event = { + playerLeave = 1, + playerJoin = 2, + playerMgrInit = 3, + playerMgrShutdown = 4, + ChatMessageReceived = 5, + ScriptedGameEventReceived = 6, + MenuUnloaded = 7, + ScriptsReloaded = 8, + Wndproc = 9 + } + end + + if (not memory) then + memory = { + pointer = { + add = function(self, offset) return memory.pointer end, + sub = function(self, offset) return memory.pointer end, + get_byte = function(self) return 0 end, + get_word = function(self) return 0 end, + get_dword = function(self) return 0 end, + get_qword = function(self) return 0 end, + get_int = function(self) return 0 end, + get_float = function(self) return 0.0 end, + get_vec3 = function(self) return vec3:zero() end, + get_address = function(self) return 0x0 end, + get_disp32 = function(self, offset, adjust) return 0 end, + set_address = function(self, address) end, + is_null = function(self) return true end, + is_valid = function(self) return false end }, - vec3 - ) + + scan_pattern = function(ida_ptrn) return memory.pointer end, + allocate = function(size) return memory.pointer end, + free = function(ptr) end, + handle_to_ptr = function(handle) return memory.pointer end, + ptr_to_handle = function(ptr) return 0x0 end, + dynamic_call = function(ret_type, arg_types, ptr) end + } end -end -if (not gui) then - gui = {} - gui.add_tab = function(name) - print("[mock gui.add_tab]") - return gui + if (not vec3) then + vec3 = {} + ---@param x float + ---@param y float + ---@param z float + ---@return vec3 + function vec3:new(x, y, z) + return setmetatable( + { + x = x or 0, + y = y or 0, + z = z or 0, + }, + vec3 + ) + end end - gui.add_imgui = function(_) end - gui.add_always_draw_imgui = function() - print("[mock gui.add_always_draw_imgui]") + + if (not gui) then + gui = { + add_imgui = NOP, + add_always_draw_imgui = NOP, + override_mouse = NOP, + is_open = function() return false end, + mouse_override = function() return false end, + } + gui.add_tab = function(_) return gui end end -end -if (not STREAMING) then - STREAMING = { - IS_PLAYER_SWITCH_IN_PROGRESS = function() - return false - end - } + if (not STREAMING) then + STREAMING = { IS_PLAYER_SWITCH_IN_PROGRESS = function() return false end, } + end + + if (not stats) then stats = {} end + if (not ImGui) then ImGui = {} end end + +return MockEnv diff --git a/SSV2/includes/lib/translations/__hashmap.json b/SSV2/includes/lib/translations/__hashmap.json index ab6f257..b0c3659 100644 --- a/SSV2/includes/lib/translations/__hashmap.json +++ b/SSV2/includes/lib/translations/__hashmap.json @@ -678,5 +678,9 @@ "YH_TP_FACILITY": 3250104265, "YH_DDAY_FORCE": 2809611945, "YH_DDAY_HELP1": 588208172, - "YH_DDAY_HELP2_FMT": 3603977575 + "YH_DDAY_HELP2_FMT": 3603977575, + "GENERIC_LOADED": 509630758, + "GENERIC_RELOADED": 1138106350, + "SETTINGS_GAME_LANGUAGE": 3879522379, + "SETTINGS_GAME_LANGUAGE_TT": 1188280448 } \ No newline at end of file diff --git a/SSV2/includes/lib/translations/__locales.lua b/SSV2/includes/lib/translations/__locales.lua index 096de2b..cc4bb70 100644 --- a/SSV2/includes/lib/translations/__locales.lua +++ b/SSV2/includes/lib/translations/__locales.lua @@ -1,28 +1,28 @@ --- Copyright (C) 2026 SAMURAI (xesdoog) & Contributors. --- This file is part of Samurai's Scripts. --- --- Permission is hereby granted to copy, modify, and redistribute --- this code as long as you respect these conditions: --- * Credit the owner and contributors. --- * Provide a copy of or a link to the original license (GPL-3.0 or later); see LICENSE.md or . - - --- Only add locales if you have matching files for them under /lib/translations/ otherwise you'll get an error when trying --- --- to select a new language because `require` falls back to `package.searcher` which is disabled in V1's sandbox (don't know about V2 yet). --- --- The error is actually just a warning from the API but just to keep things clean and running smoothly, don't add non-existing locales. -return { - { name = "English", iso = "en-US" }, - { name = "Français", iso = "fr-FR" }, - { name = "Deütsch", iso = "de-DE" }, - { name = "Español", iso = "es-ES" }, - { name = "Italiano", iso = "it-IT" }, - { name = "Português", iso = "pt-BR" }, - { name = "Русский", iso = "ru-RU" }, - { name = "中國人", iso = "zh-TW" }, - { name = "中国人", iso = "zh-CN" }, - { name = "日本語", iso = "ja-JP" }, - { name = "Polski", iso = "pl-PL" }, - { name = "한국인", iso = "ko-KR" }, -} +-- Copyright (C) 2026 SAMURAI (xesdoog) & Contributors. +-- This file is part of Samurai's Scripts. +-- +-- Permission is hereby granted to copy, modify, and redistribute +-- this code as long as you respect these conditions: +-- * Credit the owner and contributors. +-- * Provide a copy of or a link to the original license (GPL-3.0 or later); see LICENSE.md or . + + +-- Only add locales if you have matching files for them under /lib/translations/ otherwise you'll get an error when trying +-- +-- to select a new language because `require` falls back to `package.searcher` which is disabled in V1's sandbox (don't know about V2 yet). +-- +-- The error is actually just a warning from the API but just to keep things clean and running smoothly, don't add non-existing locales. +return { + { name = "English", iso = "en-US" }, + { name = "Français", iso = "fr-FR" }, + { name = "Deütsch", iso = "de-DE" }, + { name = "Español", iso = "es-ES" }, + { name = "Italiano", iso = "it-IT" }, + { name = "Português", iso = "pt-BR" }, + { name = "Русский", iso = "ru-RU" }, + { name = "中國人", iso = "zh-TW" }, + { name = "中国人", iso = "zh-CN" }, + { name = "日本語", iso = "ja-JP" }, + { name = "Polski", iso = "pl-PL" }, + { name = "한국인", iso = "ko-KR" }, +} diff --git a/SSV2/includes/lib/translations/de-DE.lua b/SSV2/includes/lib/translations/de-DE.lua index 9311700..01463d7 100644 --- a/SSV2/includes/lib/translations/de-DE.lua +++ b/SSV2/includes/lib/translations/de-DE.lua @@ -678,5 +678,9 @@ return { ["YH_DDAY_FORCE"] = "Zurücksetzen erzwingen", ["YH_DDAY_HELP1"] = "Diese Methode ist notwendig, wenn Sie Doomsday noch nie als Moderator gespielt haben. Wenn Sie als Gastgeber bereits einige Raubüberfälle gespielt und abgeschlossen haben, überspringen Sie diesen Schritt.", ["YH_FACILITY_NOT_OWNED"] = "Kaufen Sie eine Einrichtung, um auf The Doomsday Heist zuzugreifen.", - ["YH_DDAY_HELP2_FMT"] = "Drücken Sie die Schaltfläche „%s“ und rufen Sie dann Lester an, um alle drei Raubüberfälle für Doomsday abzubrechen." + ["YH_DDAY_HELP2_FMT"] = "Drücken Sie die Schaltfläche „%s“ und rufen Sie dann Lester an, um alle drei Raubüberfälle für Doomsday abzubrechen.", + ["GENERIC_LOADED"] = "Geladen.", + ["GENERIC_RELOADED"] = "Neu geladen.", + ["SETTINGS_GAME_LANGUAGE"] = "Verwenden Sie die Spielsprache", + ["SETTINGS_GAME_LANGUAGE_TT"] = "Verwenden Sie die aktuell ausgewählte Spielsprache." } diff --git a/SSV2/includes/lib/translations/en-US.lua b/SSV2/includes/lib/translations/en-US.lua index 4f47224..879d408 100644 --- a/SSV2/includes/lib/translations/en-US.lua +++ b/SSV2/includes/lib/translations/en-US.lua @@ -113,6 +113,8 @@ return { ["GENERIC_TIME_LEFT"] = "Time Left", ["GENERIC_TP_INTERIOR_ERR"] = "Please go outside first!", ["GENERIC_TP_INVALID_COORDS_ERR"] = "Invalid coordinates!", + ["GENERIC_LOADED"] = "Loaded.", + ["GENERIC_RELOADED"] = "Reloaded.", --#endregion --#region CasinoPacino @@ -624,6 +626,8 @@ return { ["SETTINGS_ENTITY_REPLACE"] = "Auto-Replace Entities", ["SETTINGS_ENTITY_REPLACE_TT"] = "This project has a limit to how many entities you can spawn (peds, vehicles, objects). All features adhere to that limit to prevent entity spam or choking the game. This option allows the script to automatically replace old spawned entities once you reach the limit for a certain entity type and try to spawn a new one.", ["SETTINGS_LANGUAGE"] = "Language", + ["SETTINGS_GAME_LANGUAGE"] = "Use Game Language", + ["SETTINGS_GAME_LANGUAGE_TT"] = "Use the currently selected game language.", ["SETTINGS_TOOLTIPS"] = "Disable Tooltips", ["SETTINGS_UI_SOUND"] = "Disable Sound Feedback", ["SETTINGS_WINDOW_GEOMETRY"] = "Window Geometry", diff --git a/SSV2/includes/lib/translations/es-ES.lua b/SSV2/includes/lib/translations/es-ES.lua index b5715c1..3acfec3 100644 --- a/SSV2/includes/lib/translations/es-ES.lua +++ b/SSV2/includes/lib/translations/es-ES.lua @@ -678,5 +678,9 @@ return { ["YH_FACILITY_NOT_OWNED"] = "Compra una instalación para acceder a The Doomsday Heist.", ["YH_DDAY_FORCE"] = "FORZAR RESET", ["YH_TP_FACILITY"] = "Teletransportarse a la instalación", - ["YH_DDAY_HELP2_FMT"] = "Presiona el botón '%s', luego llama a Lester para cancelar los 3 atracos de Doomsday." + ["YH_DDAY_HELP2_FMT"] = "Presiona el botón '%s', luego llama a Lester para cancelar los 3 atracos de Doomsday.", + ["GENERIC_LOADED"] = "Cargado.", + ["GENERIC_RELOADED"] = "Recargado.", + ["SETTINGS_GAME_LANGUAGE"] = "Usar lenguaje de juego", + ["SETTINGS_GAME_LANGUAGE_TT"] = "Usa el idioma del juego seleccionado actualmente." } diff --git a/SSV2/includes/lib/translations/fr-FR.lua b/SSV2/includes/lib/translations/fr-FR.lua index 54defa2..db43534 100644 --- a/SSV2/includes/lib/translations/fr-FR.lua +++ b/SSV2/includes/lib/translations/fr-FR.lua @@ -678,5 +678,9 @@ return { ["YH_DDAY_HELP2_FMT"] = "Appuyez sur le bouton '%s', puis appelez Lester pour annuler les 3 braquages ​​de Doomsday.", ["YH_FACILITY_NOT_OWNED"] = "Achetez une installation pour accéder à The Doomsday Heist.", ["YH_TP_FACILITY"] = "Téléportation vers l'installation", - ["YH_DDAY_FORCE"] = "FORCER LA RÉINITIALISATION" + ["YH_DDAY_FORCE"] = "FORCER LA RÉINITIALISATION", + ["GENERIC_LOADED"] = "Chargé.", + ["SETTINGS_GAME_LANGUAGE_TT"] = "Utilisez la langue de jeu actuellement sélectionnée.", + ["SETTINGS_GAME_LANGUAGE"] = "Utiliser le langage du jeu", + ["GENERIC_RELOADED"] = "Rechargé." } diff --git a/SSV2/includes/lib/translations/it-IT.lua b/SSV2/includes/lib/translations/it-IT.lua index f3309c6..434a98e 100644 --- a/SSV2/includes/lib/translations/it-IT.lua +++ b/SSV2/includes/lib/translations/it-IT.lua @@ -678,5 +678,9 @@ return { ["YH_FACILITY_NOT_OWNED"] = "Acquista una struttura per accedere al colpo dell'apocalisse.", ["YH_DDAY_HELP2_FMT"] = "Premi il pulsante '%s', quindi chiama Lester per annullare tutti e 3 i colpi per Doomsday.", ["YH_TP_FACILITY"] = "Teletrasportarsi alla struttura", - ["YH_DDAY_FORCE"] = "RESET FORZATO" + ["YH_DDAY_FORCE"] = "RESET FORZATO", + ["GENERIC_LOADED"] = "Caricato.", + ["SETTINGS_GAME_LANGUAGE"] = "Usa la lingua del gioco", + ["GENERIC_RELOADED"] = "Ricaricato.", + ["SETTINGS_GAME_LANGUAGE_TT"] = "Utilizza la lingua di gioco attualmente selezionata." } diff --git a/SSV2/includes/lib/translations/ja-JP.lua b/SSV2/includes/lib/translations/ja-JP.lua index 7723771..3f44776 100644 --- a/SSV2/includes/lib/translations/ja-JP.lua +++ b/SSV2/includes/lib/translations/ja-JP.lua @@ -678,5 +678,9 @@ return { ["YH_DDAY_FORCE"] = "強制リセット", ["YH_DDAY_HELP1"] = "この方法は、ホストとして Doomsday をプレイしたことがない場合に必要です。すでにホストとしてプレイし、いくつかの強盗を完了している場合は、このステップをスキップしてください。", ["YH_FACILITY_NOT_OWNED"] = "施設を購入してThe Doomsday Heistにアクセスしてください。", - ["YH_DDAY_HELP2_FMT"] = "「%s」ボタンを押してから、レスターに電話して、ドゥームズデイの 3 つの強盗をすべてキャンセルしてください。" + ["YH_DDAY_HELP2_FMT"] = "「%s」ボタンを押してから、レスターに電話して、ドゥームズデイの 3 つの強盗をすべてキャンセルしてください。", + ["GENERIC_LOADED"] = "ロードされました。", + ["SETTINGS_GAME_LANGUAGE"] = "ゲーム言語を使用する", + ["GENERIC_RELOADED"] = "リロードされました。", + ["SETTINGS_GAME_LANGUAGE_TT"] = "現在選択されているゲーム言語を使用します。" } diff --git a/SSV2/includes/lib/translations/ko-KR.lua b/SSV2/includes/lib/translations/ko-KR.lua index 9637e82..964a59d 100644 --- a/SSV2/includes/lib/translations/ko-KR.lua +++ b/SSV2/includes/lib/translations/ko-KR.lua @@ -678,5 +678,9 @@ return { ["YH_DDAY_HELP1"] = "Doomsday를 호스트로 플레이한 적이 없는 경우 이 방법이 필요합니다. 이미 호스트로서 일부 습격을 플레이하고 완료했다면 이 단계를 건너뛰세요.", ["YH_DDAY_FORCE"] = "강제 재설정", ["YH_TP_FACILITY"] = "시설로 순간이동", - ["YH_DDAY_HELP2_FMT"] = "'%s' 버튼을 누른 다음 Lester에게 전화하여 Doomsday의 3가지 습격을 모두 취소하세요." + ["YH_DDAY_HELP2_FMT"] = "'%s' 버튼을 누른 다음 Lester에게 전화하여 Doomsday의 3가지 습격을 모두 취소하세요.", + ["GENERIC_LOADED"] = "짐을 실은.", + ["SETTINGS_GAME_LANGUAGE_TT"] = "현재 선택된 게임 언어를 사용합니다.", + ["SETTINGS_GAME_LANGUAGE"] = "게임 언어 사용", + ["GENERIC_RELOADED"] = "다시 로드되었습니다." } diff --git a/SSV2/includes/lib/translations/pl-PL.lua b/SSV2/includes/lib/translations/pl-PL.lua index 1ee2ce9..b9c9960 100644 --- a/SSV2/includes/lib/translations/pl-PL.lua +++ b/SSV2/includes/lib/translations/pl-PL.lua @@ -678,5 +678,9 @@ return { ["YH_DDAY_FORCE"] = "WYMUŚ RESET", ["YH_DDAY_HELP2_FMT"] = "Naciśnij przycisk „%s”, a następnie zadzwoń do Lestera, aby anulował wszystkie 3 napady na Dzień Sądu.", ["YH_DDAY_HELP1"] = "Ta metoda jest konieczna, jeśli nigdy nie grałeś w Doomsday jako gospodarz. Jeśli grałeś już i ukończyłeś kilka napadów jako gospodarz, pomiń ten krok.", - ["YH_TP_FACILITY"] = "Teleportuj się do obiektu" + ["YH_TP_FACILITY"] = "Teleportuj się do obiektu", + ["GENERIC_LOADED"] = "Załadowany.", + ["SETTINGS_GAME_LANGUAGE_TT"] = "Użyj aktualnie wybranego języka gry.", + ["SETTINGS_GAME_LANGUAGE"] = "Użyj języka gry", + ["GENERIC_RELOADED"] = "Załadowano ponownie." } diff --git a/SSV2/includes/lib/translations/pt-BR.lua b/SSV2/includes/lib/translations/pt-BR.lua index 78b831f..49b0680 100644 --- a/SSV2/includes/lib/translations/pt-BR.lua +++ b/SSV2/includes/lib/translations/pt-BR.lua @@ -678,5 +678,9 @@ return { ["YH_TP_FACILITY"] = "Teleporte para a instalação", ["YH_FACILITY_NOT_OWNED"] = "Compre uma instalação para acessar The Doomsday Heist.", ["YH_DDAY_HELP1"] = "Este método é necessário se você nunca jogou Doomsday como anfitrião. Se você já jogou e completou alguns assaltos como anfitrião, pule esta etapa.", - ["YH_DDAY_HELP2_FMT"] = "Pressione o botão '%s' e ligue para Lester para cancelar todos os 3 assaltos do Doomsday." + ["YH_DDAY_HELP2_FMT"] = "Pressione o botão '%s' e ligue para Lester para cancelar todos os 3 assaltos do Doomsday.", + ["GENERIC_LOADED"] = "Carregado.", + ["GENERIC_RELOADED"] = "Recarregado.", + ["SETTINGS_GAME_LANGUAGE_TT"] = "Use o idioma do jogo atualmente selecionado.", + ["SETTINGS_GAME_LANGUAGE"] = "Use a linguagem do jogo" } diff --git a/SSV2/includes/lib/translations/ru-RU.lua b/SSV2/includes/lib/translations/ru-RU.lua index ad2d443..d85fd9c 100644 --- a/SSV2/includes/lib/translations/ru-RU.lua +++ b/SSV2/includes/lib/translations/ru-RU.lua @@ -678,5 +678,9 @@ return { ["YH_DDAY_HELP1"] = "Этот метод необходим, если вы никогда не играли в «Судный день» в качестве ведущего. Если вы уже играли и завершили несколько ограблений в качестве организатора, пропустите этот шаг.", ["YH_DDAY_HELP2_FMT"] = "Нажмите кнопку «%s», затем позвоните Лестеру, чтобы отменить все 3 ограбления Судного дня.", ["YH_TP_FACILITY"] = "Телепортироваться на объект", - ["YH_DDAY_FORCE"] = "ПРИНУДИТЕЛЬНЫЙ СБРОС" + ["YH_DDAY_FORCE"] = "ПРИНУДИТЕЛЬНЫЙ СБРОС", + ["SETTINGS_GAME_LANGUAGE"] = "Используйте язык игры", + ["GENERIC_RELOADED"] = "Перезагрузил.", + ["GENERIC_LOADED"] = "Загружено.", + ["SETTINGS_GAME_LANGUAGE_TT"] = "Используйте выбранный в данный момент язык игры." } diff --git a/SSV2/includes/lib/translations/zh-CN.lua b/SSV2/includes/lib/translations/zh-CN.lua index 24aab2d..73f7a88 100644 --- a/SSV2/includes/lib/translations/zh-CN.lua +++ b/SSV2/includes/lib/translations/zh-CN.lua @@ -678,5 +678,9 @@ return { ["YH_TP_FACILITY"] = "传送到设施", ["YH_DDAY_HELP2_FMT"] = "按“%s”按钮,然后呼叫莱斯特取消世界末日的所有 3 次抢劫。", ["YH_DDAY_HELP1"] = "如果你没有玩过末日主机,这个方法很有必要。如果您已经作为主持人玩过并完成了一些抢劫,请跳过此步骤。", - ["YH_FACILITY_NOT_OWNED"] = "购买设施来访问末日抢劫。" + ["YH_FACILITY_NOT_OWNED"] = "购买设施来访问末日抢劫。", + ["GENERIC_LOADED"] = "已加载。", + ["SETTINGS_GAME_LANGUAGE"] = "使用游戏语言", + ["GENERIC_RELOADED"] = "重新加载。", + ["SETTINGS_GAME_LANGUAGE_TT"] = "使用当前选择的游戏语言。" } diff --git a/SSV2/includes/lib/translations/zh-TW.lua b/SSV2/includes/lib/translations/zh-TW.lua index d81e1b8..a5ac21a 100644 --- a/SSV2/includes/lib/translations/zh-TW.lua +++ b/SSV2/includes/lib/translations/zh-TW.lua @@ -678,5 +678,9 @@ return { ["YH_DDAY_HELP2_FMT"] = "按下“%s”按鈕,然後呼叫萊斯特取消世界末日的所有 3 次搶劫。", ["YH_FACILITY_NOT_OWNED"] = "購買設施造訪末日搶劫。", ["YH_TP_FACILITY"] = "傳送到設施", - ["YH_DDAY_FORCE"] = "強制重置" + ["YH_DDAY_FORCE"] = "強制重置", + ["GENERIC_LOADED"] = "已載入。", + ["GENERIC_RELOADED"] = "重新加載。", + ["SETTINGS_GAME_LANGUAGE_TT"] = "使用目前選擇的遊戲語言。", + ["SETTINGS_GAME_LANGUAGE"] = "使用遊戲語言" } diff --git a/SSV2/includes/lib/types.lua b/SSV2/includes/lib/types.lua index 933e556..7071641 100644 --- a/SSV2/includes/lib/types.lua +++ b/SSV2/includes/lib/types.lua @@ -110,21 +110,25 @@ do new = RET_NULLPTR, } - for k, v in pairs(getmetatable(memory.pointer)) do - if (type(k) ~= "string" or __null_idx[k] or type(v) ~= "function") then - goto continue - end - local sub = k:sub(1, 3) - if (sub == "set") then - __null_idx[k] = NOP - elseif (sub == "get") then - __null_idx[k] = (k == "get_string") and RET_NULL or RET_ZERO - else - __null_idx[k] = v + local sol_ptr_mt = getmetatable(memory.pointer) + if (sol_ptr_mt) then + for k, v in pairs(sol_ptr_mt) do + if (type(k) ~= "string" or __null_idx[k] or type(v) ~= "function") then + goto continue + end + + local sub = k:sub(1, 3) + if (sub == "set") then + __null_idx[k] = NOP + elseif (sub == "get") then + __null_idx[k] = (k == "get_string") and RET_NULL or RET_ZERO + else + __null_idx[k] = v + end + + ::continue:: end - - ::continue:: end local __null_mt = { diff --git a/SSV2/includes/lib/utils.lua b/SSV2/includes/lib/utils.lua index e4fde76..1c83f99 100644 --- a/SSV2/includes/lib/utils.lua +++ b/SSV2/includes/lib/utils.lua @@ -26,6 +26,17 @@ yield = coroutine.yield sleep = Time.Sleep _F = string.format +-- Wrapper for `Translator:Translate` +---@param label string +---@return string +function _T(label) + if (not Translator) then + return label + end + + return Translator:Translate(label) +end + -- Lua version of Bob Jenskins' "Jenkins One At A Time" hash function -- -- https://en.wikipedia.org/wiki/Jenkins_hash_function @@ -102,13 +113,13 @@ function IsInstance(obj, T) local obj_type = type(obj) local T_type = type(T) + local obj_mt = getmetatable(obj) + local T_mt = getmetatable(T) + if (T_type == "string" and T == "pointer" and obj_type == "userdata") then - return (type(obj.rip) == "function") + return (obj_mt and type(obj_mt.rip) == "function" or false) end - local obj_mt = getmetatable(obj) - local T_mt = getmetatable(T) - if (T_type == "table") then if (obj_type == "userdata" and T.__type == "vec3") then return obj_mt and obj_mt.__type == T.__type @@ -1040,10 +1051,11 @@ function table.swap(inTable, outTable) end end ----@param t table +-- Overwrites `this` table with `src` table. +---@param this table ---@param src table ---@param seen? table -function table.overwrite(t, src, seen) +function table.overwrite(this, src, seen) seen = seen or {} if (seen[src]) then return @@ -1051,20 +1063,20 @@ function table.overwrite(t, src, seen) seen[src] = true - for k in pairs(t) do + for k in pairs(this) do if (src[k] == nil) then - t[k] = nil + this[k] = nil end end for k, v in pairs(src) do if (type(v) == "table") then - if (type(t[k]) ~= "table") then - t[k] = {} + if (type(this[k]) ~= "table") then + this[k] = {} end - table.overwrite(t[k], v, seen) + table.overwrite(this[k], v, seen) else - t[k] = v + this[k] = v end end end @@ -1523,6 +1535,16 @@ function string.titlecase(str) end)) end +---@return string +function string.pascalcase(str) + return str:lower():gsub("%_+", " "):gsub("%-+", ""):titlecase():gsub("%s+", "") +end + +---@return string +function string.snakecase(str) + return str:lower():gsub("[%s%-]", "_") +end + ---@param value number|string ---@return string function string.formatint(value) @@ -1570,14 +1592,18 @@ function math.round(n, x) return tonumber(string.format("%." .. (x or 0) .. "f", n)) or 0 end ----@param ... any +---@param ... any Varargs or array of numbers. ---@return number function math.sum(...) local result = 0 - local args = type(...) == "table" and ... or { ... } + local args = type(...) == "table" and ... or { ... } + local __len = #args + if (__len == 0) then + return 0 + end - for i = 1, table.getlen(args) do - if type(args[i]) == "number" then + for i = 1, __len do + if (type(args[i]) == "number") then result = result + args[i] end end @@ -1619,16 +1645,16 @@ local INT_SIZES = { } local INT_INFERENCE = { ["unsigned"] = { - { Cast.AsUint8_t, "uint8_t" }, - { Cast.AsUint16_t, "uint16_t" }, - { Cast.AsUint32_t, "uint32_t" }, - { Cast.AsUint64_t, "uint64_t" }, + { Cast.AsUint8, "uint8_t" }, + { Cast.AsUint16, "uint16_t" }, + { Cast.AsUint32, "uint32_t" }, + { Cast.AsUint64, "uint64_t" }, }, ["signed"] = { - { Cast.AsInt8_t, "int8_t" }, - { Cast.AsInt16_t, "int16_t" }, - { Cast.AsInt32_t, "int32_t" }, - { Cast.AsInt64_t, "int64_t" }, + { Cast.AsInt8, "int8_t" }, + { Cast.AsInt16, "int16_t" }, + { Cast.AsInt32, "int32_t" }, + { Cast.AsInt64, "int64_t" }, } } ---@param n integer @@ -1672,14 +1698,14 @@ function math.lerp(a, b, t) return a + (b - a) * math.clamp(t, 0, 1) end --- Generates a trianguar wave oscillating between 1 and -1 +-- Generates a triangular wave oscillating between 1 and -1 ---@param t number ---@return number function math.tent(t) return 2 * math.abs(2 * (t - math.floor(t + 0.5))) - 1 end --- 3x²-2x² +-- 3x² - 2x² ---@param x number ---@return number function math.smooth_step(x) diff --git a/SSV2/includes/modules/Cast.lua b/SSV2/includes/modules/Cast.lua index ec831de..e13dd88 100644 --- a/SSV2/includes/modules/Cast.lua +++ b/SSV2/includes/modules/Cast.lua @@ -44,12 +44,12 @@ function Cast.new(n) end ---@return uint8_t -function Cast:AsUint8_t() +function Cast:AsUint8() return self.m_value & 0xFF end ---@return int8_t -function Cast:AsInt8_t() +function Cast:AsInt8() local v = self.m_value & 0xFF if (v >= 0x80) then @@ -60,12 +60,12 @@ function Cast:AsInt8_t() end ---@return uint16_t -function Cast:AsUint16_t() +function Cast:AsUint16() return self.m_value & 0xFFFF end ---@return int16_t -function Cast:AsInt16_t() +function Cast:AsInt16() local v = self.m_value & 0xFFFF if (v >= 0x8000) then @@ -76,12 +76,12 @@ function Cast:AsInt16_t() end ---@return uint32_t -function Cast:AsUint32_t() +function Cast:AsUint32() return self.m_value & 0xFFFFFFFF end ---@return int32_t -function Cast:AsInt32_t() +function Cast:AsInt32() local v = self.m_value & 0xFFFFFFFF if (v >= 0x80000000) then @@ -92,16 +92,16 @@ function Cast:AsInt32_t() end ---@return joaat_t -function Cast:AsJoaat_t() +function Cast:AsJoaat() ---@diagnostic disable-next-line: return-type-mismatch - return self:AsUint32_t() + return self:AsUint32() end -- **[NOTE]** Lua numbers are IEEE-754 doubles so this **will lose precision above 2^53**. -- -- V1 does not have `bigint` or an `FFI` lib so this will have to do. ---@return uint64_t -function Cast:AsUint64_t() +function Cast:AsUint64() local lo = self.m_value & 0xFFFFFFFF local hi = math.floor(self.m_value / 0x100000000) & 0xFFFFFFFF @@ -109,8 +109,8 @@ function Cast:AsUint64_t() end ---@return int64_t -function Cast:AsInt64_t() - local u = self:AsUint64_t() +function Cast:AsInt64() + local u = self:AsUint64() if (u >= 0x8000000000000000) then return u - 0x10000000000000000 diff --git a/SSV2/includes/modules/Chrono.lua b/SSV2/includes/modules/Chrono.lua index e1b0e19..081591b 100644 --- a/SSV2/includes/modules/Chrono.lua +++ b/SSV2/includes/modules/Chrono.lua @@ -14,9 +14,6 @@ -------------------------------------- --**Global Singleton.** ---@class Time ----@field TimePoint TimePoint ----@field Timer Timer ----@field DateTime DateTime local Time = { __type = "Time" } Time.__index = Time setmetatable(Time, Time) @@ -320,6 +317,7 @@ end ---@class DateTime ---@field private m_epoch seconds ---@field private m_dt osdate +---@field private m_fmt_warn boolean ---@overload fun(p1: (seconds|osdateparam)?): DateTime local DateTime = { __type = "DateTime" } DateTime.__index = DateTime @@ -359,15 +357,31 @@ function DateTime.Now() return DateTime.new() end +---@return DateTime +function DateTime.Today() + return DateTime.new() +end + ---@param fmt? string ---@return string function DateTime:Format(fmt) + local epoch = self.m_epoch + if (epoch == nil) then + if (not self.m_fmt_warn) then + log.warning("[DateTime]: Calling 'Format' from the DateTime class itself. Please consider creating an instance first.") + self.m_fmt_warn = true + end + + epoch = os.time() + end + ---@diagnostic disable - if (string.isvalid(fmt) and fmt == "*t") then - return os.date("%Y-%m-%d %H:%M:%S", self.m_epoch) + if (fmt == "*t") then + log.warning("[DateTime]: '*t' format is not supported. Format method only returns a string.") + return "" end - return os.date(fmt or "%Y-%m-%d %H:%M:%S", self.m_epoch) + return os.date(fmt or "%Y-%m-%d %H:%M:%S", epoch) ---@diagnostic enable end diff --git a/SSV2/includes/modules/Game.lua b/SSV2/includes/modules/Game.lua index e2cb53e..04cc6a6 100644 --- a/SSV2/includes/modules/Game.lua +++ b/SSV2/includes/modules/Game.lua @@ -37,33 +37,22 @@ Game.__index = Game ---@return VersionInfo function Game.GetVersion() - return Memory:GetGameVersion() + return GPointers.GameVersion end ---@return vec2 function Game.GetScreenResolution() - return Memory:GetScreenResolution() or vec2:zero() + return GPointers.ScreenResolution end ----@return string, string +---@return integer, string, string function Game.GetLanguage() - local lang_iso = "en-US" - local lang_name = "English" - local i_LangID = LOCALIZATION.GET_CURRENT_LANGUAGE() - - for _, _lang in ipairs(Refs.locales) do - if i_LangID == _lang.id then - lang_iso = _lang.iso - lang_name = _lang.name - break - end - end - - if lang_iso == "es-MX" then - lang_iso = "es-ES" - end + local idx = LOCALIZATION.GET_CURRENT_LANGUAGE() + local _t = Refs.locales[idx] + local iso = _t.iso or "en-US" + local name = _t.name or "English" - return lang_iso, lang_name + return idx, iso, name end ---@return float @@ -106,7 +95,6 @@ end ---@return boolean function Game.IsInNetworkTransition() - -- PlayerSwitch is invalid here as it will return true in Single Player return script.is_active("maintransition") end diff --git a/SSV2/includes/modules/LocalPlayer.lua b/SSV2/includes/modules/LocalPlayer.lua index 8d9ea56..70676d3 100644 --- a/SSV2/includes/modules/LocalPlayer.lua +++ b/SSV2/includes/modules/LocalPlayer.lua @@ -195,11 +195,7 @@ end ---@return boolean, hash function LocalPlayer:IsUsingVehicleMG() local veh = self:GetVehiclePlayerIsIn() - if (not veh or not veh:IsValid()) then - return false, 0 - end - - if (not veh:IsWeaponized()) then + if (not veh or not veh:IsValid() or not veh:IsWeaponized()) then return false, 0 end @@ -213,11 +209,11 @@ function LocalPlayer:IsUsingVehicleMG() return false, 0 end - local effectGroup = cvehicleweaponinfo.m_effect_group:get_int() local weaponHash = cvehicleweaponinfo.m_name_hash:get_dword() + local effectGroup = cvehicleweaponinfo.m_effect_group:get_int() -- we specifically want to return zero for the weapon hash if false - if (effectGroup ~= Enums.eWeaponEffectGroup.VehicleMG or weaponHash == 0) then + if (weaponHash == 0 or effectGroup ~= Enums.eWeaponEffectGroup.VehicleMG) then return false, 0 end @@ -228,7 +224,7 @@ end -- -- If true, returns `true` and the `weapon hash`; else returns `false` and `0`. ---@return boolean, hash -function LocalPlayer:IsUsingAirctaftMG() +function LocalPlayer:IsUsingAircraftMG() local veh = self:GetVehiclePlayerIsIn() if (not veh or not veh:IsAircraft()) then return false, 0 @@ -237,15 +233,16 @@ function LocalPlayer:IsUsingAirctaftMG() return self:IsUsingVehicleMG() end +---@return boolean function LocalPlayer:IsBeingArrested() return PLAYER.IS_PLAYER_BEING_ARRESTED(self:GetPlayerID(), true) end -- Teleports local player to the provided coordinates. ---@param where (integer|vec3)? -- [blip ID](https://wiki.rage.mp/wiki/Blips) or vector3 coordinates ----@param keep_vehicle? boolean +---@param keepVehicle? boolean ---@param loadGround? boolean -function LocalPlayer:Teleport(where, keep_vehicle, loadGround) +function LocalPlayer:Teleport(where, keepVehicle, loadGround) ThreadManager:Run(function() if (not self:IsOutside()) then Notifier:ShowError(_T("GENERIC_TELEPORT"), _T("GENERIC_TP_INTERIOR_ERR")) @@ -258,7 +255,7 @@ function LocalPlayer:Teleport(where, keep_vehicle, loadGround) return end - if (not keep_vehicle and not LocalPlayer:IsOnFoot()) then + if (not keepVehicle and not LocalPlayer:IsOnFoot()) then TASK.CLEAR_PED_TASKS_IMMEDIATELY(LocalPlayer:GetHandle()) sleep(50) end @@ -456,7 +453,7 @@ end function LocalPlayer:SetMovementClipset(data, isJson) local mvmtclipset = isJson and data.Name or data.mvmt - script.run_in_fiber(function(s) + ThreadManager:Run(function(s) self:ResetMovementClipsets() s:sleep(100) @@ -524,6 +521,8 @@ Backend:RegisterEventCallbackAll(function() end) ThreadManager:RegisterLooped("SS_PV_HANDLER", function() + yield() + if (LocalPlayer.m_vehicle and LocalPlayer.m_vehicle:IsValid()) then if (LocalPlayer:IsOnFoot()) then LocalPlayer:OnVehicleExit() diff --git a/SSV2/includes/modules/Logger.lua b/SSV2/includes/modules/Logger.lua index 8bb2b3a..7c75ccd 100644 --- a/SSV2/includes/modules/Logger.lua +++ b/SSV2/includes/modules/Logger.lua @@ -7,10 +7,6 @@ -- * Provide a copy of or a link to the original license (GPL-3.0 or later); see LICENSE.md or . -if (not Backend or (Backend:GetAPIVersion() ~= Enums.eAPIVersion.L54)) then - return -end - ---@alias LogLevel string ---|"trace" ---|"debug" @@ -130,13 +126,13 @@ function Logger.new(name, options) options = options or {} local instance = { - name = name or "Logger", - level = LEVELS[options.level or "debug"] or LEVELS.debug, + name = name or "Logger", + level = LEVELS[options.level or "debug"] or LEVELS.debug, use_timestamp = options.use_timestamp ~= false, - use_caller = options.use_caller ~= false, - use_colors = options.use_colors or false, - file_path = options.file or nil, - max_size = options.max_size or nil + use_caller = options.use_caller ~= false, + use_colors = options.use_colors or false, + file_path = options.file or nil, + max_size = options.max_size or nil } return setmetatable(instance, Logger) diff --git a/SSV2/includes/modules/Ped.lua b/SSV2/includes/modules/Ped.lua index 0b7ee17..efcb59f 100644 --- a/SSV2/includes/modules/Ped.lua +++ b/SSV2/includes/modules/Ped.lua @@ -59,6 +59,11 @@ function Ped:IsRagdoll() return PED.IS_PED_RAGDOLL(self:GetHandle()) end +---@return boolean +function Ped:CanRagdoll() + return PED.CAN_PED_RAGDOLL(self:GetHandle()) +end + ---@param radius? number ---@return boolean function Ped:IsInCombat(radius) diff --git a/SSV2/includes/modules/Tab.lua b/SSV2/includes/modules/Tab.lua index adf3c92..350e340 100644 --- a/SSV2/includes/modules/Tab.lua +++ b/SSV2/includes/modules/Tab.lua @@ -15,6 +15,7 @@ ---@ignore ---@class Tab : ClassMeta ---@field private m_name string +---@field private m_id joaat_t ---@field private m_selected_tab_name string ---@field private m_callback? GuiCallback ---@field private m_subtabs? table @@ -34,6 +35,7 @@ function Tab.new(name, drawable, subtabs, isTranslatorLabel) return setmetatable( { m_name = name, + m_id = joaat(name), m_callback = drawable, m_subtabs = subtabs or {}, m_has_error = false, @@ -103,6 +105,15 @@ function Tab:GetName() return self.m_has_translator_label and _T(self.m_name) or self.m_name end +---@return joaat_t +function Tab:GetID() + if (not self.m_id) then + self.m_id = joaat(self.m_name) + end + + return self.m_id +end + ---@return GuiCallback function Tab:GetGUICallback() return self.m_callback @@ -265,6 +276,7 @@ function Tab:Notify(fmt, ...) Notifier:ShowMessage(self:GetName(), msg, false, 5) end +---@parivate function Tab:DrawInternal() if (not self.m_callback) then return @@ -276,9 +288,11 @@ function Tab:DrawInternal() ImGui.Spacing() ImGui.BulletText("Traceback most recent call:") -- not an actual trace because Lua's debug is disabled in this sandbox ImGui.Indent() + ImGui.PushTextWrapPos() ImGui.TextColored(1, 0, 0, 1, self.m_traceback) + ImGui.PopTextWrapPos() ImGui.Unindent() - if (GUI:Button("Copy Trace")) then + if (ImGui.Button("Copy Trace")) then ImGui.SetClipboardText(self.m_traceback) end end @@ -300,27 +314,29 @@ function Tab:Draw() return end - ImGui.BeginTabBar(_F("##sutab_selector%s", self.m_name)) - if (ImGui.BeginTabItem(self:GetName())) then - self:DrawInternal() - ImGui.EndTabItem() - end + local __next = nil + if (ImGui.BeginTabBar("ss_tabs")) then + local __label = _F("%s##%d", self:GetName(), self:GetID()) + if (ImGui.BeginTabItem(__label)) then + self:DrawInternal() + ImGui.EndTabItem() + end - for _, tab in pairs(self.m_subtabs) do - local label = tab:GetName() - if (ImGui.BeginTabItem(label)) then - if (tab:HasSubtabs()) then + for _, tab in pairs(self.m_subtabs) do + local label = _F("%s##%d", tab:GetName(), tab:GetID()) + if (ImGui.BeginTabItem(label)) then + if (tab:HasSubtabs()) then + __next = tab + else + tab:DrawInternal() + end ImGui.EndTabItem() - ImGui.EndTabBar() - tab:Draw() - return end - - tab:DrawInternal() - ImGui.EndTabItem() end + ImGui.EndTabBar() end - ImGui.EndTabBar() + + if (__next) then __next:Draw() end end return Tab diff --git a/SSV2/includes/modules/Vehicle.lua b/SSV2/includes/modules/Vehicle.lua index 3ed0fef..c804c66 100644 --- a/SSV2/includes/modules/Vehicle.lua +++ b/SSV2/includes/modules/Vehicle.lua @@ -1641,7 +1641,7 @@ function Vehicle:SaveToJSON(name) mods = mods } - Serializer:WriteToFile(t, filename) + Serializer:WriteToFile(filename, t) self:notify("Saved vehicle to '%s'", filename) end diff --git a/SSV2/includes/services/GUI.lua b/SSV2/includes/services/GUI.lua index bc14110..ec56f45 100644 --- a/SSV2/includes/services/GUI.lua +++ b/SSV2/includes/services/GUI.lua @@ -17,6 +17,12 @@ local debug_counter = GVars.backend.debug_mode and 7 or 0 local DrawClock = require("includes.frontend.clock") +local mainWindowFlags = ImGuiWindowFlags.NoTitleBar + | ImGuiWindowFlags.NoResize + | ImGuiWindowFlags.NoBringToFrontOnFocus + | ImGuiWindowFlags.NoScrollbar + + ---@class WindowRequest ---@field m_should_draw boolean ---@field m_label string @@ -24,6 +30,7 @@ local DrawClock = require("includes.frontend.clock") ---@field m_flags? integer ---@field m_size? vec2 ---@field m_pos? vec2 +---@field m_error? string ---@enum eTabID Enums.eTabID = { @@ -56,7 +63,7 @@ for _, enum in pairs(Enums.eTabID) do defaultTabs[enum] = { first = "", second = {} } end ---#region GUI + -------------------------------------- -- GUI Class -------------------------------------- @@ -77,37 +84,54 @@ end ---@field private m_sidebar_width number ---@field private m_snap_animator WindowAnimator ---@field private m_notifier_pos vec2 +---@field private m_has_error boolean +---@field private m_traceback? string ---@field protected m_initialized boolean -local GUI = Class("GUI") -GUI.m_main_window_label = "##ss_main_window" -GUI.m_should_draw = false -GUI.m_is_drawing_sidebar = false -GUI.m_cb_window_pos = vec2:zero() -GUI.m_notifier_pos = vec2:zero() -GUI.m_screen_resolution = Game.GetScreenResolution() -GUI.m_sidebar_width = 200 -GUI.m_selected_category = Enums.eTabID.TAB_SELF -GUI.m_tabs = defaultTabs -GUI.m_independent_windows = {} -GUI.m_requested_windows = {} -GUI.m_prev_category_tabs = {} -GUI.m_snap_animator = WindowAnimator() +---@overload fun(): GUI +local GUI = Class("GUI") +---@private +---@return GUI function GUI:init() - if (self.m_initialized) then - return - end + if (self.m_initialized) then return self end + if (_G.GUI) then return _G.GUI end + + return setmetatable({ + m_main_window_label = "##ss_main_window", + m_should_draw = false, + m_has_error = false, + m_is_drawing_sidebar = false, + m_sidebar_width = 200, + m_cb_window_pos = vec2:zero(), + m_notifier_pos = vec2:zero(), + m_screen_resolution = vec2:zero(), + m_selected_category = Enums.eTabID.TAB_SELF, + m_tabs = defaultTabs, + m_independent_windows = {}, + m_requested_windows = {}, + m_prev_category_tabs = {}, + m_snap_animator = WindowAnimator(), + m_dummy_tab = gui.add_tab(Backend.script_name or "Samurai's Scripts"), + m_initialized = true, + ---@diagnostic disable-next-line: param-type-mismatch + }, GUI) +end - ThemeManager:Load() +function GUI:LateInit() + if (not math.is_inrange(GVars.ui.last_tab.tab_id, TABID_MIN, TABID_MAX)) then + GVars.ui.last_tab.tab_id = TABID_MIN + end - gui.add_always_draw_imgui(function() - self:Draw() - end) + local __t = self.m_tabs[GVars.ui.last_tab.tab_id][GVars.ui.last_tab.array_index] + if (not __t) then + GVars.ui.last_tab.array_index = 1 + end - self.m_dummy_tab = gui.add_tab(Backend.script_name or "Samurai's Scripts") - self.m_dummy_tab:add_imgui(function() - self:DrawDummyTab() - end) + self.m_screen_resolution = GPointers.ScreenResolution + self.m_selected_category = GVars.ui.last_tab.tab_id + self.m_selected_tab = self.m_tabs[GVars.ui.last_tab.tab_id][GVars.ui.last_tab.array_index].second + self.m_selected_category_tabs = self.m_tabs[GVars.ui.last_tab.tab_id] + self.m_is_drawing_sidebar = #self.m_selected_category_tabs > 1 KeyManager:RegisterKeybind(GVars.keyboard_keybinds.gui_toggle, function() self:Toggle() @@ -117,32 +141,32 @@ function GUI:init() self:Close() end) - self:LateInit() - self.m_initialized = true -end + ThemeManager:Load() + + self.m_dummy_tab:add_imgui(function() self:DrawDummyTab() end) + gui.add_always_draw_imgui(function() self:Draw() end) -function GUI:LateInit() for _, drawfunc in ipairs(self.m_independent_windows) do gui.add_always_draw_imgui(drawfunc) end - - if (not math.is_inrange(GVars.ui.last_tab.tab_id, TABID_MIN, TABID_MAX)) then - GVars.ui.last_tab.tab_id = 1 - end - - local __t = self.m_tabs[GVars.ui.last_tab.tab_id][GVars.ui.last_tab.array_index] - if (not __t) then - GVars.ui.last_tab.array_index = 1 - end - - self.m_selected_category = GVars.ui.last_tab.tab_id - self.m_selected_tab = self.m_tabs[GVars.ui.last_tab.tab_id][GVars.ui.last_tab.array_index].second - self.m_selected_category_tabs = self.m_tabs[GVars.ui.last_tab.tab_id] - self.m_is_drawing_sidebar = #self.m_selected_category_tabs > 1 end function GUI:Toggle() self.m_should_draw = not self.m_should_draw + + if (self.m_should_draw and self.m_has_error) then + self.m_should_draw = false + + local msg = "[GUI]: Unrecoverable callback error. Please contact a developer." + Notifier:ShowError("GUI", msg, false, 6.0) + + if (self.m_traceback) then + msg = msg .. "\n Traceback: " .. self.m_traceback + end + + log.warning(msg) + end + gui.override_mouse(self.m_should_draw) end @@ -337,7 +361,7 @@ end ---@param desired vec2 function GUI:GetMaxSizeForWindow(desired) - local maxwidth = math.min(desired.x, GVars.ui.window_size.x - 20) + local maxwidth = math.min(desired.x, GVars.ui.window_size.x - 20) local maxheight = math.min(desired.y, GVars.ui.window_size.y - 20) return vec2:new(maxwidth, maxheight) end @@ -390,12 +414,12 @@ local underlineSet = false ---@private function GUI:DrawTopBar() local drawList = ImGui.GetWindowDrawList() - local spacing = 10 + local tabCount = table.getlen(tabIdToString) - 1 local availWidth, _ = ImGui.GetContentRegionAvail() - local elemWidth = 90.0 + local spacing = 10 + local elemWidth = math.min(90.0, availWidth / tabCount) local elemHeight = 40.0 local tabHeight = 55.0 - local tabCount = table.getlen(tabIdToString) - 1 local totalWidth = tabCount * elemWidth + (tabCount - 1) * spacing local startX = (availWidth - totalWidth) * 0.5 local cursorPos = vec2:new(ImGui.GetCursorScreenPos()) @@ -405,14 +429,15 @@ function GUI:DrawTopBar() ImGui.SetCursorPosX(ImGui.GetCursorPosX() + startX) for i = 1, tabCount do - local tabName = _T(Match(i, tabIdToString)) - local selected = (self.m_selected_category == i) + local tabName_orig = _T(Match(i, tabIdToString)) + local selected = (self.m_selected_category == i) + local tabName, clipping = ImGui.TrimTextToWidth(tabName_orig, elemWidth - 10) - cursorPos = vec2:new(ImGui.GetCursorScreenPos()) - local yCenter = cursorPos.y + tabHeight * 0.5 - elemHeight * 0.5 - local elemPos = vec2:new(cursorPos.x, yCenter) - local rect = Rect(elemPos, vec2:new(elemPos.x + elemWidth, elemPos.y + elemHeight)) - local rectSize = rect:GetSize() + cursorPos = vec2:new(ImGui.GetCursorScreenPos()) + local yCenter = cursorPos.y + tabHeight * 0.5 - elemHeight * 0.5 + local elemPos = vec2:new(cursorPos.x, yCenter) + local rect = Rect(elemPos, vec2:new(elemPos.x + elemWidth, elemPos.y + elemHeight)) + local rectSize = rect:GetSize() ImGui.PushID(i) ImGui.InvisibleButton(tabName, rectSize.x, rectSize.y) @@ -420,6 +445,9 @@ function GUI:DrawTopBar() local clicked = ImGui.IsItemClicked() local held = (hovered and KeyManager:IsKeyPressed(eVirtualKeyCodes.VK_LBUTTON)) local rectMod = vec2:new((held or clicked) and 1 or 0, (held or clicked) and 1 or 0) + if (clipping) then + self:Tooltip(tabName_orig) + end if (not underlineSet) then underlineTargetX = rect.min.x @@ -523,6 +551,33 @@ function GUI:DrawTopBar() self.m_cursor_pos = vec2:new(ImGui.GetCursorScreenPos()) end +---@private +---@param yPos float +function GUI:DrawCallbackWindow(yPos) + local cb_window_pos_x = GVars.ui.window_pos.x + if (self.m_is_drawing_sidebar) then + cb_window_pos_x = cb_window_pos_x + self.m_sidebar_width + 10 + end + + if (self.m_selected_tab) then + local fixedWidth = self.m_is_drawing_sidebar and (GVars.ui.window_size.x - self.m_sidebar_width - 10) or GVars.ui.window_size.x + ImGui.SetNextWindowBgAlpha(GVars.ui.style.bg_alpha) + ImGui.SetNextWindowPos(cb_window_pos_x, yPos, ImGuiCond.Always) + ImGui.SetNextWindowSizeConstraints(fixedWidth, 20, fixedWidth, GVars.ui.window_size.y - 10) + if (ImGui.Begin("##ss_callback_window", + ImGuiWindowFlags.NoTitleBar | + ImGuiWindowFlags.NoMove | + ImGuiWindowFlags.NoBringToFrontOnFocus | + ImGuiWindowFlags.AlwaysAutoResize) + ) then + ImGui.PushTextWrapPos(fixedWidth - 10) + self.m_selected_tab:Draw() + ImGui.PopTextWrapPos() + ImGui.End() + end + end +end + ---@private ---@param yPos float function GUI:DrawSideBar(yPos) @@ -552,7 +607,6 @@ function GUI:DrawSideBar(yPos) ImGui.SetNextWindowBgAlpha(GVars.ui.style.bg_alpha) ImGui.SetNextWindowPos(GVars.ui.window_pos.x, yPos, ImGuiCond.Always) ImGui.SetNextWindowSizeConstraints(self.m_sidebar_width, 0, self.m_sidebar_width, GVars.ui.window_size.y) - ThemeManager:PushTheme() if (ImGui.Begin("##ss_side_bar", ImGuiWindowFlags.NoTitleBar | ImGuiWindowFlags.NoResize | @@ -562,87 +616,92 @@ function GUI:DrawSideBar(yPos) ) then for i, pair in ipairs(self.m_selected_category_tabs) do if (pair and pair.second) then - local tab = pair.second + local tab = pair.second local label = tab:GetName() + ImGui.PushID(i) if (ImGui.Selectable2(label, self.m_selected_tab == tab, selectableSize)) then self:PlaySound(self.Sounds.Nav) - self.m_selected_tab = tab + self.m_selected_tab = tab GVars.ui.last_tab.array_index = i end + ImGui.PopID() end end ImGui.End() end - ThemeManager:PopTheme() - end -end - -function GUI:ShowWindowHeightLimit() - local windowFlags = ImGuiWindowFlags.NoTitleBar - | ImGuiWindowFlags.NoResize - | ImGuiWindowFlags.NoBringToFrontOnFocus - | ImGuiWindowFlags.NoScrollbar - | ImGuiWindowFlags.AlwaysAutoResize - | ImGuiWindowFlags.NoBackground - - local color = Color(GVars.ui.style.theme.SSAccent:unpack()) - local topHeight = self:GetMaxTopBarHeight() - local pos = vec2:new(GVars.ui.window_pos.x, GVars.ui.window_pos.y + GVars.ui.window_size.y + topHeight - 10) - ImGui.SetNextWindowSize(GVars.ui.window_size.x + 10, 0) - ImGui.SetNextWindowPos(pos.x - 10, pos.y) - if (ImGui.Begin("##indicator", windowFlags)) then - local ImDrawList = ImGui.GetWindowDrawList() - local cursorPos = vec2:new(ImGui.GetCursorScreenPos()) - local p2 = vec2:new(cursorPos.x + GVars.ui.window_size.x, cursorPos.y) - ImGui.ImDrawListAddLine(ImDrawList, cursorPos.x, cursorPos.y, p2.x, p2.y, color:AsU32(), 3) - ImGui.End() end end -function GUI:Draw() - if (not self.m_should_draw) then - return - end - +---@private +function GUI:OnDrawCallback(fixed_height) if (not gui.mouse_override()) then gui.override_mouse(true) end - local default_size, default_pos = self:GetNewWindowSizeAndCenterPos(0.45, 0.8) - default_pos.y = 1 - - local windowFlags = ImGuiWindowFlags.NoTitleBar - | ImGuiWindowFlags.NoResize - | ImGuiWindowFlags.NoBringToFrontOnFocus - | ImGuiWindowFlags.NoScrollbar - - if (GVars.ui.moveable) then - windowFlags = Bit.Clear(windowFlags, ImGuiWindowFlags.NoMove) - else - windowFlags = Bit.Set(windowFlags, ImGuiWindowFlags.NoMove) - end + local size, pos = self:GetNewWindowSizeAndCenterPos(0.45, 0.8); pos.y = 1 + local bitwise = GVars.ui.moveable and Bit.Clear or Bit.Set + mainWindowFlags = bitwise(mainWindowFlags, ImGuiWindowFlags.NoMove) if (GVars.ui.window_pos:is_zero()) then - ImGui.SetNextWindowPos(default_pos.x, default_pos.y, ImGuiCond.Always) - GVars.ui.window_pos = default_pos + ImGui.SetNextWindowPos(pos.x, pos.y, ImGuiCond.Always) + GVars.ui.window_pos = pos else - ImGui.SetNextWindowPos(default_pos.x, default_pos.y, - GVars.ui.moveable and ImGuiCond.FirstUseEver or ImGuiCond.Always) + local cond = GVars.ui.moveable and ImGuiCond.FirstUseEver or ImGuiCond.Always + ImGui.SetNextWindowPos(pos.x, pos.y, cond) end - local fixed_height = self:GetMaxTopBarHeight() if (GVars.ui.window_size:is_zero()) then - ImGui.SetNextWindowSize(default_size.x, fixed_height, ImGuiCond.Always) - GVars.ui.window_size = default_size + ImGui.SetNextWindowSize(size.x, fixed_height, ImGuiCond.Always) + GVars.ui.window_size = size else ImGui.SetNextWindowSize(GVars.ui.window_size.x, fixed_height, ImGuiCond.Always) end +end + +---@private +function GUI:DrawWindowRequests() + for label, request in pairs(self.m_requested_windows) do + if (not request.m_should_draw) then + goto continue + end + + if (request.m_pos) then + ImGui.SetNextWindowPos(request.m_pos.x, request.m_pos.y, ImGuiCond.Always) + end + + if (request.m_size) then + ImGui.SetNextWindowSize(request.m_size.x, request.m_size.y, ImGuiCond.Always) + end + + ImGui.Begin(label, request.m_flags) + if (request.m_error) then + ImGui.Text(_F("Callback error for requested window '%s':", label)) + ImGui.Indent() + ImGui.PushStyleColor(ImGuiCol.Text, 1, 0, 0, 1) + ImGui.Text(request.m_error) + ImGui.PopStyleColor(1) + ImGui.Unindent() + else + local ok, err = pcall(request.m_callback) + if (not ok) then + request.m_error = type(err) == "string" and err or "Unknown error." + end + end + ImGui.End() + + ::continue:: + end +end + +---@private +function GUI:__DrawImpl() + local topBarHeight = self:GetMaxTopBarHeight() + self:OnDrawCallback(topBarHeight) self.m_snap_animator:Apply() - ThemeManager:PushTheme() ImGui.SetNextWindowBgAlpha(GVars.ui.style.bg_alpha) - if (ImGui.Begin(self.m_main_window_label, windowFlags)) then + if (ImGui.Begin(self.m_main_window_label, mainWindowFlags)) then local fontScale = 1.5 local titleWidth = ImGui.CalcTextSize("Samurai's Scripts") * fontScale local winWidth = ImGui.GetWindowWidth() @@ -687,53 +746,62 @@ function GUI:Draw() if (Notifier) then Notifier:DrawNotifications(self.m_notifier_pos) end - ThemeManager:PopTheme() - local next_y_pos = GVars.ui.window_pos.y + fixed_height + 10 - local cb_window_pos_x = GVars.ui.window_pos.x - if (self.m_is_drawing_sidebar) then - cb_window_pos_x = cb_window_pos_x + self.m_sidebar_width + 10 - end + local next_y_pos = GVars.ui.window_pos.y + topBarHeight + 10 + self:DrawCallbackWindow(next_y_pos) + self:DrawSideBar(next_y_pos) +end - if (self.m_selected_tab) then - local fixedWidth = self.m_is_drawing_sidebar and (GVars.ui.window_size.x - self.m_sidebar_width - 10) or GVars.ui.window_size.x - ImGui.SetNextWindowBgAlpha(GVars.ui.style.bg_alpha) - ImGui.SetNextWindowPos(cb_window_pos_x, next_y_pos, ImGuiCond.Always) - ImGui.SetNextWindowSizeConstraints(fixedWidth, 20, fixedWidth, GVars.ui.window_size.y - 10) - ThemeManager:PushTheme() - if (ImGui.Begin("##ss_callback_window", - ImGuiWindowFlags.NoTitleBar | - ImGuiWindowFlags.NoMove | - ImGuiWindowFlags.NoBringToFrontOnFocus | - ImGuiWindowFlags.AlwaysAutoResize) - ) then - ImGui.PushTextWrapPos(fixedWidth - 10) - self.m_selected_tab:Draw() - ImGui.PopTextWrapPos() - ImGui.End() - end - ThemeManager:PopTheme() +function GUI:Draw() + if (not self.m_should_draw or self.m_has_error) then + return end - self:DrawSideBar(next_y_pos) - - for label, window in pairs(self.m_requested_windows) do - if (window.m_should_draw) then - if (window.m_pos) then - ImGui.SetNextWindowPos(window.m_pos.x, window.m_pos.y, ImGuiCond.Always) - end + local startStack = ThemeManager:GetStackDepth() + ThemeManager:PushTheme() + local ok, err = pcall(function() + self:__DrawImpl() + end) + ThemeManager:PopTheme() - if (window.m_size) then - ImGui.SetNextWindowSize(window.m_size.x, window.m_size.y, ImGuiCond.Always) - end + if (not ok) then + log.fwarning("[GUI]: Unrecoverable callback error: %s", err) + self.m_has_error = true + self.m_traceback = err + end - ThemeManager:PushTheme() - if (ImGui.Begin(label, window.m_flags)) then - window.m_callback() - ImGui.End() - end + local endStack = ThemeManager:GetStackDepth() + if (endStack > startStack) then + while (ThemeManager:GetStackDepth() > startStack) do ThemeManager:PopTheme() end + elseif (endStack < startStack) then + local msg = "[ThemeManager]: stack underflow! Forgot to call PushTheme?" + log.warning(msg) + self.m_has_error = true + self.m_traceback = _F("%s%s", msg, self.m_traceback == nil and "" or self.m_traceback .. "\n\n") + end +end + +function GUI:ShowWindowHeightLimit() + local windowFlags = ImGuiWindowFlags.NoTitleBar + | ImGuiWindowFlags.NoResize + | ImGuiWindowFlags.NoBringToFrontOnFocus + | ImGuiWindowFlags.NoScrollbar + | ImGuiWindowFlags.AlwaysAutoResize + | ImGuiWindowFlags.NoBackground + + local color = Color(GVars.ui.style.theme.SSAccent:unpack()) + local topHeight = self:GetMaxTopBarHeight() + local pos = vec2:new(GVars.ui.window_pos.x, GVars.ui.window_pos.y + GVars.ui.window_size.y + topHeight - 10) + ImGui.SetNextWindowSize(GVars.ui.window_size.x + 10, 0) + ImGui.SetNextWindowPos(pos.x - 10, pos.y) + if (ImGui.Begin("##indicator", windowFlags)) then + local ptr = ImGui.GetWindowDrawList() + local p1 = vec2:new(ImGui.GetCursorScreenPos()) + local p2 = vec2:new(p1.x + GVars.ui.window_size.x, p1.y) + ImGui.ImDrawListAddLine(ptr, p1.x, p1.y, p2.x, p2.y, color:AsU32(), 3) + ImGui.End() end end @@ -813,7 +881,7 @@ function GUI:Tooltip(text, opts) if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled)) then ImGui.SetNextWindowBgAlpha(GVars.ui.style.bg_alpha) ImGui.BeginTooltip() - if IsInstance(opts.color, Color) then + if (opts.color) then self:Text(text, opts) else ImGui.PushTextWrapPos(wrap_pos) @@ -868,10 +936,11 @@ end ---@param label string ---@param callback GuiCallback ---@param onClose function -- Close button callback -function GUI:QuickConfigWindow(label, callback, onClose) +---@param alwaysCenter? boolean +function GUI:QuickConfigWindow(label, callback, onClose, alwaysCenter) local size = vec2:new(ImGui.GetWindowSize()) local _, center = self:GetNewWindowSizeAndCenterPos(0.5, 0.5, size) - ImGui.SetWindowPos(center.x, center.y, ImGuiCond.Once) + ImGui.SetWindowPos(center.x, center.y, alwaysCenter and ImGuiCond.Always or ImGuiCond.Once) ImGui.SeparatorText(label) if (self:Button("Close")) then @@ -892,18 +961,12 @@ end ---@param button GUI.MouseButtons ---@return boolean function GUI:IsItemClicked(button) - if (button == self.MouseButtons.LEFT) then - return (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled) and ImGui.IsItemClicked(0)) - elseif (button == self.MouseButtons.RIGHT) then - return (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled) and ImGui.IsItemClicked(1)) - end - - return false + return (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled) and ImGui.IsItemClicked(button)) end -- Sets the clipboard text. ---@param text string ----@param eval? function +---@param eval? fun(): boolean function GUI:SetClipBoardText(text, eval) if (type(eval) == "function" and not eval()) then return @@ -917,7 +980,7 @@ end -- Plays a sound when an ImGui widget is clicked. ---@param sound string|table function GUI:PlaySound(sound) - if GVars.ui.disable_sound_feedback then + if (GVars.ui.disable_sound_feedback) then return end @@ -933,7 +996,7 @@ end ---@param label string ---@param v boolean ----@param opts? { tooltip?: string, color?: Color, onClick?: fun(v?: boolean) } +---@param opts? { tooltip?: string, color?: Color, onClick?: fun(v?: boolean) } the `color` optional param applies to the optional tooltip, not the toggle. ---@return boolean, boolean function GUI:CustomToggle(label, v, opts) local clicked = false @@ -975,26 +1038,19 @@ function GUI:Checkbox(label, v, opts) end ---@param label string ----@param opts? { size?: vec2, repeatable?: boolean, tooltip?: string, colors?: { button?: Color, buttonHovered?: Color, buttonActive?: Color } } +---@param opts? { size?: vec2, repeatable?: boolean, tooltip?: string, colors?: { Button?: Color, ButtonHovered?: Color, ButtonActive?: Color } } function GUI:Button(label, opts) opts = opts or {} opts.size = opts.size or vec2:zero() local colorStack = 0 if (opts.colors) then - if (opts.colors.button) then - ImGui.PushStyleColor(ImGuiCol.Button, opts.colors.button:AsFloat()) - colorStack = colorStack + 1 - end - - if (opts.colors.buttonHovered) then - ImGui.PushStyleColor(ImGuiCol.ButtonHovered, opts.colors.buttonHovered:AsFloat()) - colorStack = colorStack + 1 - end - - if (opts.colors.buttonActive) then - ImGui.PushStyleColor(ImGuiCol.ButtonActive, opts.colors.buttonActive:AsFloat()) - colorStack = colorStack + 1 + for key, col in pairs(opts.colors) do + local idx = ImGuiCol[key] + if (idx) then + ImGui.PushStyleColor(idx, col:AsFloat()) + colorStack = colorStack + 1 + end end end @@ -1063,75 +1119,75 @@ end --#endregion --#region metadata -GUI.Sounds = { +GUI.Sounds = { Radar = { soundName = "RADAR_ACTIVATE", - soundRef = "DLC_BTL_SECURITY_VANS_RADAR_PING_SOUNDS" + soundRef = "DLC_BTL_SECURITY_VANS_RADAR_PING_SOUNDS" }, Button = { soundName = "SELECT", - soundRef = "HUD_FRONTEND_DEFAULT_SOUNDSET" + soundRef = "HUD_FRONTEND_DEFAULT_SOUNDSET" }, Pickup = { soundName = "PICK_UP", - soundRef = "HUD_FRONTEND_DEFAULT_SOUNDSET" + soundRef = "HUD_FRONTEND_DEFAULT_SOUNDSET" }, Pickup_alt = { soundName = "PICK_UP_WEAPON", - soundRef = "HUD_FRONTEND_CUSTOM_SOUNDSET" + soundRef = "HUD_FRONTEND_CUSTOM_SOUNDSET" }, Fail = { soundName = "CLICK_FAIL", - soundRef = "WEB_NAVIGATION_SOUNDS_PHONE" + soundRef = "WEB_NAVIGATION_SOUNDS_PHONE" }, Click = { soundName = "CLICK_LINK", - soundRef = "DLC_H3_ARCADE_LAPTOP_SOUNDS" + soundRef = "DLC_H3_ARCADE_LAPTOP_SOUNDS" }, Notify = { soundName = "LOSE_1ST", - soundRef = "GTAO_FM_EVENTS_SOUNDSET" + soundRef = "GTAO_FM_EVENTS_SOUNDSET" }, Delete = { soundName = "DELETE", - soundRef = "HUD_DEATHMATCH_SOUNDSET" + soundRef = "HUD_DEATHMATCH_SOUNDSET" }, Cancel = { soundName = "CANCEL", - soundRef = "HUD_FREEMODE_SOUNDSET" + soundRef = "HUD_FREEMODE_SOUNDSET" }, Error = { soundName = "ERROR", - soundRef = "HUD_FREEMODE_SOUNDSET" + soundRef = "HUD_FREEMODE_SOUNDSET" }, Nav = { soundName = "NAV_LEFT_RIGHT", - soundRef = "HUD_FREEMODE_SOUNDSET" + soundRef = "HUD_FREEMODE_SOUNDSET" }, Checkbox = { soundName = "NAV_UP_DOWN", - soundRef = "HUD_FREEMODE_SOUNDSET" + soundRef = "HUD_FREEMODE_SOUNDSET" }, Select_alt = { soundName = "CHANGE_STATION_LOUD", - soundRef = "RADIO_SOUNDSET" + soundRef = "RADIO_SOUNDSET" }, Focus_in = { soundName = "FOCUSIN", - soundRef = "HINTCAMSOUNDS" + soundRef = "HINTCAMSOUNDS" }, Focus_out = { soundName = "FOCUSOUT", - soundRef = "HINTCAMSOUNDS" + soundRef = "HINTCAMSOUNDS" }, } ---@enum GUI.MouseButtons GUI.MouseButtons = { - LEFT = 0x0, + LEFT = 0x0, RIGHT = 0x1 } --#endregion -return GUI +return GUI() diff --git a/SSV2/includes/services/Serializer.lua b/SSV2/includes/services/Serializer.lua index a92f7fd..7991f7c 100644 --- a/SSV2/includes/services/Serializer.lua +++ b/SSV2/includes/services/Serializer.lua @@ -76,10 +76,11 @@ function Serializer:init(script_name, default_config, runtime_vars, varargs) return self end - local timestamp = tostring(os.date("%H_%M_%S")) + local timestamp = DateTime:Now():Format("%H_%M_%S") script_name = script_name or (Backend and Backend.script_name or ("unk_cfg_%s"):format(timestamp)) + + local filename = script_name:snakecase() varargs = varargs or {} - local filename = script_name:lower():gsub("%s+", "_") self.m_default_config = default_config or { __version = Backend and Backend.__version or self.__version } self.m_file_name = _F("%s.json", filename) @@ -452,22 +453,26 @@ function Serializer:Encode(data, etc) return res end +-- Just to avoid creating a closure later +---@private +---@return Config +function Serializer:OnDecodeError() + log.warning("[Serializer]: No suitable config backup. Resetting to default.") + self:Parse(self.m_default_config) + self:SaveBackup() + return self:Postprocess(self.m_default_config) +end + ---@param data any ---@param etc? any ---@return any function Serializer:Decode(data, etc) local ok, res = self:DecodeImpl(data, etc) if (not ok) then - local function onDecodeError() - log.warning("[Serializer]: No suitable config backup. Resetting to default.") - self:Parse(self.m_default_config) - return table.copy(self.m_default_config) - end - log.warning("[Serializer]: Your config file is corrupted! Attempting to recover...") local success, recov = self:RecoverBackup() if (not success) then - return onDecodeError() + return self:OnDecodeError() end if (self:IsEncrypted(recov)) then @@ -476,7 +481,7 @@ function Serializer:Decode(data, etc) ok, res = self:DecodeImpl(recov, etc) if (not ok) then - return onDecodeError() + return self:OnDecodeError() end log.info("[Serializer]: Config backup successfullly recovered.") @@ -485,27 +490,39 @@ function Serializer:Decode(data, etc) return self:Postprocess(res) end +---@private +---@param msg? string +function Serializer:OnParseError(msg) + msg = msg or "Unknown error." + log.fwarning("[Serializer]: Failed to parse config data: %s", msg) + self.m_state = eSerializerState.SUSPENDED + self.m_disabled = true +end + ---@param data any function Serializer:Parse(data) if (self:IsDisabled()) then return end - local f = io.open(self.m_file_name, "w") - if (not f) then - log.warning("[Serializer]: Failed to write config file!") - self.m_disabled = true - return - end - self.m_state = eSerializerState.FLUSHING local json = self:Encode(data) if (not json) then + self:OnParseError("JSON encoding failed.") return end - f:write(json) - f:flush() + local fname = "ss_temp" + local temp = io.open(fname, "w") + if (not temp) then + self:OnParseError("Failed to open file.") + return + end + + temp:write(json) + temp:flush() + temp:close() + os.rename(fname, self.m_file_name) -- I came while writing this line. Thanks for accepting the PR MR-X self.m_state = eSerializerState.IDLE end @@ -540,7 +557,7 @@ end ---@return any function Serializer:ReadItem(item_path) if (type(GVars) ~= "table") then - log.warning("[Serializer]: No runtime variables table! Returning default value.") + log.warning("[Serializer]: No global runtime variables table! Returning default value.") return table.get_nested_key(self.m_default_config, item_path) end @@ -551,7 +568,7 @@ end ---@param value any function Serializer:SaveItem(item_path, value) if (type(GVars) ~= "table") then - log.warning("[Serializer]: No runtime variables table!") + log.warning("[Serializer]: No global runtime variables table!") return end @@ -630,15 +647,13 @@ function Serializer:B64Encode(input) local n = #input for i = 1, n, 3 do - local a = input:byte(i) or 0 - local b = input:byte(i + 1) or 0 - local c = input:byte(i + 2) or 0 - local triple = (a << 16) | (b << 8) | c - + local a = input:byte(i) or 0 + local b = input:byte(i + 1) or 0 + local c = input:byte(i + 2) or 0 + local triple = (a << 16) | (b << 8) | c output[#output + 1] = self.b64_chars:sub(((triple >> 18) & 63) + 1, ((triple >> 18) & 63) + 1) output[#output + 1] = self.b64_chars:sub(((triple >> 12) & 63) + 1, ((triple >> 12) & 63) + 1) - output[#output + 1] = (i + 1 <= n) and self.b64_chars:sub(((triple >> 6) & 63) + 1, ((triple >> 6) & 63) + 1) or - "=" + output[#output + 1] = (i + 1 <= n) and self.b64_chars:sub(((triple >> 6) & 63) + 1, ((triple >> 6) & 63) + 1) or "=" output[#output + 1] = (i + 2 <= n) and self.b64_chars:sub((triple & 63) + 1, (triple & 63) + 1) or "=" end @@ -654,7 +669,7 @@ function Serializer:B64Decode(base64) b64lookup[self.b64_chars:sub(i, i)] = i - 1 end - base64 = base64:gsub("%s", ""):gsub("=", "") + base64 = base64:gsub("%s", ""):gsub("=", "") local output = {} for i = 1, #base64, 4 do @@ -678,13 +693,15 @@ end ---@param input string ---@return string function Serializer:XOR(input) - local output = {} + local output = {} local key_len = #self.m_xor_key + for i = 1, #input do local input_byte = input:byte(i) - local key_byte = self.m_xor_key:byte((i - 1) % key_len + 1) - output[i] = string.char(input_byte ~ key_byte) + local key_byte = self.m_xor_key:byte((i - 1) % key_len + 1) + output[i] = string.char(input_byte ~ key_byte) end + return table.concat(output) end @@ -719,7 +736,7 @@ end ---@param data any ---@param filename string function Serializer:WriteInternal(data, filename) - local f, err = io.open(filename, "w") + local f , err = io.open(filename, "w") if (not f) then log.fwarning("[Serializer]: Failed to open file: %s", err) return @@ -727,15 +744,19 @@ function Serializer:WriteInternal(data, filename) f:write(data) f:flush() - f:close() end -- A separate write function that doesn't rely on any setup or state flags. -- -- Do not use it to write to the Serializer's config file. ----@param data any ---@param filename string -function Serializer:WriteToFile(data, filename) +---@param data any +function Serializer:WriteToFile(filename, data) + if (data == nil) then + log.warning("[Serializer]: Invalid data.") + return + end + if (type(filename) ~= "string" or not filename:endswith(".json")) then log.warning("[Serializer]: Invalid file name.") return @@ -746,12 +767,7 @@ function Serializer:WriteToFile(data, filename) return end - if (not data) then - log.warning("[Serializer]: Invalid data type.") - return - end - - local f, err = io.open(filename, "w") + local f , err = io.open(filename, "w") if (not f) then log.fwarning("[Serializer]: Failed to open file: %s", err) return @@ -759,7 +775,6 @@ function Serializer:WriteToFile(data, filename) f:write(self:Encode(data)) f:flush() - f:close() end -- A separate read function. @@ -774,7 +789,7 @@ function Serializer:ReadFromFile(filename) end if (filename == self.m_file_name) then - log.warning("[Serializer]: Use Serializer:Read() instead to read the Serializer's config file.") + log.warning("[Serializer]: Use Serializer:Read() instead to read the main config file.") return end @@ -787,6 +802,7 @@ function Serializer:ReadFromFile(filename) return self:Decode(f:read("a")) end +-- Rebuilds objects from simple tables. ---@param object table function Serializer:Reconstruct(object) if (type(object) ~= "table") then @@ -809,19 +825,17 @@ function Serializer:Reconstruct(object) end end + local out = {} if (table.is_array(object)) then - local out = {} for i = 1, #object do out[i] = self:Reconstruct(object[i]) end - return out else - local out = {} for k, v in pairs(object) do out[k] = self:Reconstruct(v) end - return out end + return out end function Serializer:FlushObjectQueue() @@ -851,12 +865,13 @@ function Serializer:Flush() end function Serializer:OnTick() + yield() + if (not self.m_initialized or not self:CanAccess()) then return end if (not self.m_dirty and not self.m_last_write_time:HasElapsed(5e3)) then - yield() return end diff --git a/SSV2/includes/services/ThemeManager.lua b/SSV2/includes/services/ThemeManager.lua index 4291da8..7e0291d 100644 --- a/SSV2/includes/services/ThemeManager.lua +++ b/SSV2/includes/services/ThemeManager.lua @@ -14,6 +14,7 @@ local ThemeLibrary = require("includes.data.theme_library") ---@class ThemeManager ---@field private m_current_theme Theme +---@field private m_stack_depth integer ---@field private m_col_stack integer ---@field private m_style_stack integer ---@field private m_theme_library ThemeLibrary @@ -22,6 +23,7 @@ local ThemeManager = { m_current_theme = Theme.new(ThemeLibrary.Tenebris), m_themes_file = "ss_themes.json", m_theme_library = {}, + m_stack_depth = 0, } ThemeManager.__index = ThemeManager @@ -39,17 +41,20 @@ function ThemeManager:Load() if (not current or not current.Colors) then current = self:GetDefaultTheme() - GVars.ui.style.theme = current end if (current.JSON or not current.__type or not IsInstance(current.SSAccent, vec4)) then current = Theme.deserialize(current) - GVars.ui.style.theme = current end + GVars.ui.style.theme = current self.m_current_theme = current end +function ThemeManager:GetStackDepth() + return self.m_stack_depth +end + ---@param name string ---@return Theme? function ThemeManager:GetTheme(name) @@ -82,35 +87,45 @@ function ThemeManager:DoesThemeExist(name) return self:GetTheme(name) ~= nil end ----@return table +---@return boolean +function ThemeManager:IsBackgroundDark() + return ImGui.GetStyleColor(ImGuiCol.WindowBg):IsDark() +end + +---@return table function ThemeManager:ReadThemesJson() if (not io.exists(self.m_themes_file)) then - Serializer:WriteToFile({}, self.m_themes_file) + Serializer:WriteToFile(self.m_themes_file, {}) return {} end + ---@type table local themes = Serializer:ReadFromFile(self.m_themes_file) if (type(themes) ~= "table") then log.warning("Theme data appears to be corrupted! Returning an empty table.") - themes = {} + Serializer:WriteToFile(self.m_themes_file, {}) + return {} end return themes end ---@param theme Theme -function ThemeManager:AddNewTheme(theme) +---@param apply? boolean +function ThemeManager:AddNewTheme(theme, apply) if (self:DoesThemeExist(theme.Name)) then return end - local json_themes = self:ReadThemesJson() - theme.JSON = true - json_themes[theme.Name] = theme - self.m_theme_library[theme.Name] = theme + theme.JSON = true + local lib = self.m_theme_library + local json = self:ReadThemesJson() + local serialized = theme:serialize() + json[theme.Name] = serialized + lib[theme.Name] = theme - Serializer:WriteToFile(json_themes, self.m_themes_file) - self:SetCurrentTheme(theme) + if (apply) then self:SetCurrentTheme(theme) end + Serializer:WriteToFile(self.m_themes_file, json) end ---@param theme Theme @@ -123,11 +138,12 @@ function ThemeManager:RemoveTheme(theme) self:SetCurrentTheme(self:GetDefaultTheme()) end - local json_themes = self:ReadThemesJson() - json_themes[theme.Name] = nil - self.m_theme_library[theme.Name] = nil + local lib = self.m_theme_library + local json = self:ReadThemesJson() + json[theme.Name] = nil + lib[theme.Name] = nil - Serializer:WriteToFile(json_themes, self.m_themes_file) + Serializer:WriteToFile(self.m_themes_file, json) end function ThemeManager:FetchSavedThemes() @@ -166,16 +182,20 @@ function ThemeManager:PushTheme() self.m_style_stack = self.m_style_stack + 1 end end + + self.m_stack_depth = self.m_stack_depth + 1 end function ThemeManager:PopTheme() - if (self.m_col_stack ~= 0) then + if (self.m_col_stack > 0) then ImGui.PopStyleColor(self.m_col_stack) end - if (self.m_style_stack ~= 0) then + if (self.m_style_stack > 0) then ImGui.PopStyleVar(self.m_style_stack) end + + self.m_stack_depth = self.m_stack_depth - 1 end return ThemeManager diff --git a/SSV2/includes/services/ThreadManager.lua b/SSV2/includes/services/ThreadManager.lua index 0dbf954..37e6700 100644 --- a/SSV2/includes/services/ThreadManager.lua +++ b/SSV2/includes/services/ThreadManager.lua @@ -30,6 +30,13 @@ eInternalThreadState = { DEADLOCKED = 5, } +---@enum eThreadStage +eThreadStage = { + IDLE = 0, + PRE_CALLBACK = 1, + POST_CALLBACK = 2, +} + --#region Thread -------------------------------------- @@ -41,7 +48,9 @@ eInternalThreadState = { ---@field private m_callback function ---@field private m_can_run boolean ---@field private m_should_pause boolean +---@field private m_should_terminate boolean ---@field private m_state eThreadState +---@field private m_stage eThreadStage ---@field private m_time_created TimePoint ---@field private m_time_started seconds ---@field private m_last_entry_at seconds @@ -65,7 +74,9 @@ function Thread.new(name, callback) m_callback = callback, m_can_run = false, m_should_pause = false, + m_wants_exit = false, m_state = eThreadState.UNK, + m_stage = eThreadStage.IDLE, m_time_created = TimePoint.new(), m_time_started = 0, m_last_entry_at = 0, @@ -89,6 +100,12 @@ function Thread:GetState() return self.m_state end +---@private +---@return eThreadStage +function Thread:GetStage() + return self.m_stage +end + ---@return function function Thread:GetCallback() return self.m_callback @@ -143,28 +160,40 @@ function Thread:OnTick(s) Backend:debug("Started thread %s", self.m_name) while (self.m_can_run) do + self.m_stage = eThreadStage.IDLE + if (self.m_should_pause) then self.m_state = eThreadState.SUSPENDED self.m_last_entry_at = 0 repeat self.m_last_yield_at = Time.Now() yield() - until not self.m_should_pause + until not self.m_should_pause or self.m_should_terminate self.m_time_started = Time.Now() self.m_last_entry_at = Time.Now() end + if (self.m_should_terminate) then + self:Stop() + return + end + self.m_state = eThreadState.RUNNING local cycle_start = Time.Now() self.m_last_entry_at = cycle_start local work_start = cycle_start + self.m_stage = eThreadStage.PRE_CALLBACK local ok, err = pcall(self.m_callback, s) + self.m_stage = eThreadStage.POST_CALLBACK local work_end = Time.Now() local work_ms = (work_end - work_start) * 1000 self.m_avg_work_ms = self.m_avg_work_ms * 0.9 + work_ms * 0.1 - if (not ok) then - log.fwarning("Thread %s was terminated due to an unhandled exception: %s", self.m_name, err) + if (self.m_should_terminate or not ok) then + if (not ok and err) then + log.fwarning("Thread %s was terminated due to an unhandled exception: %s", self.m_name, err) + end + self:Stop() return end @@ -173,7 +202,6 @@ function Thread:OnTick(s) self.m_last_yield_at = self.m_last_exit_at local cycle_ms = (self.m_last_exit_at - cycle_start) * 1000 self.m_avg_cycle_ms = self.m_avg_cycle_ms * 0.9 + cycle_ms * 0.1 - yield() end end @@ -202,9 +230,17 @@ function Thread:Stop() return end - self.m_time_started = 0 - self.m_can_run = false - self.m_state = eThreadState.DEAD + if (self.m_stage ~= eThreadStage.IDLE) then + Backend:debug("Thread %s tried to exit mid-callback.", self.m_name) + self.m_should_terminate = true + return + end + + self.m_time_started = 0 + self.m_can_run = false + self.m_should_terminate = false + self.m_state = eThreadState.DEAD + self.m_stage = eThreadStage.IDLE Backend:debug("Terminated thread %s", self.m_name) end @@ -325,7 +361,7 @@ function ThreadManager:Run(func) end local handler = self.m_callback_handlers[API_VER] - if not (handler or handler.dispatch) then + if not (handler and handler.dispatch) then Backend:debug("[ThreadManager] No handler for API version: %s", EnumToString(Enums.eAPIVersion, API_VER)) return end @@ -403,25 +439,21 @@ end ---@param suspended? boolean ---@param is_debug_thread? boolean function ThreadManager:RegisterLooped(name, func, suspended, is_debug_thread) - if (API_VER == Enums.eAPIVersion.L54 and not is_debug_thread) then - return - end + local isMock = (API_VER == Enums.eAPIVersion.L54) + if (isMock and not is_debug_thread) then return end + if (is_debug_thread and not isMock) then return end - if (is_debug_thread and API_VER ~= Enums.eAPIVersion.L54) then - return - end - - if (string.isempty(name) or string.iswhitespace(name)) then + if (not string.isvalid(name)) then name = string.random(5, true):upper() end if (self:IsThreadRegistered(name)) then - log.fwarning("a thread with the name '%s' is already registered!", name) + log.fwarning("A thread with the name '%s' is already registered!", name) return end local thread = Thread(name, func) - if suspended then + if (suspended) then thread:Suspend() end @@ -438,14 +470,34 @@ function ThreadManager:GetThread(name) return self.m_threads[name] end ----@return eThreadState -function ThreadManager:GetThreadState(name) +---@generic RET +---@param name string +---@param default RET +---@param func fun(thread: Thread, ...?: any): RET +---@param ... any +---@return RET +function ThreadManager:WithThreadName(name, default, func, ...) local thread = self:GetThread(name) - if not thread then - return eThreadState.UNK + if (not thread) then + return default end - return thread:GetState() + return func(thread, ...) +end + +---@param func fun(name: string, thread: Thread): nil +function ThreadManager:ForEach(func) + for name, thread in pairs(self.m_threads) do + func(name, thread) + end +end + +---@param name string +---@return eThreadState +function ThreadManager:GetThreadState(name) + return self:WithThreadName(name, eThreadState.UNK, function(thread) + return thread:GetState() + end) end function ThreadManager:ListThreads() @@ -461,57 +513,54 @@ end ---@param name string ---@return boolean function ThreadManager:IsThreadRunning(name) - local thread = self:GetThread(name) - return thread and thread:IsRunning() or false + return self:WithThreadName(name, false, function(thread) + return thread:IsRunning() + end) end +-- ### This is dangerous outside of debug context. Do not call. +-- +-- TODO: Refactor and properly document this. +---@private ---@param name string -function ThreadManager:StartThread(name) - local thread = self:GetThread(name) - if not thread then - return - end - - local ok = thread:Start() - if not ok then - local func = thread:GetCallback() - local new_thread = Thread(name, func) +function ThreadManager:RestartThread(name) + self:WithThreadName(name, nil, function(thread) + if (thread:IsRunning()) then + return + end - self.m_threads[name] = new_thread - self:Run(function(s) - new_thread:OnTick(s) - end) - end + -- This is horrible. Some of our modules hold references to their threads and doing this leaves them with dangling references. + -- Thankfully this does not end up actually happening since this branch is never reached. + -- I can't remember why I did this but I'm pretty sure it's supposed to be a duct tape fix to some obscure bug. I blame Beck's. + if (not thread:Start()) then + Backend:debug("Recreating thread %s", name) + local func = thread:GetCallback() + local new_thread = Thread(name, func) + self.m_threads[name] = new_thread + self:Run(function(s) new_thread:OnTick(s) end) + end + end) end ---@param name string function ThreadManager:SuspendThread(name) - local thread = self:GetThread(name) - if not thread then - return - end - - thread:Suspend() + self:WithThreadName(name, nil, function(thread) + thread:Suspend() + end) end ---@param name string function ThreadManager:ResumeThread(name) - local thread = self:GetThread(name) - if not thread then - return - end - - thread:Resume() + self:WithThreadName(name, nil, function(thread) + thread:Resume() + end) end ---@param name string function ThreadManager:TerminateThread(name) - local thread = self:GetThread(name) - if not thread then - return - end - - thread:Stop() + self:WithThreadName(name, nil, function(thread) + thread:Stop() + end) end ---@param name string @@ -521,22 +570,15 @@ function ThreadManager:RemoveThread(name) end function ThreadManager:SuspendAllThreads() - for _, thread in pairs(self.m_threads) do - thread:Suspend() - end + self:ForEach(function(_, thread) thread:Suspend() end) end function ThreadManager:ResumeAllThreads() - for _, thread in pairs(self.m_threads) do - thread:Resume() - end + self:ForEach(function(_, thread) thread:Resume() end) end function ThreadManager:RemoveAllThreads() - for name, _ in pairs(self.m_threads) do - self:RemoveThread(name) - end - + self:ForEach(function(name, _) self:RemoveThread(name) end) self.m_mock_routines = {} end diff --git a/SSV2/includes/services/ToastNotifier.lua b/SSV2/includes/services/ToastNotifier.lua index 04d2a7c..bb698c5 100644 --- a/SSV2/includes/services/ToastNotifier.lua +++ b/SSV2/includes/services/ToastNotifier.lua @@ -133,6 +133,9 @@ end function Notification:Draw(context, x_left, content_width, pImDrawList, ease, pendingCount, showProgress, progress) pendingCount = pendingCount or 0 ease = ease or self:GetEaseIn() + local isToast = context == Enums.eNotificationContext.TOAST + local isNotif = not isToast + local hasCallback = isNotif and self.m_callback ~= nil local cardRounding = context == Enums.eNotificationContext.TOAST and 3.0 or 8.0 local padding = 12.0 local accentWidth = 8.0 @@ -154,11 +157,15 @@ function Notification:Draw(context, x_left, content_width, pImDrawList, ease, pe local alpha = 0.0 + ease local cardTL = vec2:new(cursor.x, cursor.y + slideOffset) local cardBR = vec2:new(cursor.x + cardWidth, cursor.y + slideOffset + cardHeight) - local windowBG = GVars.ui.style.theme.Colors.WindowBg + local frameBG = GVars.ui.style.theme.Colors.FrameBg local windowAlpha = GVars.ui.style.bg_alpha - local notifBG = Color(windowBG.x, windowBG.y, windowBG.z, windowAlpha * alpha):AsU32() + local hovered = ImGui.IsMouseHoveringRect(cardTL.x, cardTL.y, cardBR.x, cardBR.y) + local mainCol = Color(frameBG.x, frameBG.y, frameBG.z, isNotif and 1 or (windowAlpha * alpha)) + local method = ThemeManager:IsBackgroundDark() and Color.Brighten or Color.Darken + local notifCol = (hovered and hasCallback) and method(mainCol, 0.12) or mainCol + local notifBG = notifCol:AsU32() - if (context == Enums.eNotificationContext.TOAST and pendingCount > 0) then + if (isToast and pendingCount > 0) then local stackSpacing = 6.0 local maxOffset = 24.0 for i = 1, pendingCount do @@ -169,7 +176,7 @@ function Notification:Draw(context, x_left, content_width, pImDrawList, ease, pe cardBR.y + ghostOffset - 1, cardBR.x - i + 1, cardBR.y + ghostOffset + 6, - ImGui.GetColorU32(windowBG.x, windowBG.y, windowBG.z, windowAlpha - (i * 0.1) * alpha), + ImGui.GetColorU32(frameBG.x, frameBG.y, frameBG.z, windowAlpha - (i * 0.1) * alpha), cardRounding + i ) end @@ -212,10 +219,8 @@ function Notification:Draw(context, x_left, content_width, pImDrawList, ease, pe notifBG, cardRounding ) - local hovered = ImGui.IsMouseHoveringRect(cardTL.x, cardTL.y, cardBR.x, cardBR.y) - local titlePos = vec2:new(cardTL.x + padding, cardTL.y + padding) - local textCol = ImGui.GetAutoTextColor(Color(windowBG:unpack())) + local textCol = ImGui.GetAutoTextColor(Color(frameBG:unpack())) local r, g, b, _ = self.m_accent_color:AsFloat() local headerCol = self.m_level == Enums.eNotificationLevel.MESSAGE and textCol or Color(r, g, b, 1.0 * alpha) ImGui.ImDrawListAddText( @@ -227,7 +232,7 @@ function Notification:Draw(context, x_left, content_width, pImDrawList, ease, pe self.m_title ) - if (context == Enums.eNotificationContext.CENTER and hovered) then + if (isNotif and hovered) then local btnPos = vec2:new(cardBR.x - padding - rightButtonW + 6.0, cardTL.y + padding - 2.0) local btnBR = vec2:new(btnPos.x + 20.0, btnPos.y + 20.0) local btnBg = ImGui.GetStyleColorU32(ImGuiCol.Button) @@ -257,6 +262,14 @@ function Notification:Draw(context, x_left, content_width, pImDrawList, ease, pe ImGui.SetTooltip(_T("GENERIC_DISMISS")) end + + if (hasCallback) then + ImGui.SetTooltip("Click to execute.") + + if (KeyManager:IsKeyJustPressed(eVirtualKeyCodes.VK_LBUTTON)) then + self:Invoke() + end + end end local bodyPos = vec2:new(cardTL.x + padding, cardTL.y + padding + titleSize.y + titleSpacing) @@ -271,7 +284,7 @@ function Notification:Draw(context, x_left, content_width, pImDrawList, ease, pe content_width - (padding * 2.0) ) - if (context == Enums.eNotificationContext.TOAST and showProgress and progress) then + if (isToast and showProgress and progress) then local barHeight = 4.0 local barW = cardWidth * (1.0 - progress) @@ -715,6 +728,7 @@ function Notifier:DrawNotifications(start_pos) ImGui.PushStyleVar(ImGuiStyleVar.WindowBorderSize, 0) ImGui.PushStyleVar(ImGuiStyleVar.ScrollbarSize, 9.0) ImGui.PushStyleColor(ImGuiCol.ScrollbarBg, 0) + ImGui.SetNextWindowBgAlpha(0) if (ImGui.Begin("##notif_center", ImGuiWindowFlags.NoMove | ImGuiWindowFlags.NoResize @@ -724,21 +738,13 @@ function Notifier:DrawNotifications(start_pos) )) then local drawList = ImGui.GetWindowDrawList() local windowBG = vec4:new(ImGui.GetStyleColorVec4(ImGuiCol.WindowBg)) - local frame = vec4:new(ImGui.GetStyleColorVec4(ImGuiCol.FrameBg)) - local contrast = math.abs(frame.x - windowBG.x) - if (contrast >= 0.1) then - frame.x = frame.x * 0.85 - frame.y = frame.y * 0.85 - frame.z = frame.z * 0.85 - frame.w = frame.w * 0.85 - end ImGui.ImDrawListAddRectFilled( drawList, window_pos.x + 10, window_pos.y + 10, window_pos.x + self.m_window_width - 10, window_pos.y + height - 10, - ImGui.GetColorU32(frame.x, frame.y, frame.z, frame.w), + ImGui.GetColorU32(windowBG.x, windowBG.y, windowBG.z, windowBG.w), GVars.ui.style.theme.Styles.WindowRounding or 2 ) @@ -753,8 +759,10 @@ function Notifier:DrawNotifications(start_pos) ImGui.SetWindowFontScale(0.81) local muteText = _T(self:IsMuted() and "GENERIC_UNMUTE" or "GENERIC_MUTE") local muteTextWidth = ImGui.CalcTextSize(muteText) - local totalWidth = muteTextWidth + (style.FramePadding.x * (count > 0 and 4 or 2)) + - (style.ItemSpacing.x * (count > 0 and 2 or 1)) + local totalWidth = muteTextWidth + + (style.FramePadding.x * (count > 0 and 4 or 2)) + + (style.ItemSpacing.x * (count > 0 and 2 or 1)) + if (count > 0) then totalWidth = totalWidth + ImGui.CalcTextSize(_T("GENERIC_CLEAR_ALL")) end diff --git a/SSV2/includes/services/Translator.lua b/SSV2/includes/services/Translator.lua index 6fcdbc4..dba400b 100644 --- a/SSV2/includes/services/Translator.lua +++ b/SSV2/includes/services/Translator.lua @@ -7,42 +7,130 @@ -- * Provide a copy of or a link to the original license (GPL-3.0 or later); see LICENSE.md or . -local en_loaded, en = pcall(require, "lib.translations.en-US") -local locales_loaded, t = pcall(require, "lib.translations.__locales") +local en_loaded, en = pcall(require, "lib.translations.en-US") +local locales_loaded, __locales = pcall(require, "lib.translations.__locales") +local GameLangToCustom = { + [0] = 1, + [1] = 2, + [2] = 3, + [3] = 5, + [4] = 4, + [5] = 6, + [6] = 11, + [7] = 7, + [8] = 12, + [9] = 8, + [10] = 10, + [11] = 4, + [12] = 9, +} -------------------------------------- -- Class: Translator -------------------------------------- ---**Global Singleton.** ---@class Translator ----@field labels table +---@field labels dict +---@field default_labels dict ---@field lang_code string +---@field locales array<{ name: string, iso: string }> +---@field wants_reload boolean ---@field private m_log_history table ---@field private m_cache table> ---@field private m_last_load_time TimePoint -Translator = { - default_labels = en_loaded and en or {}, - m_last_load_time = TimePoint.new(), - m_cache = {}, - locales = locales_loaded and t or { { name = "English", iso = "en-US" } }, -} +---@field private m_reloading boolean +---@field protected m_initialized boolean +local Translator = {} Translator.__index = Translator +---@return Translator +function Translator:init() + if (self.m_initialized) then return self end + if (_G.Translator) then return _G.Translator end + + return setmetatable({ + default_labels = en_loaded and en or {}, + locales = locales_loaded and __locales or { { name = "English", iso = "en-US" } }, + labels = {}, + m_cache = {}, + m_log_history = {}, + m_last_load_time = TimePoint.new(), + m_initialized = false, + m_reloading = false, + }, Translator) +end + +function Translator:MatchGameLanguage() + local current = LOCALIZATION.GET_CURRENT_LANGUAGE() + local idx = GameLangToCustom[current] or 1 + local match = self.locales[idx] + + if (not match) then return false end + + GVars.backend.language_index = idx + GVars.backend.language_code = match.iso + GVars.backend.language_name = match.name + + return true +end + function Translator:Load() - GVars.backend.language_code = GVars.backend.language_code or "en-US" - local iso = GVars.backend.language_code - local ok, res + ThreadManager:Run(function() + if (GVars.backend.use_game_language) then + if (not self:MatchGameLanguage() and self.m_reloading) then + Notifier:ShowError("Translator", "Failed to match game language.") + GVars.backend.use_game_language = false + return + end + end + + GVars.backend.language_index = GVars.backend.language_index or 1 + GVars.backend.language_code = GVars.backend.language_code or "en-US" + GVars.backend.language_name = GVars.backend.language_name or "English" + + local ok, res + if (GVars.backend.language_code ~= "en-US") then + local path = "lib.translations." .. GVars.backend.language_code + ok, res = pcall(require, path) + end + + local newLabels = (ok and (type(res) == "table")) and res or self.default_labels + table.overwrite(self.labels, newLabels) + + self.m_log_history = {} + self.m_cache = {} + self.lang_code = GVars.backend.language_code + self.m_initialized = true + self.m_reloading = false + self.m_last_load_time:Reset() + end) +end - if (iso ~= "en-US") then - local path = _F("lib.translations.%s", iso) - ok, res = pcall(require, path) +---@private +function Translator:Reload() + if (not self.m_initialized or not self.m_last_load_time:HasElapsed(3e3)) then -- 3. we have this though so reload can not be called twice. I'm a bit confused + return end - self.labels = (ok and (type(res) == "table")) and res or self.default_labels - self.lang_code = iso - self.m_log_history = {} - self.m_last_load_time:Reset() + self.m_initialized = false + self.m_reloading = true + self:Load() + Notifier:ShowMessage("Translator", "Reloaded.") +end + +---@return boolean +function Translator:IsReady() + return self.m_initialized and not self.m_reloading +end + +---@return boolean +function Translator:IsReloading() + return self.m_reloading +end + +---@return boolean +function Translator:CanReload() + return self:IsReady() and self.m_last_load_time:HasElapsed(3e3) end ---@param msg string @@ -64,26 +152,21 @@ function Translator:Log(message) table.insert(self.m_log_history, message) end -function Translator:Reload() - if (not self.m_last_load_time:HasElapsed(3e3)) then - return - end - - -- We can't even unload files because package is fully disabled. loadfile? in your dreams... 🥲 - self:Load() - Notifier:ShowMessage("Translator", "Reloaded.") +---@return table> +function Translator:GetCache() + return self.m_cache end ---@param label string ---@return string -function Translator:GetCache(label) +function Translator:GetCachedLabel(label) self.m_cache[self.lang_code] = self.m_cache[self.lang_code] or {} return self.m_cache[self.lang_code][label] end ---@param label string ---@param text string -function Translator:SetCache(label, text) +function Translator:CacheLabel(label, text) self.m_cache[self.lang_code] = self.m_cache[self.lang_code] or {} self.m_cache[self.lang_code][label] = text end @@ -92,15 +175,15 @@ end ---@param label string ---@return string function Translator:Translate(label) + if (not self:IsReady()) then return "" end + if (self.lang_code ~= GVars.backend.language_code) then - self:Reload() + self.wants_reload = true return "" end - local cached = self:GetCache(label) - if (cached) then - return cached - end + local cached = self:GetCachedLabel(label) + if (cached) then return cached end local text = self.labels[label] if (not text) then @@ -114,20 +197,11 @@ function Translator:Translate(label) return _F("[!MISSING LABEL]: %s", label) end - if (not cached) then - self:SetCache(label, text) - end + if (not cached) then self:CacheLabel(label, text) end return text end --- Wrapper for `Translator:Translate` ----@param label string ----@return string -function _T(label) - return Translator:Translate(label) -end - -- Translates text using GXTs if expected and available. ---@param label string ---@return string @@ -146,3 +220,15 @@ function Translator:TranslateGXTList(labels) labels[k] = self:TranslateGXT(v) end end + +-- This is called in `Backend`'s main thread. +function Translator:OnTick() + -- currently only handles reload requests. + + if (self.wants_reload and not self:IsReloading()) then + self.wants_reload = false + self:Reload() + end +end + +return Translator:init() diff --git a/SSV2/includes/structs/StateMachine.lua b/SSV2/includes/structs/StateMachine.lua index 340344e..23fc7e5 100644 --- a/SSV2/includes/structs/StateMachine.lua +++ b/SSV2/includes/structs/StateMachine.lua @@ -11,7 +11,7 @@ ---@field predicate? Predicate ---@field interval? seconds ---@field callback? fun(self: StateMachine, context: table|metatable|userdata|lightuserdata) -local StateMachineParams = {} + ---@class StateMachine ---@field private m_is_active boolean @@ -33,11 +33,11 @@ setmetatable(StateMachine, { ---@param opts StateMachineParams function StateMachine:new(opts) return setmetatable({ - m_predicate = opts.predicate, - m_interval = opts.interval or 0, - m_callback = opts.callback, - m_is_active = false, - m_is_toggled = false, + m_predicate = opts.predicate, + m_interval = opts.interval or 0, + m_callback = opts.callback, + m_is_active = false, + m_is_toggled = false, m_next_update = 0 ---@diagnostic disable-next-line }, StateMachine) @@ -48,20 +48,21 @@ function StateMachine:Update(context) local should_be_active = not self.m_predicate or self:m_predicate(context) if (not should_be_active) then - self.m_is_active = false + self.m_is_active = false self.m_is_toggled = false return end + local now = Time.Now() if (not self.m_is_active) then - self.m_is_active = true - self.m_is_toggled = false - self.m_next_update = Time.Now() + self.m_interval + self.m_is_active = true + self.m_is_toggled = false + self.m_next_update = now + self.m_interval end - if (Time.Now() >= self.m_next_update) then - self.m_is_toggled = not self.m_is_toggled - self.m_next_update = Time.Now() + self.m_interval + if (now >= self.m_next_update) then + self.m_is_toggled = not self.m_is_toggled + self.m_next_update = now + self.m_interval end if (self.m_is_toggled and self.m_callback) then @@ -72,7 +73,7 @@ end -- Manual function StateMachine:Activate() - self.m_is_active = true + self.m_is_active = true self.m_next_update = Time.Now() end diff --git a/SSV2/includes/tests.lua b/SSV2/includes/tests.lua new file mode 100644 index 0000000..2710421 --- /dev/null +++ b/SSV2/includes/tests.lua @@ -0,0 +1,10 @@ +--[[ + Mock environment playground. + + Write your code in this file then open the project's folder in a terminal and type: lua samurais_scripts.lua + + Must have Lua 5.4 installed. Duh! +]] + + +-- ThreadManager:UpdateMockRoutines() diff --git a/SSV2/samurais_scripts.lua b/SSV2/samurais_scripts.lua index a6bc261..e71dab9 100644 --- a/SSV2/samurais_scripts.lua +++ b/SSV2/samurais_scripts.lua @@ -7,24 +7,30 @@ -- * Provide a copy of or a link to the original license (GPL-3.0 or later); see LICENSE.md or . +local start_time = os.clock() require("includes.init") +if (Backend:IsMockEnv()) then + require("includes.tests") + return +end + local commandRegistry = require("includes.lib.commands") -local Weapons = require("includes.data.weapons") +local weapons = require("includes.data.weapons") local weaponData = require("includes.data.weapon_data") local weapons_map = { - ["GROUP_MELEE"] = Weapons.Melee, - ["GROUP_PISTOL"] = Weapons.Pistols, - ["GROUP_RIFLE"] = Weapons.AssaultRifles, - ["GROUP_SHOTGUN"] = Weapons.Shotguns, - ["GROUP_SMG"] = Weapons.SMG, - ["GROUP_MG"] = Weapons.MachineGuns, - ["GROUP_SNIPER"] = Weapons.SniperRifles, - ["GROUP_HEAVY"] = Weapons.Heavy, - ["GROUP_THROWN"] = Weapons.Throwables, - ["GROUP_PETROLCAN"] = Weapons.Misc, - ["GROUP_STUNGUN"] = Weapons.Misc, - ["GROUP_TRANQILIZER"] = Weapons.Misc, + ["GROUP_MELEE"] = weapons.Melee, + ["GROUP_PISTOL"] = weapons.Pistols, + ["GROUP_RIFLE"] = weapons.AssaultRifles, + ["GROUP_SHOTGUN"] = weapons.Shotguns, + ["GROUP_SMG"] = weapons.SMG, + ["GROUP_MG"] = weapons.MachineGuns, + ["GROUP_SNIPER"] = weapons.SniperRifles, + ["GROUP_HEAVY"] = weapons.Heavy, + ["GROUP_THROWN"] = weapons.Throwables, + ["GROUP_PETROLCAN"] = weapons.Misc, + ["GROUP_STUNGUN"] = weapons.Misc, + ["GROUP_TRANQILIZER"] = weapons.Misc, } local function populate_weapons() @@ -40,7 +46,7 @@ local function populate_weapons() end table.insert(weapon_group, hash) - table.insert(Weapons.All, hash) + table.insert(weapons.All, hash) ::continue:: end @@ -50,11 +56,9 @@ GPointers:Init() Serializer:FlushObjectQueue() Backend:RegisterHandlers() Translator:Load() -GUI:init() +GUI:LateInit() ThreadManager:Run(function() - local start_time = os.clock() - for name, cmd in pairs(commandRegistry) do CommandExecutor:RegisterCommand(name, cmd.callback, cmd.opts) end