From 9bef125cc23a81c98c2e6816ce6798dc0a3ded5e Mon Sep 17 00:00:00 2001
From: SAMURAI <66764345+xesdoog@users.noreply.github.com>
Date: Sun, 22 Mar 2026 01:37:49 +0100
Subject: [PATCH 1/5] chore(Serializer): make use of the new os.rename
- With this, configs should no longer get corrupted and even if they do, a backup is always available.
> [!Note]
> For YimLuaAPI compatibility, this relies on https://github.com/TupoyeMenu/YimLuaAPI/pull/3
---
SSV2/includes/backend.lua | 25 +-
SSV2/includes/classes/Mutex.lua | 7 +-
.../features/vehicle/flappy_doors.lua | 5 +-
SSV2/includes/frontend/settings/debug_ui.lua | 3 +-
SSV2/includes/init.lua | 41 +--
SSV2/includes/lib/compat.lua | 2 +-
SSV2/includes/lib/mock_env.lua | 260 +++++++++---------
SSV2/includes/lib/types.lua | 30 +-
SSV2/includes/lib/utils.lua | 40 ++-
SSV2/includes/modules/Cast.lua | 20 +-
SSV2/includes/modules/Chrono.lua | 26 +-
SSV2/includes/modules/Logger.lua | 16 +-
SSV2/includes/services/Serializer.lua | 99 ++++---
SSV2/includes/services/ThreadManager.lua | 178 +++++++-----
SSV2/includes/structs/StateMachine.lua | 29 +-
SSV2/includes/tests.lua | 14 +
SSV2/samurais_scripts.lua | 36 +--
17 files changed, 478 insertions(+), 353 deletions(-)
create mode 100644 SSV2/includes/tests.lua
diff --git a/SSV2/includes/backend.lua b/SSV2/includes/backend.lua
index 3b8244e..3c4bd88 100644
--- a/SSV2/includes/backend.lua
+++ b/SSV2/includes/backend.lua
@@ -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,38 @@ 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()
+
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/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/frontend/settings/debug_ui.lua b/SSV2/includes/frontend/settings/debug_ui.lua
index 00d0ccf..4a229b2 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
diff --git a/SSV2/includes/init.lua b/SSV2/includes/init.lua
index 86bce62..d3454ac 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,16 @@ 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()
----------------------------------------------------------------------------------------------------
+
----------------- Big Features (for smaller features, refer to includes/features) ------------------
BillionaireServices = require("includes.features.BillionaireServicesV2"):init()
EntityForge = require("includes.features.EntityForge"):init()
@@ -95,12 +104,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",
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/mock_env.lua b/SSV2/includes/lib/mock_env.lua
index 13205c2..78cefcd 100644
--- a/SSV2/includes/lib/mock_env.lua
+++ b/SSV2/includes/lib/mock_env.lua
@@ -9,149 +9,159 @@
---@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 = function(name, fn)
+ print("[mock script looped]", name)
+ fn({ sleep = NOP, yield = NOP })
+ end,
+ run_in_fiber = function(fn)
+ fn({ sleep = NOP, yield = NOP })
+ end,
+
+ is_active = function(scr_name)
+ print("[mock script active check]", scr_name)
+ 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 = function(evt, fn)
+ print("[mock event]", evt)
+ return fn
+ end
+ }
+ 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,
- },
- vec3
- )
+ 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
-end
-if (not gui) then
- gui = {}
- gui.add_tab = function(name)
- print("[mock gui.add_tab]")
- return gui
+ 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
- gui.add_imgui = function(_) end
- gui.add_always_draw_imgui = function()
- print("[mock gui.add_always_draw_imgui]")
+
+ 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
-end
-if (not STREAMING) then
- STREAMING = {
- IS_PLAYER_SWITCH_IN_PROGRESS = function()
- return false
+ if (not gui) then
+ gui = {}
+ gui.add_tab = function(_)
+ print("[mock gui.add_tab]")
+ return gui
end
- }
+ gui.add_imgui = NOP
+ gui.add_always_draw_imgui = NOP
+ 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/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..9006e44 100644
--- a/SSV2/includes/lib/utils.lua
+++ b/SSV2/includes/lib/utils.lua
@@ -1523,6 +1523,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 +1580,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 +1633,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.AsInt8, "int8_t" },
+ { Cast.AsInt16, "int16_t" },
{ Cast.AsInt32_t, "int32_t" },
- { Cast.AsInt64_t, "int64_t" },
+ { Cast.AsInt64, "int64_t" },
}
}
---@param n integer
@@ -1672,14 +1686,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..7e1a5b4 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,7 +76,7 @@ function Cast:AsInt16_t()
end
---@return uint32_t
-function Cast:AsUint32_t()
+function Cast:AsUint32()
return self.m_value & 0xFFFFFFFF
end
@@ -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/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/services/Serializer.lua b/SSV2/includes/services/Serializer.lua
index a92f7fd..0413e1c 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,7 +744,6 @@ 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.
@@ -751,7 +767,7 @@ function Serializer:WriteToFile(data, filename)
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/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/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..fc04fb4
--- /dev/null
+++ b/SSV2/includes/tests.lua
@@ -0,0 +1,14 @@
+--[[
+ 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()
+
+local test_str = "some_random_ass_string"
+print(test_str:snakecase())
+print(test_str:pascalcase())
diff --git a/SSV2/samurais_scripts.lua b/SSV2/samurais_scripts.lua
index a6bc261..7cb9fb2 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
@@ -53,8 +59,6 @@ Translator:Load()
GUI:init()
ThreadManager:Run(function()
- local start_time = os.clock()
-
for name, cmd in pairs(commandRegistry) do
CommandExecutor:RegisterCommand(name, cmd.callback, cmd.opts)
end
From 7d7bae4ea4838693b2e25ba57b393cb2c3cbff77 Mon Sep 17 00:00:00 2001
From: SAMURAI <66764345+xesdoog@users.noreply.github.com>
Date: Sun, 22 Mar 2026 01:40:36 +0100
Subject: [PATCH 2/5] remove test code
---
SSV2/includes/tests.lua | 4 ----
1 file changed, 4 deletions(-)
diff --git a/SSV2/includes/tests.lua b/SSV2/includes/tests.lua
index fc04fb4..2710421 100644
--- a/SSV2/includes/tests.lua
+++ b/SSV2/includes/tests.lua
@@ -8,7 +8,3 @@
-- ThreadManager:UpdateMockRoutines()
-
-local test_str = "some_random_ass_string"
-print(test_str:snakecase())
-print(test_str:pascalcase())
From 3ca56f2b568218de7ca3204832a74716535af48a Mon Sep 17 00:00:00 2001
From: SAMURAI <66764345+xesdoog@users.noreply.github.com>
Date: Tue, 24 Mar 2026 16:11:50 +0100
Subject: [PATCH 3/5] fix: miscellaneous fixes
---
SSV2/includes/classes/gta/CPed.lua | 8 +-
SSV2/includes/data/config.lua | 3 +-
SSV2/includes/data/enums/__init__.lua | 29 +++----
SSV2/includes/data/enums/game_language.lua | 27 +++++++
...g_flags.lua => vehicle_handling_flags.lua} | 0
.../features/vehicle/misc_vehicle.lua | 2 +-
SSV2/includes/frontend/self/self_ui.lua | 7 +-
SSV2/includes/frontend/settings/debug_ui.lua | 4 +-
.../frontend/settings/settings_ui.lua | 70 ++++++++++-------
SSV2/includes/frontend/vehicle/stancer_ui.lua | 2 +-
SSV2/includes/frontend/vehicle/vehicle_ui.lua | 2 +-
SSV2/includes/lib/imgui_ext.lua | 6 +-
SSV2/includes/lib/mock_env.lua | 38 ++++------
SSV2/includes/lib/translations/__hashmap.json | 6 +-
SSV2/includes/lib/translations/__locales.lua | 56 +++++++-------
SSV2/includes/lib/translations/de-DE.lua | 6 +-
SSV2/includes/lib/translations/en-US.lua | 4 +
SSV2/includes/lib/translations/es-ES.lua | 6 +-
SSV2/includes/lib/translations/fr-FR.lua | 6 +-
SSV2/includes/lib/translations/it-IT.lua | 6 +-
SSV2/includes/lib/translations/ja-JP.lua | 6 +-
SSV2/includes/lib/translations/ko-KR.lua | 6 +-
SSV2/includes/lib/translations/pl-PL.lua | 6 +-
SSV2/includes/lib/translations/pt-BR.lua | 6 +-
SSV2/includes/lib/translations/ru-RU.lua | 6 +-
SSV2/includes/lib/translations/zh-CN.lua | 6 +-
SSV2/includes/lib/translations/zh-TW.lua | 6 +-
SSV2/includes/lib/utils.lua | 16 ++--
SSV2/includes/modules/Cast.lua | 2 +-
SSV2/includes/modules/LocalPlayer.lua | 23 +++---
SSV2/includes/modules/Ped.lua | 5 ++
SSV2/includes/services/GUI.lua | 31 ++++----
SSV2/includes/services/Serializer.lua | 10 +--
SSV2/includes/services/ThemeManager.lua | 3 +-
SSV2/includes/services/Translator.lua | 75 +++++++++++++++----
35 files changed, 312 insertions(+), 183 deletions(-)
create mode 100644 SSV2/includes/data/enums/game_language.lua
rename SSV2/includes/data/enums/{handling_flags.lua => vehicle_handling_flags.lua} (100%)
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/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 4a229b2..779ee37 100644
--- a/SSV2/includes/frontend/settings/debug_ui.lua
+++ b/SSV2/includes/frontend/settings/debug_ui.lua
@@ -547,8 +547,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..ceba287 100644
--- a/SSV2/includes/frontend/settings/settings_ui.lua
+++ b/SSV2/includes/frontend/settings/settings_ui.lua
@@ -7,10 +7,11 @@
-- * 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 selectedTheme
+local newThemeBuff
+
+local cfgReset = {
---@type Set
exceptions = Set.new("backend.debug_mode"),
excToggles = {
@@ -23,14 +24,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 +44,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
+ GVars.backend.use_game_language = GUI:CustomToggle(_T("SETTINGS_GAME_LANGUAGE"),
+ GVars.backend.use_game_language,
+ { onClick = function() Translator:Reload() end, }
+ )
+ GUI:HelpMarker(_T("SETTINGS_GAME_LANGUAGE_TT"))
+
+ ImGui.Spacing()
+ ImGui.BeginDisabled(GVars.backend.use_game_language)
+ 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
+ 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 +114,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 +127,6 @@ local function drawThemeSettings()
if (ImGui.Begin("##new_theme",
ImGuiWindowFlags.NoTitleBar
- | ImGuiWindowFlags.NoMove
| ImGuiWindowFlags.NoResize
| ImGuiWindowFlags.AlwaysAutoResize
)) then
@@ -306,7 +313,8 @@ local function drawGuiSettings()
ImGui.PopStyleVar()
GUI:ShowWindowHeightLimit()
end,
- ImGui.CloseCurrentPopup
+ ImGui.CloseCurrentPopup,
+ true
)
ImGui.EndPopup()
end
@@ -346,14 +354,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()
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/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 78cefcd..7129d31 100644
--- a/SSV2/includes/lib/mock_env.lua
+++ b/SSV2/includes/lib/mock_env.lua
@@ -62,28 +62,15 @@ function MockEnv.Setup(version)
if (not script) then
script = {
- register_looped = function(name, fn)
- print("[mock script looped]", name)
- fn({ sleep = NOP, yield = NOP })
- end,
- run_in_fiber = function(fn)
- fn({ sleep = NOP, yield = NOP })
- end,
-
- is_active = function(scr_name)
- print("[mock script active check]", scr_name)
- return false
- end
+ register_looped = NOP,
+ run_in_fiber = NOP,
+ execute_as_script = NOP,
+ is_active = function(_) return false end,
}
end
if (not event) then
- event = {
- register_handler = function(evt, fn)
- print("[mock event]", evt)
- return fn
- end
- }
+ event = { register_handler = NOP }
end
if (not menu_event) then
@@ -147,13 +134,14 @@ function MockEnv.Setup(version)
end
if (not gui) then
- gui = {}
- gui.add_tab = function(_)
- print("[mock gui.add_tab]")
- return gui
- end
- gui.add_imgui = NOP
- gui.add_always_draw_imgui = NOP
+ 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
if (not STREAMING) then
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/utils.lua b/SSV2/includes/lib/utils.lua
index 9006e44..09ba320 100644
--- a/SSV2/includes/lib/utils.lua
+++ b/SSV2/includes/lib/utils.lua
@@ -102,13 +102,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
@@ -1639,10 +1639,10 @@ local INT_INFERENCE = {
{ Cast.AsUint64, "uint64_t" },
},
["signed"] = {
- { Cast.AsInt8, "int8_t" },
- { Cast.AsInt16, "int16_t" },
- { Cast.AsInt32_t, "int32_t" },
- { Cast.AsInt64, "int64_t" },
+ { Cast.AsInt8, "int8_t" },
+ { Cast.AsInt16, "int16_t" },
+ { Cast.AsInt32, "int32_t" },
+ { Cast.AsInt64, "int64_t" },
}
}
---@param n integer
diff --git a/SSV2/includes/modules/Cast.lua b/SSV2/includes/modules/Cast.lua
index 7e1a5b4..e13dd88 100644
--- a/SSV2/includes/modules/Cast.lua
+++ b/SSV2/includes/modules/Cast.lua
@@ -81,7 +81,7 @@ function Cast:AsUint32()
end
---@return int32_t
-function Cast:AsInt32_t()
+function Cast:AsInt32()
local v = self.m_value & 0xFFFFFFFF
if (v >= 0x80000000) then
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/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/services/GUI.lua b/SSV2/includes/services/GUI.lua
index bc14110..fe706cd 100644
--- a/SSV2/includes/services/GUI.lua
+++ b/SSV2/includes/services/GUI.lua
@@ -127,7 +127,7 @@ function GUI:LateInit()
end
if (not math.is_inrange(GVars.ui.last_tab.tab_id, TABID_MIN, TABID_MAX)) then
- GVars.ui.last_tab.tab_id = 1
+ GVars.ui.last_tab.tab_id = TABID_MIN
end
local __t = self.m_tabs[GVars.ui.last_tab.tab_id][GVars.ui.last_tab.array_index]
@@ -390,12 +390,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 +405,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 +421,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
@@ -868,10 +872,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
diff --git a/SSV2/includes/services/Serializer.lua b/SSV2/includes/services/Serializer.lua
index 0413e1c..282d38a 100644
--- a/SSV2/includes/services/Serializer.lua
+++ b/SSV2/includes/services/Serializer.lua
@@ -752,6 +752,11 @@ end
---@param data any
---@param filename string
function Serializer:WriteToFile(data, filename)
+ 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
@@ -762,11 +767,6 @@ 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")
if (not f) then
log.fwarning("[Serializer]: Failed to open file: %s", err)
diff --git a/SSV2/includes/services/ThemeManager.lua b/SSV2/includes/services/ThemeManager.lua
index 4291da8..c7a44c5 100644
--- a/SSV2/includes/services/ThemeManager.lua
+++ b/SSV2/includes/services/ThemeManager.lua
@@ -39,14 +39,13 @@ 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
diff --git a/SSV2/includes/services/Translator.lua b/SSV2/includes/services/Translator.lua
index 6fcdbc4..536acfe 100644
--- a/SSV2/includes/services/Translator.lua
+++ b/SSV2/includes/services/Translator.lua
@@ -7,8 +7,23 @@
-- * 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,
+}
--------------------------------------
@@ -21,28 +36,51 @@ local locales_loaded, t = pcall(require, "lib.translations.__locales")
---@field private m_log_history table
---@field private m_cache table>
---@field private m_last_load_time TimePoint
+---@field protected m_initialized boolean
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" } },
+ locales = locales_loaded and __locales or { { name = "English", iso = "en-US" } },
+ m_initialized = false
}
Translator.__index = Translator
-function Translator:Load()
- GVars.backend.language_code = GVars.backend.language_code or "en-US"
- local iso = GVars.backend.language_code
- local ok, res
-
- if (iso ~= "en-US") then
- local path = _F("lib.translations.%s", iso)
- ok, res = pcall(require, path)
+function Translator:MatchGameLanguage()
+ self.m_last_game_lang_idx = LOCALIZATION.GET_CURRENT_LANGUAGE()
+ local idx = GameLangToCustom[self.m_last_game_lang_idx] or 1
+ local match = self.locales[idx]
+ if (not match) then
+ return false
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()
+ GVars.backend.language_index = idx
+ GVars.backend.language_code = match.iso
+ GVars.backend.language_name = match.name
+ return true
+end
+
+function Translator:Load()
+ ThreadManager:Run(function()
+ if (GVars.backend.use_game_language) then
+ self:MatchGameLanguage()
+ end
+
+ GVars.backend.language_code = GVars.backend.language_code or "en-US"
+ local iso = GVars.backend.language_code
+ local ok, res
+
+ if (iso ~= "en-US") then
+ local path = _F("lib.translations.%s", iso)
+ ok, res = pcall(require, path)
+ end
+
+ self.labels = (ok and (type(res) == "table")) and res or self.default_labels
+ self.lang_code = iso
+ self.m_log_history = {}
+ self.m_initialized = true
+ self.m_last_load_time:Reset()
+ end)
end
---@param msg string
@@ -65,15 +103,20 @@ function Translator:Log(message)
end
function Translator:Reload()
- if (not self.m_last_load_time:HasElapsed(3e3)) then
+ if (not self.m_initialized or 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.m_initialized = false
self:Load()
Notifier:ShowMessage("Translator", "Reloaded.")
end
+function Translator:IsReady()
+ return self.m_initialized
+end
+
---@param label string
---@return string
function Translator:GetCache(label)
From 2eaefa741a60af84e5c364fa77f5e9ff929070f1 Mon Sep 17 00:00:00 2001
From: SAMURAI <66764345+xesdoog@users.noreply.github.com>
Date: Tue, 24 Mar 2026 16:28:12 +0100
Subject: [PATCH 4/5] fix(Settings): fix tab switch on language change
---
SSV2/includes/modules/Tab.lua | 13 ++++++++++++-
SSV2/includes/services/Translator.lua | 6 ++++--
2 files changed, 16 insertions(+), 3 deletions(-)
diff --git a/SSV2/includes/modules/Tab.lua b/SSV2/includes/modules/Tab.lua
index adf3c92..b18b396 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
@@ -300,7 +311,7 @@ function Tab:Draw()
return
end
- ImGui.BeginTabBar(_F("##sutab_selector%s", self.m_name))
+ ImGui.BeginTabBar(_F("##sutab_selector%d", self:GetID()))
if (ImGui.BeginTabItem(self:GetName())) then
self:DrawInternal()
ImGui.EndTabItem()
diff --git a/SSV2/includes/services/Translator.lua b/SSV2/includes/services/Translator.lua
index 536acfe..efcf82c 100644
--- a/SSV2/includes/services/Translator.lua
+++ b/SSV2/includes/services/Translator.lua
@@ -66,8 +66,10 @@ function Translator:Load()
self:MatchGameLanguage()
end
- GVars.backend.language_code = GVars.backend.language_code or "en-US"
- local iso = GVars.backend.language_code
+ 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 iso = GVars.backend.language_code
local ok, res
if (iso ~= "en-US") then
From 4e7f0ff33d1d44f060469e2a10d800a3c88bf393 Mon Sep 17 00:00:00 2001
From: SAMURAI <66764345+xesdoog@users.noreply.github.com>
Date: Fri, 27 Mar 2026 18:00:36 +0100
Subject: [PATCH 5/5] fix: more fixes
- Gracefully handle `GUI` errors.
- Remove scattered `ThemeManager` calls.
- Refactor `Translator`.
- Add game language synchronization.
- `Tab` module: Fix ImGui assertion errors when switching languages.
- Fix bursting `EntityForge` thread.
- Fix `lockpv` command not playing an animation when invoked on foot.
- Remove redundant copy in `ThemeEditor`
- Fix notification visibility inside the notification center.
- Add notification callback execution logic *(unused for now)*.
---
SSV2/includes/backend.lua | 7 +-
SSV2/includes/data/pointers.lua | 30 +-
SSV2/includes/data/refs.lua | 26 +-
SSV2/includes/features/EntityForge.lua | 1 +
SSV2/includes/features/YimActionsV3.lua | 5 +-
SSV2/includes/frontend/settings/debug_ui.lua | 24 +-
.../frontend/settings/settings_ui.lua | 39 +-
SSV2/includes/init.lua | 2 +-
SSV2/includes/lib/utils.lua | 28 +-
SSV2/includes/modules/Game.lua | 28 +-
SSV2/includes/modules/Tab.lua | 39 +-
SSV2/includes/modules/Vehicle.lua | 2 +-
SSV2/includes/services/GUI.lua | 411 ++++++++++--------
SSV2/includes/services/Serializer.lua | 4 +-
SSV2/includes/services/ThemeManager.lua | 53 ++-
SSV2/includes/services/ToastNotifier.lua | 48 +-
SSV2/includes/services/Translator.lua | 145 +++---
SSV2/samurais_scripts.lua | 2 +-
18 files changed, 522 insertions(+), 372 deletions(-)
diff --git a/SSV2/includes/backend.lua b/SSV2/includes/backend.lua
index 3c4bd88..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
@@ -493,6 +493,7 @@ function Backend:RegisterHandlers()
PreviewService:Update()
Decorator:CollectGarbage()
+ Translator:OnTick()
yield()
end)
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/frontend/settings/debug_ui.lua b/SSV2/includes/frontend/settings/debug_ui.lua
index 779ee37..acd2076 100644
--- a/SSV2/includes/frontend/settings/debug_ui.lua
+++ b/SSV2/includes/frontend/settings/debug_ui.lua
@@ -410,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
@@ -537,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
diff --git a/SSV2/includes/frontend/settings/settings_ui.lua b/SSV2/includes/frontend/settings/settings_ui.lua
index ceba287..24d478a 100644
--- a/SSV2/includes/frontend/settings/settings_ui.lua
+++ b/SSV2/includes/frontend/settings/settings_ui.lua
@@ -7,11 +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 ThemeManager = require("includes.services.ThemeManager")
+local LOCALES = Translator.locales
local selectedTheme
local newThemeBuff
-local cfgReset = {
+local cfgReset = {
---@type Set
exceptions = Set.new("backend.debug_mode"),
excToggles = {
@@ -24,7 +25,7 @@ local cfgReset = {
},
open = false,
}
-local themeEditor = {
+local themeEditor = {
shouldDraw = false,
liveEdit = false,
shouldFocusName = false,
@@ -49,20 +50,20 @@ local function drawGeneralSettings()
if (Translator and Translator:IsReady()) then
GUI:HeaderText(_T("SETTINGS_LANGUAGE"), { separator = true, spacing = true })
+ ImGui.BeginDisabled(not Translator:CanReload())
GVars.backend.use_game_language = GUI:CustomToggle(_T("SETTINGS_GAME_LANGUAGE"),
GVars.backend.use_game_language,
- { onClick = function() Translator:Reload() end, }
+ { 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)",
- 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.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
@@ -155,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()
@@ -212,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
@@ -375,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
@@ -395,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/init.lua b/SSV2/includes/init.lua
index d3454ac..694f664 100644
--- a/SSV2/includes/init.lua
+++ b/SSV2/includes/init.lua
@@ -94,6 +94,7 @@ 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")
----------------------------------------------------------------------------------------------------
@@ -119,7 +120,6 @@ local packages = {
"modules.LocalPlayer",
"services.GridRenderer",
- "services.Translator",
"frontend.entity_forge_ui",
"frontend.bsv2_ui",
diff --git a/SSV2/includes/lib/utils.lua b/SSV2/includes/lib/utils.lua
index 09ba320..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
@@ -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
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/Tab.lua b/SSV2/includes/modules/Tab.lua
index b18b396..350e340 100644
--- a/SSV2/includes/modules/Tab.lua
+++ b/SSV2/includes/modules/Tab.lua
@@ -276,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
@@ -287,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
@@ -311,27 +314,29 @@ function Tab:Draw()
return
end
- ImGui.BeginTabBar(_F("##sutab_selector%d", self:GetID()))
- 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 fe706cd..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,55 +84,40 @@ 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
-
- ThemeManager:Load()
-
- gui.add_always_draw_imgui(function()
- self:Draw()
- 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)
-
- KeyManager:RegisterKeybind(GVars.keyboard_keybinds.gui_toggle, function()
- self:Toggle()
- end)
-
- Backend:RegisterEventCallback(Enums.eBackendEvent.RELOAD_UNLOAD, function()
- self:Close()
- end)
-
- self:LateInit()
- self.m_initialized = true
+ 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
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 = TABID_MIN
end
@@ -135,14 +127,46 @@ function GUI:LateInit()
GVars.ui.last_tab.array_index = 1
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()
+ end)
+
+ Backend:RegisterEventCallback(Enums.eBackendEvent.RELOAD_UNLOAD, function()
+ self:Close()
+ end)
+
+ ThemeManager:Load()
+
+ self.m_dummy_tab:add_imgui(function() self:DrawDummyTab() end)
+ gui.add_always_draw_imgui(function() self:Draw() end)
+
+ for _, drawfunc in ipairs(self.m_independent_windows) do
+ gui.add_always_draw_imgui(drawfunc)
+ end
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
@@ -527,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)
@@ -556,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 |
@@ -566,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()
@@ -691,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
@@ -817,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)
@@ -897,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
@@ -922,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
@@ -938,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
@@ -980,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
@@ -1068,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 282d38a..7991f7c 100644
--- a/SSV2/includes/services/Serializer.lua
+++ b/SSV2/includes/services/Serializer.lua
@@ -749,9 +749,9 @@ 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
diff --git a/SSV2/includes/services/ThemeManager.lua b/SSV2/includes/services/ThemeManager.lua
index c7a44c5..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
@@ -49,6 +51,10 @@ function ThemeManager:Load()
self.m_current_theme = current
end
+function ThemeManager:GetStackDepth()
+ return self.m_stack_depth
+end
+
---@param name string
---@return Theme?
function ThemeManager:GetTheme(name)
@@ -81,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
@@ -122,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()
@@ -165,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/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 efcf82c..dba400b 100644
--- a/SSV2/includes/services/Translator.lua
+++ b/SSV2/includes/services/Translator.lua
@@ -29,62 +29,110 @@ local GameLangToCustom = {
--------------------------------------
-- 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
+---@field private m_reloading boolean
---@field protected m_initialized boolean
-Translator = {
- default_labels = en_loaded and en or {},
- m_last_load_time = TimePoint.new(),
- m_cache = {},
- locales = locales_loaded and __locales or { { name = "English", iso = "en-US" } },
- m_initialized = false
-}
+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()
- self.m_last_game_lang_idx = LOCALIZATION.GET_CURRENT_LANGUAGE()
- local idx = GameLangToCustom[self.m_last_game_lang_idx] or 1
- local match = self.locales[idx]
- if (not match) then
- return false
- end
+ 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()
ThreadManager:Run(function()
if (GVars.backend.use_game_language) then
- self:MatchGameLanguage()
+ 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 iso = GVars.backend.language_code
- local ok, res
- if (iso ~= "en-US") then
- local path = _F("lib.translations.%s", iso)
+ 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
- self.labels = (ok and (type(res) == "table")) and res or self.default_labels
- self.lang_code = iso
+ 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
+---@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.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
---@return boolean
function Translator:WasLogged(msg)
@@ -104,31 +152,21 @@ function Translator:Log(message)
table.insert(self.m_log_history, message)
end
-function Translator:Reload()
- if (not self.m_initialized or 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.m_initialized = false
- self:Load()
- Notifier:ShowMessage("Translator", "Reloaded.")
-end
-
-function Translator:IsReady()
- return self.m_initialized
+---@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
@@ -137,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
@@ -159,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
@@ -191,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/samurais_scripts.lua b/SSV2/samurais_scripts.lua
index 7cb9fb2..e71dab9 100644
--- a/SSV2/samurais_scripts.lua
+++ b/SSV2/samurais_scripts.lua
@@ -56,7 +56,7 @@ GPointers:Init()
Serializer:FlushObjectQueue()
Backend:RegisterHandlers()
Translator:Load()
-GUI:init()
+GUI:LateInit()
ThreadManager:Run(function()
for name, cmd in pairs(commandRegistry) do