Files
BarotraumaModServer/LocalMods/Neurotrauma/Lua/Scripts/Client/configgui.lua
2026-06-09 00:42:10 +03:00

581 lines
20 KiBLFS
Lua
Executable File

--easysettings by Evil Factory
local easySettings = dofile(NT.Path .. "/Lua/Scripts/Client/easysettings.lua")
local MultiLineTextBox = dofile(NT.Path .. "/Lua/Scripts/Client/MultiLineTextBox.lua")
local GUIComponent = LuaUserData.CreateStatic("Barotrauma.GUIComponent")
local configUI
local BaseConfigPages = {
{ name = "Item Prices", key = "prices" },
{ name = "Item Availability", key = "availability" },
}
-- Did you know the default background colour is not true black?
local Transparent = Color(2, 2, 2)
-- Barotrauma beige
local DefaultTextColour = Color(210, 200, 154)
local ExpansionNameForUI = {}
local function CommaStringToTable(str)
local tbl = {}
for word in string.gmatch(str, "([^,]+)") do
table.insert(tbl, word)
end
return tbl
end
-- Function that autofills the addon dropdown
-- We also automatically make the UI display the names as "NT [mod]" to match their Content Package / Steam Workhop name to minimise confusion.
local function PopulateDropdown(dropdown)
ExpansionNameForUI = {}
-- Goofy but it works!
-- Ensure Neurotrauma is on top by just loading it first
for _, expansion in ipairs(NTConfig.Expansions) do
if expansion.Name == "Neurotrauma" then
local name = tostring(expansion.Name)
local uiName = name
table.insert(ExpansionNameForUI, { uiName, name, type = "expansion" })
dropdown.AddItem(uiName)
end
end
-- Then get all the base pages (expansion proof!)
for _, page in ipairs(BaseConfigPages) do
table.insert(ExpansionNameForUI, { page.name, page.key, type = "page" })
dropdown.AddItem(page.name)
end
-- Then dump all other expansions in there
for _, expansion in ipairs(NTConfig.Expansions) do
if expansion.Name ~= "Neurotrauma" then
local name = tostring(expansion.Name)
-- First, we make the UI Name the same as their expansion name
local uiName = name
-- Add NT at the front if it does not already have that
if not string.find(uiName, "^NT%s") then uiName = "NT " .. uiName end
-- Add them to a lookup table for later
table.insert(ExpansionNameForUI, { uiName, name, type = "expansion" })
-- Finally, add it to the dropdown menu
dropdown.AddItem(uiName)
end
end
end
-- TODO: Readd the difficulty calculation I kinda dont care about it right now so it can fuck off
-- Function to determine how the layout should be structured
local function PrebuildConfigLayout(entries, selectedExpansion)
-- This table will contain subtables that determine how the ConstructUI function actually 'constructs' the UI.
-- These chunks can be gone over using Ipairs to ensure the order is correct (a previous test made unrelated settings render together)
local LayoutChunks = {}
local CurrentGroup = nil
local lastType = nil
local selectedKey = nil
local selectedType = nil
-- Grab actual expansion name based on their UIName
for _, uiEntry in ipairs(ExpansionNameForUI) do
if uiEntry[1] == selectedExpansion then
selectedKey = uiEntry[2]
selectedType = uiEntry.type
break
end
end
for key, entry in pairs(entries) do
if selectedType == "page" then
-- Get entries with this page set
if not entry.page or entry.page ~= selectedKey then goto continue end
elseif selectedType == "expansion" then
-- Get expansion entries that do NOT have a page set (so other expansion can force their price changes into the combined page, for example)
if entry.expansion ~= selectedKey or entry.page ~= nil then goto continue end
end
-- Automatically add some white space between unique item types to prevent bleeding
if entry.type ~= lastType and lastType ~= nil then table.insert(LayoutChunks, { type = "spacer" }) end
lastType = entry.type
-- Categories are always standalone and should be put above the settings they govern
-- Interrupt a group if it exists
if entry.type == "category" then
CurrentGroup = nil
table.insert(LayoutChunks, { type = "category", entry = entry })
-- Floats / Strings until now were just using up way too much screen space. We want to put these into a LayoutGroup together to maximise screen usage.
-- If entries have 'group=true', then all entries of the same type that follow one another will be grouped together. If there are other entry types (like categories) in-between, they will not.
-- This prevents unrelated settings from merging together + adds reverse-compat
elseif entry.type == "float" and entry.group then
-- Make a group and keep adding config entries that come after this as long as the criteria stands
if not CurrentGroup or CurrentGroup.type ~= "float_group" then
CurrentGroup = { type = "float_group", items = {} }
table.insert(LayoutChunks, CurrentGroup)
end
table.insert(CurrentGroup.items, { key = key, entry = entry })
elseif entry.type == "string" and entry.group then
if not CurrentGroup or CurrentGroup.type ~= "string_group" then
CurrentGroup = { type = "string_group", items = {} }
table.insert(LayoutChunks, CurrentGroup)
end
table.insert(CurrentGroup.items, { key = key, entry = entry })
-- Anything else is anything that shouldn't be grouped. Interrupt a group if it exists and add the setting as standalone.
else
CurrentGroup = nil
table.insert(LayoutChunks, { type = "standalone", key = key, entry = entry })
end
::continue::
end
return LayoutChunks
end
local function PopulateSettingsIntoUI(list, selectedExpansion)
list.Content:ClearChildren()
-- Infotext (should always be shown)
local tb_BasicConfigInfo = GUI.TextBlock(
GUI.RectTransform(Vector2(1, 0.1), list.Content.RectTransform),
"Server config can be changed by owner or a client with manage settings permission. If the server doesn't allow writing into the config folder, then it must be edited manually.",
DefaultTextColour,
nil,
GUI.Alignment.Center,
true,
nil,
Transparent
)
-- Prevent textblocks from lighting up the background when clicked / changing the cursor on hover.
tb_BasicConfigInfo.CanBeFocused = false
local LayoutChunks = PrebuildConfigLayout(NTConfig.Entries, selectedExpansion)
-- Go over the pre-generated Layout table and construct settings procedurally
for _, chunk in ipairs(LayoutChunks) do
-- Categories
if chunk.type == "category" then
local tb_ProceduralCategoryHeader = GUI.TextBlock(
GUI.RectTransform(Vector2(1, 0.09), list.Content.RectTransform),
chunk.entry.name,
DefaultTextColour,
GUI.GUIStyle.LargeFont,
GUI.Alignment.BottomCenter,
true,
nil,
Transparent
)
tb_ProceduralCategoryHeader.CanBeFocused = false
-- Standalone items
elseif chunk.type == "standalone" then
local key = chunk.key
local entry = chunk.entry
-- Ungrouped float
if entry.type == "float" then
local minrange = (entry.range and entry.range[1]) or ""
-- This is fucking stupid. If the default value is the same as the minimum value of a scalar, it bugs and displays 0.
-- So, we set the minimum to 0.99 and just render it like it were 1
if minrange == 0.99 then minrange = 1 end
local maxrange = (entry.range and entry.range[2]) or ""
local rect = GUI.RectTransform(Vector2(1, 0.04), list.Content.RectTransform)
local tb_EntryInformation = GUI.TextBlock(
rect,
entry.name .. " (" .. minrange .. "-" .. maxrange .. ")",
DefaultTextColour,
nil,
GUI.Alignment.Center,
true,
nil,
Transparent
)
tb_EntryInformation.CanBeFocused = false
if entry.description then tb_EntryInformation.ToolTip = entry.description end
local scalar =
GUI.NumberInput(GUI.RectTransform(Vector2(1, 0.08), list.Content.RectTransform), NumberType.Float)
scalar.valueStep = 0.1
scalar.MinValueFloat = entry.range and entry.range[1] or 0
scalar.MaxValueFloat = entry.range and entry.range[2] or 100
scalar.FloatValue = NTConfig.Get(key, 1)
scalar.OnValueChanged = function()
NTConfig.Set(key, scalar.FloatValue)
end
-- Bool
elseif entry.type == "bool" then
local rect = GUI.RectTransform(Vector2(0.5, 1), list.Content.RectTransform)
local toggle = GUI.TickBox(rect, entry.name)
if entry.description then toggle.ToolTip = entry.description end
toggle.Selected = NTConfig.Get(key, false)
toggle.OnSelected = function()
NTConfig.Set(key, toggle.State == GUIComponent.ComponentState.Selected)
end
-- String (a textblock to input)
elseif entry.type == "string" then
local style = ""
if entry.style ~= nil then style = " (" .. entry.style .. ")" end
local rect = GUI.RectTransform(Vector2(1, 0.05), list.Content.RectTransform)
local tb_StringInformation = GUI.TextBlock(
rect,
entry.name .. style,
DefaultTextColour,
nil,
GUI.Alignment.Center,
true,
nil,
Transparent
)
-- By default, don't change cursor on hovering
tb_StringInformation.CanBeFocused = false
-- If there's a tooltip, set it and re-enable hovering
if entry.description then
tb_StringInformation.ToolTip = entry.description
tb_StringInformation.CanBeFocused = true
end
local stringinput
-- Make MultiLineTextBox the default, but now allow normal ones too. A single line entry does not need a multi-line textblock.
if entry.noMLTB == true then
stringinput = GUI.TextBox(
GUI.RectTransform(Vector2(1, entry.boxsize), list.Content.RectTransform),
"",
nil,
nil,
nil,
true
)
else
stringinput = MultiLineTextBox(list.Content.RectTransform, "", entry.boxsize)
end
stringinput.Text = table.concat(entry.value, ",")
stringinput.OnTextChangedDelegate = function(textBox)
entry.value = CommaStringToTable(textBox.Text)
end
end
-- Auto-added empty space
elseif chunk.type == "spacer" then
GUI.LayoutGroup(GUI.RectTransform(Vector2(1, 0.02), list.Content.RectTransform), false)
-- Grouped Floats
elseif chunk.type == "float_group" then
local MaxPerRow = 2
local row = nil
local count = 0
for _, item in ipairs(chunk.items) do
-- Make a new LayoutGroup to add entries into everytime the MaxPerRow is hit (default of 2)
if not row or (count % MaxPerRow == 0) then
row = GUI.LayoutGroup(GUI.RectTransform(Vector2(1, 0.09), list.Content.RectTransform), true)
end
-- Safety check!
if not row then return end
-- This determines how the space in the UI is used, tied together to make fucking around a bit easier
row.RelativeSpacing = 0.01
local Text_space = 0.53
local Scalar_space = 0.30
local Reset_space = 0.07
local baseWidth = 1 / MaxPerRow
local textcellwidth = baseWidth * Text_space
local scalarcellwidth = baseWidth * Scalar_space
local resetcellwidth = baseWidth * Reset_space
local key = item.key
local entry = item.entry
local resetButton
-- Make each subdivided part of the group their own
-- Part of the group that holds text
local textcell = GUI.LayoutGroup(GUI.RectTransform(Vector2(textcellwidth, 1), row.RectTransform), false)
-- Part of the group that holds the numberinput box
local scalarcell =
GUI.LayoutGroup(GUI.RectTransform(Vector2(scalarcellwidth, 1), row.RectTransform), false)
-- In case a reset button is set
local resetbuttoncell =
GUI.LayoutGroup(GUI.RectTransform(Vector2(resetcellwidth, 0.59), row.RectTransform), false)
-- Relativespacing but larger space for the center instead of everywhere
local additionalspacecell =
GUI.LayoutGroup(GUI.RectTransform(Vector2(resetcellwidth, 0.59), row.RectTransform), false)
local minrange = entry.range and entry.range[1] or ""
if minrange == 0.99 then minrange = 1 end
local maxrange = entry.range and entry.range[2] or ""
local tb_EntryInformation = GUI.TextBlock(
GUI.RectTransform(Vector2(1, 0.7), textcell.RectTransform),
entry.name .. " (" .. minrange .. "-" .. maxrange .. ")",
DefaultTextColour,
nil,
GUI.Alignment.Center,
true,
nil,
Transparent
)
tb_EntryInformation.CanBeFocused = false
local scalar =
GUI.NumberInput(GUI.RectTransform(Vector2(1, 0.6), scalarcell.RectTransform), NumberType.Float)
scalar.PlusButton.RectTransform.RelativeSize = Vector2(1, 0.5)
scalar.MinusButton.RectTransform.RelativeSize = Vector2(1, 0.5)
scalar.valueStep = 0.1
scalar.MinValueFloat = entry.range and entry.range[1] or 0
scalar.MaxValueFloat = entry.range and entry.range[2] or 100
scalar.FloatValue = NTConfig.Get(key, 1)
scalar.OnValueChanged = function()
NTConfig.Set(key, scalar.FloatValue)
end
-- Leftover space in the Row goes to the reset button if enabled
if entry.resettable then
resetButton = GUI.Button(
GUI.RectTransform(Vector2(1, 1), resetbuttoncell.RectTransform, GUI.Anchor.BottomLeft),
GUI.Alignment.BottomLeft,
nil,
Transparent
)
-- Give the reset button a sprite that matches its function (yoinked from basegame)
local resetButtonStyle =
GUI.Image(GUI.RectTransform(Vector2(1, 1), resetButton.RectTransform), "GUIButtonRefresh")
resetButtonStyle.ToolTip = "Reset to default"
-- On button press, fetch default value.
resetButton.OnClicked = function()
local defaultValue = entry.default
scalar.FloatValue = defaultValue
NTConfig.Set(key, defaultValue)
end
end
count = count + 1
end
-- Grouped Strings
elseif chunk.type == "string_group" then
local MaxPerRow = 2
local row = nil
local count = 0
for _, item in ipairs(chunk.items) do
-- Make a new LayoutGroup to add entries into everytime the MaxPerRow is hit (default of 2)
if not row or (count % MaxPerRow == 0) then
row = GUI.LayoutGroup(GUI.RectTransform(Vector2(1, 0.09), list.Content.RectTransform), true)
end
-- Safety check!
if not row then return end
row.RelativeSpacing = 0.01
local Text_space = 0.50
-- Any less than 0.33 per string and the max value (255,255,255) or 3 digits per value will bleed (at 1920 * 1080 default)
local String_space = 0.33
local Reset_space = 0.07
local baseWidth = 1 / MaxPerRow
local textcellwidth = baseWidth * Text_space
local scalarcellwidth = baseWidth * String_space
local resetcellwidth = baseWidth * Reset_space
local key = item.key
local entry = item.entry
local resetButton
-- Make each subdivided part of the group their own
-- Part of the group that holds text
local textcell = GUI.LayoutGroup(GUI.RectTransform(Vector2(textcellwidth, 1), row.RectTransform), false)
-- Part of the group that holds the input box
local stringinputcell =
GUI.LayoutGroup(GUI.RectTransform(Vector2(scalarcellwidth, 1), row.RectTransform), false)
-- In case a reset button is set
local resetbuttoncell =
GUI.LayoutGroup(GUI.RectTransform(Vector2(resetcellwidth, 0.45), row.RectTransform), false)
-- Relativespacing but larger space for the center instead of everywhere
local additionalspacecell =
GUI.LayoutGroup(GUI.RectTransform(Vector2(resetcellwidth, 0.59), row.RectTransform), false)
local style = ""
if entry.style ~= nil then style = " (" .. entry.style .. ")" end
local tb_StringInformation = GUI.TextBlock(
GUI.RectTransform(Vector2(1, 0.4), textcell.RectTransform),
entry.name .. style,
DefaultTextColour,
nil,
GUI.Alignment.Center,
true,
nil,
Transparent
)
tb_StringInformation.CanBeFocused = false
if entry.description then
tb_StringInformation.ToolTip = entry.description
tb_StringInformation.CanBeFocused = true
end
local stringinput
-- Not all strings need a MultiLineTextBox, especially since they can just yoink the mouse cursor while scrolling.
-- Make MLTB's a toggleable option
if entry.noMLTB == true then
stringinput = GUI.TextBox(
GUI.RectTransform(
Vector2(1, entry.boxsize),
stringinputcell.RectTransform,
GUI.Anchor.CenterLeft
),
"",
nil,
nil,
nil,
true
)
else
stringinput = MultiLineTextBox(stringinputcell.RectTransform, "", entry.boxsize)
end
stringinput.Text = table.concat(entry.value, ",")
stringinput.OnTextChangedDelegate = function(textBox)
entry.value = CommaStringToTable(textBox.Text)
end
-- Leftover space in the Row goes to the reset button if enabled
if entry.resettable then
resetButton = GUI.Button(
GUI.RectTransform(Vector2(1, 1), resetbuttoncell.RectTransform),
GUI.Alignment.CenterRight,
nil,
Transparent
)
-- Give the reset button a sprite that matches its function (yoinked from basegame)
local resetButtonStyle =
GUI.Image(GUI.RectTransform(Vector2(1, 1), resetButton.RectTransform), "GUIButtonRefresh")
resetButtonStyle.ToolTip = "Reset to default"
-- On button press, fetch default value.
resetButton.OnClicked = function()
entry.value = entry.default
stringinput.Text = table.concat(entry.value, ",")
end
end
count = count + 1
end
end
end
if Game.IsMultiplayer and not Game.Client.HasPermission(ClientPermissions.ManageSettings) then
for guicomponent in list.GetAllChildren() do
guicomponent.enabled = false
end
end
return list
end
-- Base UI construction
local function ConstructUI(parent)
-- Set the default to display (Neurotrauma, duh)
local selectedExpansion = BaseConfigPages[1].name
local list = easySettings.BasicList(parent)
-- Get the Title block to put the drop down menu into (could be moved tbh)
local innerLayout = list.Parent
local children = innerLayout.RectTransform.Children
local title = children[1].GUIComponent
-- Get the amount of loaded expansions + seperate pages
local dropdownheight = #NTConfig.Expansions + #BaseConfigPages
-- Don't show the dropdown if we only have Neurotrauma or no other addons that have settings to show
if dropdownheight > 1 then
local dropdown_AddonSelection = GUI.DropDown(
GUI.RectTransform(Vector2(0.18, 1), title.RectTransform),
"",
dropdownheight - 2,
nil,
false,
false,
GUI.Alignment.CenterLeft
)
dropdown_AddonSelection.ListBox.RectTransform.RelativeOffset = (Vector2(0, 0.5))
PopulateDropdown(dropdown_AddonSelection)
dropdown_AddonSelection.Select(0)
selectedExpansion = dropdown_AddonSelection.Text
dropdown_AddonSelection.ToolTip = "Choose which mod's settings to display."
-- Using the dropdown changes a variable so we can change the page accordingly; only do so if we're not already on that page
dropdown_AddonSelection.OnSelected = function(guiComponent)
local newSelection = tostring(guiComponent.Text)
-- Check if changed
if newSelection == selectedExpansion then return end
selectedExpansion = newSelection
-- Redo content based on new selection
PopulateSettingsIntoUI(list, selectedExpansion)
end
end
-- Default
PopulateSettingsIntoUI(list, selectedExpansion)
end
Networking.Receive("NT.ConfigUpdate", function(msg)
NTConfig.ReceiveConfig(msg)
if configUI == nil then return end
if configUI.RectTransform == nil then return end
local parent = configUI.RectTransform.Parent
configUI = nil
configUI = ConstructUI(parent)
end)
easySettings.AddMenu("Neurotrauma", function(parent)
if Game.IsMultiplayer then
local msg = Networking.Start("NT.ConfigRequest")
Networking.Send(msg)
end
configUI = ConstructUI(parent)
end)