Add other mods

This commit is contained in:
2025-03-31 13:19:47 +02:00
parent bdc5488720
commit be593696b2
2266 changed files with 109313 additions and 512 deletions

View File

@@ -0,0 +1,47 @@
--this is the entry point for the code. This runs all other scripts.
--get the local path and save it as a global. only autorun files can get the path in this way!
blue_prints = {}
blue_prints.path = ...
-- Always use forward slashes internally, both Windows and Linux can handle this
blue_prints.path = blue_prints.path and blue_prints.path:gsub("\\", "/") or ""
-- Set up save path - will be normalized in read_write.lua functions
blue_prints.save_path = "LocalMods/Blueprints_saved_blueprints"
blue_prints.most_recent_circuitbox = nil
blue_prints.time_delay_between_loops = 150
blue_prints.component_batch_size = 10
blue_prints.current_gui_page = nil
blue_prints.most_recently_used_blueprint_name = nil
blue_prints.most_recent_folder = "[Root Directory]" -- Default to root directory
blue_prints.unit_tests_enabled = false
dofile(blue_prints.path .. "/Lua/gui/cs_required_warning.lua")
if CSActive then --CSActive is if csharp scripts are enabled. This mod requires them.
--setup
dofile(blue_prints.path .. "/Lua/register_types.lua")
dofile(blue_prints.path .. "/Lua/utilities/read_write.lua") -- Load read_write before first_time_setup
dofile(blue_prints.path .. "/Lua/first_time_setup.lua")
--utilities
dofile(blue_prints.path .. "/Lua/utilities/utilities.lua")
dofile(blue_prints.path .. "/Lua/utilities/safety_checks.lua")
--core logic
dofile(blue_prints.path .. "/Lua/save_blueprint.lua")
dofile(blue_prints.path .. "/Lua/load_blueprint.lua")
dofile(blue_prints.path .. "/Lua/delete_blueprint.lua")
dofile(blue_prints.path .. "/Lua/commands.lua")
dofile(blue_prints.path .. "/Lua/unit_tests.lua")
--gui
dofile(blue_prints.path .. "/Lua/gui/gui_buttons_frame.lua")
dofile(blue_prints.path .. "/Lua/gui/load_gui.lua")
dofile(blue_prints.path .. "/Lua/gui/save_gui.lua")
dofile(blue_prints.path .. "/Lua/gui/clear_gui.lua")
dofile(blue_prints.path .. "/Lua/gui/delay_slider.lua")
dofile(blue_prints.path .. "/Lua/gui/popup_gui.lua")
--dofile(blue_prints.path .. "/Lua/gui/custom_gui_example.lua")
end

115
Blueprints/Lua/commands.lua Normal file
View File

@@ -0,0 +1,115 @@
if SERVER then return end --prevents it from running on the server
local configDescriptions = {}
configDescriptions["commands"] = "you can use blueprints or bp"
configDescriptions["load"] = "load a blueprint. EX: bp load reactor_controller"
configDescriptions["save"] = "save a blueprint. EX: bp save reactor_controller"
configDescriptions["need"] = "get requirements for a blueprint. EX: bp need reactor_controller"
configDescriptions["delete"] = "delete a blueprint. EX: bp delete reactor_controller"
configDescriptions["list"] = "list all saved files. EX: bp list"
configDescriptions["toggle"] = "toggle things on and off. EX: bp toggle tests"
configDescriptions["clear"] = "Remove all components and labels from a circuitbox. EX: bp clear"
local function checkStringAgainstTags(targetString, tags) --this is needed to run the command line args
for tag, _ in pairs(tags) do
if targetString == tag then
return true -- Match found
end
end
return false -- No match found
end
local function runCommand(command)
if command[1] == nil or command[1] == "help" or command[1] == "commands" then
for key, value in pairs(configDescriptions) do
print(key .. ": " .. value)
end
end
if command[1] == "load" then
if command[2] ~= nil then
print("Attempting to build blueprint")
blue_prints.construct_blueprint(command[2])
else
print("No filename given. EX: bp load file_name.txt")
end
end
if command[1] == "save" then
if command[2] ~= nil then
print("Attempting to save blueprint")
blue_prints.save_blueprint(command[2])
else
print("No filename given. EX: bp save file_name.txt")
end
end
if command[1] == "need" then
if command[2] ~= nil then
print("Attempting to get blueprint requirements")
blue_prints.print_requirements_of_circuit(command[2])
blue_prints.check_what_is_needed_for_blueprint(command[2])
else
print("No filename given. EX: bp need file_name.txt")
end
end
if command[1] == "delete" then
if command[2] ~= nil then
print("Attempting to delete blueprint")
blue_prints.delete_blueprint(command[2])
else
print("No filename given. EX: bp delete file_name.txt")
end
end
if command[1] == "clear" then
blue_prints.clear_circuitbox()
end
if command[1] == "list" then
blue_prints.print_all_saved_files()
end
if command[1] == "unit_tests" then
blue_prints.unit_tests_enabled = true
blue_prints.unit_test_all_blueprint_files()
end
if command[1] == "toggle" then
if command[2] == "tests" then
blue_prints.unit_tests_enabled = not blue_prints.unit_tests_enabled
print("tests enabled: " .. tostring(blue_prints.unit_tests_enabled))
end
end
if checkStringAgainstTags(command[1], configDescriptions) then
--print("Match found!")
else
print("Command not recognized. type bp to see available commands.")
end
end
Game.AddCommand("blueprints", "configures blueprints", function (command)
runCommand(command)
end)
Game.AddCommand("bp", "configures blueprints abbreviated", function (command)
runCommand(command)
end)

View File

@@ -0,0 +1,31 @@
if SERVER then return end --prevents it from running on the server
function blue_prints.delete_blueprint(provided_path)
-- Check if the filename already ends with .txt
if not string.match(provided_path, "%.txt$") then
-- Add .txt if it's not already present
provided_path = provided_path .. ".txt"
end
local file_path = blue_prints.normalizePath(blue_prints.save_path .. "/" .. provided_path)
if File.Exists(file_path) then
local success = blue_prints.safeFileOperation(os.remove, file_path)
if success then
print("File deleted successfully.")
else
-- Try alternate path if first attempt fails
local alt_path = file_path:gsub("LocalMods/", "local_mods/")
success = blue_prints.safeFileOperation(os.remove, alt_path)
if success then
print("File deleted successfully.")
else
print("Error deleting file")
end
end
else
print("file not found")
print("saved designs:")
blue_prints.print_all_saved_files()
end
end

View File

@@ -0,0 +1,81 @@
if SERVER then return end --prevents it from running on the server
-- Function to write text to a file
local function writeFile(path, text)
return blue_prints.writeFile(path, text)
end
-- Recursively copy a directory structure
local function copy_directory_structure(source, destination)
-- Normalize paths
source = blue_prints.normalizePath(source)
destination = blue_prints.normalizePath(destination)
-- Ensure destination exists
if not blue_prints.createFolder(destination) then
print("Failed to create destination directory: " .. destination)
return false
end
-- Get all files and directories using our safe functions
local files = blue_prints.getFiles(source)
local directories = blue_prints.getDirectories(source)
-- Copy files
for _, filepath in pairs(files) do
if string.match(filepath, "%.txt$") then
local filename = filepath:match("([^/\\]+)$")
local file_content = blue_prints.readFileContents(filepath)
if file_content then
local dest_path = blue_prints.normalizePath(destination .. "/" .. filename)
if not File.Exists(dest_path) then
writeFile(dest_path, file_content)
end
end
end
end
-- Recursively copy subdirectories
for _, dir in pairs(directories) do
local dir_name = dir:match("([^/\\]+)$")
if dir_name then
local source_subdir = blue_prints.normalizePath(source .. "/" .. dir_name)
local dest_subdir = blue_prints.normalizePath(destination .. "/" .. dir_name)
copy_directory_structure(source_subdir, dest_subdir)
end
end
return true
end
-- Create base directory first
if not blue_prints.ensureBaseDirectory() then
print("Failed to create blueprint directory")
return nil
end
-- Check if this is first run by looking for existing content
local existing_files = blue_prints.getFiles(blue_prints.save_path)
local existing_dirs = blue_prints.getDirectories(blue_prints.save_path)
local is_first_run = #existing_files == 0 and #existing_dirs == 0
if is_first_run then
-- Look for starter_blueprints directory
local starter_blueprints_path = blue_prints.normalizePath(blue_prints.path .. "/starter_blueprints")
if File.DirectoryExists(starter_blueprints_path) then
-- Copy the entire directory structure
if copy_directory_structure(starter_blueprints_path, blue_prints.save_path) then
print("Successfully copied starter blueprints")
else
print("Failed to copy starter blueprints")
end
else
print("No starter_blueprints directory found at: " .. starter_blueprints_path)
-- Create default folders if no starter blueprints exist
local defaultFolders = {"General", "Reactor", "Navigation", "Weapons", "Medical"}
for _, folder in ipairs(defaultFolders) do
local folderPath = blue_prints.normalizePath(blue_prints.save_path .. "/" .. folder)
blue_prints.createFolder(folderPath)
end
end
end

View File

@@ -0,0 +1,54 @@
if SERVER then return end -- we don't want server to run GUI code.
local resolution = blue_prints.getScreenResolution()
local run_once_at_start = false
local function check_and_rebuild_frame()
local new_resolution = blue_prints.getScreenResolution()
if new_resolution ~= resolution or run_once_at_start == false then
local spacer = GUI.TextBlock(GUI.RectTransform(Vector2(1, 0.04), blue_prints.gui_button_frame_list.Content.RectTransform), "", nil, nil, GUI.Alignment.Center)
local button = GUI.Button(GUI.RectTransform(Vector2(1, 0.1), blue_prints.gui_button_frame_list.Content.RectTransform), "Clear Circuitbox", GUI.Alignment.Center, "GUIButtonSmall")
button.OnClicked = function ()
local message_box = GUI.MessageBox('Are you sure you want to clear the box?', 'This will remove all components, labels and wires.', {'Cancel', 'Clear Box'})
local cancel_button = nil
local clear_button = nil
if message_box.Buttons[0] == nil then --this is if no one has registered it. If some other mod registers it I dont want it to break.
cancel_button = message_box.Buttons[1]
clear_button = message_box.Buttons[2]
else --if its been registered, it will behave as a csharp table
cancel_button = message_box.Buttons[0]
clear_button = message_box.Buttons[1]
end
clear_button.Color = Color(160, 160, 255) -- Base color (more blue)
clear_button.HoverColor = Color(190, 190, 255) -- Lighter blue when hovering
cancel_button.OnClicked = function ()
message_box.Close()
end
clear_button.OnClicked = function ()
blue_prints.clear_circuitbox()
GUI.AddMessage('Circuitbox Cleared', Color.White)
message_box.Close()
end
end
resolution = new_resolution
run_once_at_start = true
end
end
Hook.Patch("Barotrauma.Items.Components.CircuitBox", "AddToGUIUpdateList", function()
check_and_rebuild_frame()
end, Hook.HookMethodType.After)

View File

@@ -0,0 +1,43 @@
if SERVER then return end -- we don't want server to run GUI code.
if CSActive then return end -- dont show the warning if CS is on
-- our main frame where we will put our custom GUI
local frame = GUI.Frame(GUI.RectTransform(Vector2(1, 1)), nil)
frame.CanBeFocused = false
-- popup frame
local popup = GUI.Frame(GUI.RectTransform(Vector2(1, 1), frame.RectTransform, GUI.Anchor.Center), nil)
popup.CanBeFocused = false
popup.Visible = true
local popupContent = GUI.Frame(GUI.RectTransform(Vector2(0.4, 0.6), popup.RectTransform, GUI.Anchor.Center))
local popupList = GUI.ListBox(GUI.RectTransform(Vector2(1, 1), popupContent.RectTransform, GUI.Anchor.BottomCenter))
GUI.TextBlock(GUI.RectTransform(Vector2(1, 0.15), popupList.Content.RectTransform), "WARNING", nil, nil, GUI.Alignment.Center)
GUI.TextBlock(GUI.RectTransform(Vector2(1, 0.05), popupList.Content.RectTransform), "You are using Blueprints without enabling csharp scripting.", nil, nil, GUI.Alignment.Center)
GUI.TextBlock(GUI.RectTransform(Vector2(1, 0.05), popupList.Content.RectTransform), 'Go to the main menu. (which has singleplayer, multiplayer, etc)', nil, nil, GUI.Alignment.Center)
GUI.TextBlock(GUI.RectTransform(Vector2(1, 0.05), popupList.Content.RectTransform), 'In the main menu, click the "Open LuaCs Settings" button in the top left.', nil, nil, GUI.Alignment.Center)
GUI.TextBlock(GUI.RectTransform(Vector2(1, 0.05), popupList.Content.RectTransform), 'Then hit the "enable csharp scripting" check box.', nil, nil, GUI.Alignment.Center)
local coloredText = GUI.TextBlock(GUI.RectTransform(Vector2(1, 0.10), popupList.Content.RectTransform), "Blueprints will not function without this.", nil, nil, GUI.Alignment.Center)
coloredText.TextColor = Color(255, 0, 0) --red
GUI.TextBlock(GUI.RectTransform(Vector2(1, 0.10), popupList.Content.RectTransform), '', nil, nil, GUI.Alignment.Center)
local closeButton = GUI.Button(GUI.RectTransform(Vector2(1, 0.1), popupList.Content.RectTransform), "Close", GUI.Alignment.Center, "GUIButtonSmall")
closeButton.OnClicked = function ()
popup.Visible = not popup.Visible
end
Hook.Patch("Barotrauma.GameScreen", "AddToGUIUpdateList", function()
frame.AddToGUIUpdateList()
end, Hook.HookMethodType.After)
Hook.Patch("Barotrauma.NetLobbyScreen", "AddToGUIUpdateList", function(self, ptable)
frame.AddToGUIUpdateList()
end, Hook.HookMethodType.After)
Hook.Patch("Barotrauma.SubEditorScreen", "AddToGUIUpdateList", function()
frame.AddToGUIUpdateList()
end, Hook.HookMethodType.After)

View File

@@ -0,0 +1,108 @@
--[[
This example shows how to create a basic custom GUI. The GUI will appear top right of your in game screen.
--]]
if SERVER then return end -- we don't want server to run GUI code.
local modPath = ...
-- our main frame where we will put our custom GUI
local frame = GUI.Frame(GUI.RectTransform(Vector2(1, 1)), nil)
frame.CanBeFocused = false
-- menu frame
local menu = GUI.Frame(GUI.RectTransform(Vector2(1, 1), frame.RectTransform, GUI.Anchor.Center), nil)
menu.CanBeFocused = false
menu.Visible = false
-- put a button that goes behind the menu content, so we can close it when we click outside
local closeButton = GUI.Button(GUI.RectTransform(Vector2(1, 1), menu.RectTransform, GUI.Anchor.Center), "", GUI.Alignment.Center, nil)
closeButton.OnClicked = function ()
menu.Visible = not menu.Visible
end
-- a button top right of our screen to open a sub-frame menu
local button = GUI.Button(GUI.RectTransform(Vector2(0.2, 0.2), frame.RectTransform, GUI.Anchor.TopRight), "Custom GUI Example", GUI.Alignment.Center, "GUIButtonSmall")
button.RectTransform.AbsoluteOffset = Point(25, 200)
button.OnClicked = function ()
menu.Visible = not menu.Visible
end
local menuContent = GUI.Frame(GUI.RectTransform(Vector2(0.4, 0.6), menu.RectTransform, GUI.Anchor.Center))
local menuList = GUI.ListBox(GUI.RectTransform(Vector2(1, 1), menuContent.RectTransform, GUI.Anchor.BottomCenter))
GUI.TextBlock(GUI.RectTransform(Vector2(1, 0.05), menuList.Content.RectTransform), "This is a sample text!", nil, nil, GUI.Alignment.Center)
for i = 1, 10, 1 do
local coloredText = GUI.TextBlock(GUI.RectTransform(Vector2(1, 0.025), menuList.Content.RectTransform), "This is some colored text!", nil, nil, GUI.Alignment.Center)
coloredText.TextColor = Color(math.random(0, 255), math.random(0, 255), math.random(0, 255))
end
local textBox = GUI.TextBox(GUI.RectTransform(Vector2(1, 0.2), menuList.Content.RectTransform), "This is a text box")
textBox.OnTextChangedDelegate = function (textBox)
print(textBox.Text)
end
local tickBox = GUI.TickBox(GUI.RectTransform(Vector2(1, 0.2), menuList.Content.RectTransform), "This is a tick box")
tickBox.Selected = true
tickBox.OnSelected = function ()
print(tickBox.Selected)
end
local numberInput = GUI.NumberInput(GUI.RectTransform(Vector2(1, 0.1), menuList.Content.RectTransform), NumberType.Float)
numberInput.MinValueFloat = 0
numberInput.MaxValueFloat = 1000
numberInput.valueStep = 1
numberInput.OnValueChanged = function ()
print(numberInput.FloatValue)
end
local scrollBar = GUI.ScrollBar(GUI.RectTransform(Vector2(1, 0.1), menuList.Content.RectTransform), 0.1, nil, "GUISlider")
scrollBar.Range = Vector2(0, 100)
scrollBar.BarScrollValue = 50
scrollBar.OnMoved = function ()
print(scrollBar.BarScrollValue)
end
local someButton = GUI.Button(GUI.RectTransform(Vector2(1, 0.1), menuList.Content.RectTransform), "This is a button", GUI.Alignment.Center, "GUIButtonSmall")
someButton.OnClicked = function ()
print("button")
end
local dropDown = GUI.DropDown(GUI.RectTransform(Vector2(1, 0.05), menuList.Content.RectTransform), "This is a dropdown", 3, nil, false)
dropDown.AddItem("First Item", 0)
dropDown.AddItem("Second Item", 1)
dropDown.AddItem("Third Item", 2)
dropDown.OnSelected = function (guiComponent, object)
print(object)
end
local multiDropDown = GUI.DropDown(GUI.RectTransform(Vector2(1, 0.05), menuList.Content.RectTransform), "This is a multi-dropdown", 3, nil, true)
multiDropDown.AddItem("First Item", 0)
multiDropDown.AddItem("Second Item", 1)
multiDropDown.AddItem("Third Item", 2)
multiDropDown.OnSelected = function (guiComponent, object)
for value in multiDropDown.SelectedDataMultiple do
print(value)
end
end
local imageFrame = GUI.Frame(GUI.RectTransform(Point(65, 65), menuList.Content.RectTransform), "GUITextBox")
imageFrame.RectTransform.MinSize = Point(0, 65)
local sprite = ItemPrefab.GetItemPrefab("bandage").InventoryIcon
local image = GUI.Image(GUI.RectTransform(Vector2(1, 1), imageFrame.RectTransform, GUI.Anchor.Center), sprite)
image.ToolTip = "Bandages are pretty cool"
local customImageFrame = GUI.Frame(GUI.RectTransform(Point(128, 128), menuList.Content.RectTransform), "GUITextBox")
customImageFrame.RectTransform.MinSize = Point(138, 138)
--local customSprite = Sprite(modPath .. "/luasmall.png")
GUI.Image(GUI.RectTransform(Point(65, 65), customImageFrame.RectTransform, GUI.Anchor.Center), customSprite)
Hook.Patch("Barotrauma.GameScreen", "AddToGUIUpdateList", function()
frame.AddToGUIUpdateList()
end)
Hook.Patch("Barotrauma.SubEditorScreen", "AddToGUIUpdateList", function()
frame.AddToGUIUpdateList()
end)

View File

@@ -0,0 +1,37 @@
if SERVER then return end
local resolution = blue_prints.getScreenResolution()
local run_once_at_start = false
local function check_and_rebuild_frame()
local new_resolution = blue_prints.getScreenResolution()
if new_resolution ~= resolution or run_once_at_start == false then
local spacer = GUI.TextBlock(GUI.RectTransform(Vector2(1, 0.08), blue_prints.gui_button_frame_list.Content.RectTransform), "", nil, nil, GUI.Alignment.Center)
-- Create the label
local label = GUI.TextBlock(GUI.RectTransform(Vector2(1, 0.08), blue_prints.gui_button_frame_list.Content.RectTransform), "Load/Clear Delay", nil, nil, GUI.Alignment.Center)
local scrollBar = GUI.ScrollBar(GUI.RectTransform(Vector2(1, 0.1), blue_prints.gui_button_frame_list.Content.RectTransform), 0.1, nil, "GUISlider")
scrollBar.Range = Vector2(150, 1000)
if run_once_at_start == false then
scrollBar.BarScrollValue = blue_prints.time_delay_between_loops
run_once_at_start = true
end
scrollBar.OnMoved = function ()
local truncatedValue = math.floor(scrollBar.BarScrollValue) -- Truncate to nearest integer
scrollBar.ToolTip = "Delay for loading. Increase on laggier servers. Current Value: " .. truncatedValue .. "ms"
--print(truncatedValue)
blue_prints.time_delay_between_loops = truncatedValue
end
resolution = new_resolution
end
end
Hook.Patch("Barotrauma.Items.Components.CircuitBox", "AddToGUIUpdateList", function()
check_and_rebuild_frame()
end, Hook.HookMethodType.After)

View File

@@ -0,0 +1,33 @@
if SERVER then return end -- we don't want server to run GUI code.
local resolution = blue_prints.getScreenResolution()
local run_once_at_start = false
--gui frame to hold the UI buttons that open the various GUIs
blue_prints.gui_button_frame = GUI.Frame(GUI.RectTransform(Vector2(1, 1)), nil)
blue_prints.gui_button_frame.CanBeFocused = false
blue_prints.gui_button_frame.Visible = true
--this is needed to handle different resolutions
local function check_and_rebuild_frame()
local new_resolution = blue_prints.getScreenResolution()
if new_resolution ~= resolution or run_once_at_start == false then
blue_prints.gui_button_frame = GUI.Frame(GUI.RectTransform(Vector2(1, 1)), nil)
blue_prints.gui_button_frame.CanBeFocused = false
blue_prints.gui_button_frame_content = GUI.Frame(GUI.RectTransform(Vector2(0.1, 0.15), blue_prints.gui_button_frame.RectTransform, GUI.Anchor.CenterRight))
blue_prints.gui_button_frame_list = GUI.ListBox(GUI.RectTransform(Vector2(1, 1), blue_prints.gui_button_frame_content.RectTransform, GUI.Anchor.BottomCenter))
local spacer = GUI.TextBlock(GUI.RectTransform(Vector2(1, 0.04), blue_prints.gui_button_frame_list.Content.RectTransform), "", nil, nil, GUI.Alignment.Center)
local title = GUI.TextBlock(GUI.RectTransform(Vector2(1, 0.08), blue_prints.gui_button_frame_list.Content.RectTransform), "Blueprints", nil, nil, GUI.Alignment.Center)
title.TextColor = Color(180, 180, 255)
resolution = new_resolution
run_once_at_start = true
end
end
Hook.Patch("Barotrauma.Items.Components.CircuitBox", "AddToGUIUpdateList", function()
check_and_rebuild_frame()
blue_prints.gui_button_frame.AddToGUIUpdateList()
end, Hook.HookMethodType.After)

View File

@@ -0,0 +1,309 @@
if SERVER then return end
local resolution = blue_prints.getScreenResolution()
local run_once_at_start = false
local folder_states = {} -- Track collapsed state of folders
local button_height = 45
-- Function to move blueprints from a folder to root
local function moveFilesToRoot(folderPath)
if folderPath == "[Root Directory]" then return true end
local fullFolderPath = blue_prints.normalizePath(blue_prints.save_path .. "/" .. folderPath)
local rootPath = blue_prints.normalizePath(blue_prints.save_path)
local files = File.GetFiles(fullFolderPath)
local success = true
if files then
for _, filepath in ipairs(files) do
if string.match(filepath, "%.txt$") then
-- Read file content
local content = blue_prints.readFileContents(filepath)
if content then
-- Get just the filename
local filename = filepath:match("([^/\\]+)$")
-- Create new path in root
local newPath = blue_prints.normalizePath(rootPath .. "/" .. filename)
-- Write to new location
if not blue_prints.writeFile(newPath, content) then
success = false
end
end
end
end
end
return success
end
-- Function to delete a folder and its contents
local function deleteFolder(folderPath)
if folderPath == "[Root Directory]" then return false end
local fullFolderPath = blue_prints.normalizePath(blue_prints.save_path .. "/" .. folderPath)
-- Move all files to root first
if not moveFilesToRoot(folderPath) then
return false
end
-- Try to delete the folder
local success = pcall(function()
File.DeleteDirectory(fullFolderPath)
end)
-- If first attempt fails, try alternate path
if not success then
local altPath = fullFolderPath:gsub("LocalMods/", "local_mods/")
success = pcall(function()
File.DeleteDirectory(altPath)
end)
end
return success
end
local function count_blueprints_in_folder(folderPath)
local files = File.GetFiles(folderPath)
local count = 0
if files then
for _, filepath in ipairs(files) do
if string.match(filepath, "%.txt$") then
count = count + 1
end
end
end
return count
end
local function formatFolderHeaderText(folderName, isExpanded, blueprintCount)
return string.format("%s %s (%d blueprints)",
isExpanded and "" or "",
folderName,
blueprintCount)
end
local function generate_load_gui()
blue_prints.current_gui_page = GUI.Frame(GUI.RectTransform(Vector2(1, 1), blue_prints.gui_button_frame.RectTransform, GUI.Anchor.Center),
nil)
blue_prints.current_gui_page.CanBeFocused = false
blue_prints.current_gui_page.Visible = false
-- Background close button
local closeButton = GUI.Button(
GUI.RectTransform(Vector2(1, 1), blue_prints.current_gui_page.RectTransform, GUI.Anchor.Center), "",
GUI.Alignment.Center, nil)
closeButton.OnClicked = function()
blue_prints.current_gui_page.Visible = not blue_prints.current_gui_page.Visible
end
local menuContent = GUI.Frame(GUI.RectTransform(Vector2(0.4, 0.6), blue_prints.current_gui_page.RectTransform,
GUI.Anchor.Center))
local mainList = GUI.ListBox(GUI.RectTransform(Vector2(1, 1), menuContent.RectTransform, GUI.Anchor.BottomCenter))
-- Title
local title_text = GUI.TextBlock(GUI.RectTransform(Vector2(1, 0.08), mainList.Content.RectTransform),
"LOAD BLUEPRINT", nil, nil, GUI.Alignment.Center)
title_text.TextScale = 1.5
title_text.TextColor = Color(200, 200, 200)
-- Instructions
local instruction_text = GUI.TextBlock(GUI.RectTransform(Vector2(1, 0.25), mainList.Content.RectTransform),
'Click a button to load. Hover over the button to see its description. Click folder headers to expand/collapse.\n\n' ..
'If the base component is not available, FPGAs will be used instead. These components must be in your main inventory, not a toolbelt/backpack etc.\n\n' ..
'Click anywhere outside this box to cancel.',
nil, nil, GUI.Alignment.TopLeft)
instruction_text.Wrap = true
instruction_text.TextColor = Color(200, 200, 200)
instruction_text.Padding = Vector4(10, 5, 10, 5)
local function createBlueprintButton(filename, filepath, description, componentCount, contentList)
local buttonContainer = GUI.Frame(GUI.RectTransform(Vector2(1, 0.10), contentList.Content.RectTransform,
GUI.Anchor.TopCenter))
buttonContainer.RectTransform.MinSize = Point(0, button_height)
buttonContainer.RectTransform.MaxSize = Point(9999, button_height)
local button_label = filename .. " - " .. tostring(componentCount) .. " FPGAs"
local leftButton = GUI.Button(
GUI.RectTransform(Vector2(0.90, 1), buttonContainer.RectTransform, GUI.Anchor.CenterLeft),
button_label, GUI.Alignment.CenterLeft, "GUIButtonSmall")
leftButton.TextBlock.Padding = Vector4(80, 0, 0, 0)
if description then
description = description:gsub("
", "\n"):gsub("
", "\n")
leftButton.ToolTip = description
else
leftButton.ToolTip = "No description available"
end
leftButton.OnClicked = function()
blue_prints.construct_blueprint(filepath)
blue_prints.current_gui_page.Visible = false
end
local rightButton = GUI.Button(
GUI.RectTransform(Vector2(0.10, 1), buttonContainer.RectTransform, GUI.Anchor.CenterRight),
"Delete", GUI.Alignment.Center, "GUIButtonSmall")
rightButton.ToolTip = "Delete " .. filename
rightButton.Color = Color(255, 80, 80)
rightButton.HoverColor = Color(255, 120, 120)
rightButton.OnClicked = function()
local message_box = GUI.MessageBox('Delete Blueprint?',
'Are you sure you want to delete "' .. filename .. '"?',
{ 'Cancel', 'Delete' })
local cancel_button = nil
local delete_button = nil
if message_box.Buttons[0] == nil then
cancel_button = message_box.Buttons[1]
delete_button = message_box.Buttons[2]
else
cancel_button = message_box.Buttons[0]
delete_button = message_box.Buttons[1]
end
delete_button.Color = Color(255, 80, 80)
delete_button.HoverColor = Color(255, 120, 120)
cancel_button.OnClicked = function() message_box.Close() end
delete_button.OnClicked = function()
blue_prints.delete_blueprint(filepath)
blue_prints.current_gui_page.Visible = false
GUI.AddMessage('Blueprint Deleted', Color.White)
message_box.Close()
end
end
end
-- Process folders
local folders = blue_prints.getFolderList()
for _, folderName in ipairs(folders) do
local folderPath = folderName == "[Root Directory]" and "" or folderName
local fullFolderPath = blue_prints.normalizePath(blue_prints.save_path .. "/" .. folderPath)
local blueprintCount = count_blueprints_in_folder(fullFolderPath)
folder_states[folderName] = folder_states[folderName] or false
-- Create folder container
local folderContainer = GUI.Frame(GUI.RectTransform(Vector2(1, 0.10), mainList.Content.RectTransform,
GUI.Anchor.TopCenter))
folderContainer.RectTransform.MinSize = Point(0, button_height)
folderContainer.RectTransform.MaxSize = Point(9999, button_height)
-- Create folder button with 90% width
local headerButton = GUI.Button(
GUI.RectTransform(Vector2(0.90, 1), folderContainer.RectTransform, GUI.Anchor.CenterLeft),
formatFolderHeaderText(folderName, folder_states[folderName], blueprintCount),
GUI.Alignment.CenterLeft, "GUIButtonSmall")
headerButton.TextColor = Color(150, 150, 255)
headerButton.HoverColor = Color(180, 180, 255, 0.5)
headerButton.ForceUpperCase = 1
headerButton.TextBlock.Padding = Vector4(0, 0, 0, 0)
-- Add delete button for folder (except root)
if folderName ~= "[Root Directory]" then
local deleteButton = GUI.Button(
GUI.RectTransform(Vector2(0.10, 1), folderContainer.RectTransform, GUI.Anchor.CenterRight),
"Delete", GUI.Alignment.Center, "GUIButtonSmall")
deleteButton.ToolTip = "Delete folder and move contents to root"
deleteButton.Color = Color(255, 80, 80)
deleteButton.HoverColor = Color(255, 120, 120)
deleteButton.OnClicked = function()
local message_box = GUI.MessageBox('Delete Folder?',
'Are you sure you want to delete "' ..
folderName .. '"?\nAll blueprints will be moved to the root folder.',
{ 'Cancel', 'Delete' })
local cancel_button = nil
local delete_button = nil
if message_box.Buttons[0] == nil then
cancel_button = message_box.Buttons[1]
delete_button = message_box.Buttons[2]
else
cancel_button = message_box.Buttons[0]
delete_button = message_box.Buttons[1]
end
delete_button.Color = Color(255, 80, 80)
delete_button.HoverColor = Color(255, 120, 120)
cancel_button.OnClicked = function() message_box.Close() end
delete_button.OnClicked = function()
if deleteFolder(folderName) then
blue_prints.current_gui_page.Visible = false
GUI.AddMessage('Folder Deleted', Color.White)
message_box.Close()
-- Regenerate GUI to show updated folder structure
Timer.Wait(function()
blue_prints.current_gui_page = generate_load_gui()
blue_prints.current_gui_page.Visible = true
end, 100)
else
GUI.AddMessage('Failed to delete folder', Color(255, 0, 0))
message_box.Close()
end
end
end
end
local files = File.GetFiles(fullFolderPath)
local size_of_listbox = #files * button_height + 20
local contentList = GUI.ListBox(GUI.RectTransform(Vector2(1, 0.1), mainList.Content.RectTransform))
contentList.RectTransform.MinSize = Point(0, size_of_listbox)
contentList.RectTransform.MaxSize = Point(999999, size_of_listbox)
contentList.Visible = folder_states[folderName]
-- Process files
if files then
for _, filepath in ipairs(files) do
if string.match(filepath, "%.txt$") then
local filename = filepath:match("([^/\\]+)%.txt$")
local xmlContent = blue_prints.readFileContents(filepath)
local description = blue_prints.get_description_from_xml(xmlContent)
local componentCount = blue_prints.get_component_count_from_xml(xmlContent)
createBlueprintButton(filename, folderPath .. "/" .. filename, description, componentCount,
contentList)
end
end
end
headerButton.OnClicked = function()
folder_states[folderName] = not folder_states[folderName]
contentList.Visible = folder_states[folderName]
headerButton.Text = formatFolderHeaderText(folderName, folder_states[folderName], blueprintCount)
end
end
return blue_prints.current_gui_page
end
local function check_and_rebuild_frame()
local new_resolution = blue_prints.getScreenResolution()
if new_resolution ~= resolution or run_once_at_start == false then
local spacer = GUI.TextBlock(GUI.RectTransform(Vector2(1, 0.04), blue_prints.gui_button_frame_list.Content.RectTransform), "", nil, nil, GUI.Alignment.Center)
local button = GUI.Button(GUI.RectTransform(Vector2(1, 0.1), blue_prints.gui_button_frame_list.Content.RectTransform), "Load Blueprint", GUI.Alignment.Center, "GUIButtonSmall")
button.OnClicked = function()
if blue_prints.current_gui_page then
blue_prints.current_gui_page.Visible = false
end
blue_prints.current_gui_page = generate_load_gui()
blue_prints.current_gui_page.Visible = true
end
resolution = new_resolution
run_once_at_start = true
end
end
Hook.Patch("Barotrauma.Items.Components.CircuitBox", "AddToGUIUpdateList", function()
check_and_rebuild_frame()
end, Hook.HookMethodType.After)

View File

@@ -0,0 +1,138 @@
if SERVER then return end -- we don't want server to run GUI code.
-- Create main frame for all popups
blue_prints.popup_frame = GUI.Frame(GUI.RectTransform(Vector2(1, 1)), nil)
blue_prints.popup_frame.CanBeFocused = false
blue_prints.current_popup = nil
function blue_prints.show_popup(config)
-- Hide any existing popup
if blue_prints.current_popup then
blue_prints.current_popup.Visible = false
end
-- Create new popup frame
local popup = GUI.Frame(GUI.RectTransform(Vector2(1, 1), blue_prints.popup_frame.RectTransform, GUI.Anchor.Center), nil)
popup.CanBeFocused = false
popup.Visible = true
blue_prints.current_popup = popup
-- Background dimming
local backgroundButton = GUI.Button(GUI.RectTransform(Vector2(1, 1), popup.RectTransform), "", GUI.Alignment.Center, nil)
backgroundButton.Color = Color(0, 0, 0, 100)
-- Content container
local popupContent = GUI.Frame(GUI.RectTransform(Vector2(0.4, 0.6), popup.RectTransform, GUI.Anchor.Center))
local popupList = GUI.ListBox(GUI.RectTransform(Vector2(1, 1), popupContent.RectTransform, GUI.Anchor.BottomCenter))
-- Title
if config.title then
local titleText = GUI.TextBlock(GUI.RectTransform(Vector2(1, 0.15), popupList.Content.RectTransform),
config.title, nil, nil, GUI.Alignment.Center)
titleText.TextScale = 1.5
titleText.TextColor = Color(200, 200, 200)
titleText.Wrap = true
end
-- Message lines
if config.messages then
for _, message in ipairs(config.messages) do
local messageBlock = GUI.TextBlock(
GUI.RectTransform(Vector2(1, message.height or 0.05), popupList.Content.RectTransform),
message.text or message,
nil, nil, GUI.Alignment.Center
)
messageBlock.Wrap = true
messageBlock.TextColor = message.color or Color(200, 200, 200)
if message.padding then
messageBlock.Padding = message.padding
else
messageBlock.Padding = Vector4(10, 5, 10, 5)
end
end
end
-- Optional spacer
if config.addSpacer then
GUI.TextBlock(GUI.RectTransform(Vector2(1, 0.10), popupList.Content.RectTransform),
'', nil, nil, GUI.Alignment.Center)
end
-- Buttons
if config.buttons then
for _, buttonConfig in ipairs(config.buttons) do
local button = GUI.Button(
GUI.RectTransform(Vector2(1, 0.1), popupList.Content.RectTransform),
buttonConfig.text,
GUI.Alignment.Center,
"GUIButtonSmall"
)
if buttonConfig.color then
button.Color = buttonConfig.color
end
button.OnClicked = function()
if buttonConfig.onClick then
buttonConfig.onClick()
end
popup.Visible = false
end
end
end
-- Close on background click
backgroundButton.OnClicked = function()
popup.Visible = false
end
return popup
end
-- Add to GUI update list for all relevant screens
Hook.Patch("Barotrauma.GameScreen", "AddToGUIUpdateList", function()
blue_prints.popup_frame.AddToGUIUpdateList()
end, Hook.HookMethodType.After)
Hook.Patch("Barotrauma.NetLobbyScreen", "AddToGUIUpdateList", function(self, ptable)
blue_prints.popup_frame.AddToGUIUpdateList()
end, Hook.HookMethodType.After)
Hook.Patch("Barotrauma.SubEditorScreen", "AddToGUIUpdateList", function()
blue_prints.popup_frame.AddToGUIUpdateList()
end, Hook.HookMethodType.After)
--[[
--use like this in other scripts:
blue_prints.show_popup({
title = "Load Failed",
messages = {
{
text = "Your circuit has failed to load.",
height = 0.1 -- Taller block for wrapped text
},
{
text = "Your blueprint file might be from an earlier version of Blueprints and nothing is actually wrong. Try saving it again (overwriting the original) to update your blueprint file to the latest version.",
height = 0.2
},
{
text = "If you are certain your file is up to date try loading it again. Do not move or change anything during loading: the loaded circuit must match the blueprint file EXACTLY in order for you not to see this message.",
height = 0.2
},
{
text = "This also means if the inputs change any values in any component the unit test will also fail. Same thing if you have some value that changes over time, like RGB or timing values from an oscillator. So the circuit might still be ok, it just does not match your save exactly.",
height = 0.2
},
{
text = "If none of these exceptions apply and the problem persists please report this bug on the steam workshop page or the discord. Include a download link to your saved blueprint file and a screenshot of your console text (you can see that by hitting F3)",
--color = Color(255, 0, 0), -- Red text
height = 0.2
}
},
addSpacer = true,
buttons = {
{
text = "Close",
onClick = function() end
}
}
})
--]]

View File

@@ -0,0 +1,239 @@
if SERVER then return end -- we don't want server to run GUI code.
local resolution = blue_prints.getScreenResolution()
local run_once_at_start = false
-- Forward declarations
local check_and_rebuild_frame
local create_folder_modal
local generate_save_gui
create_folder_modal = function()
-- Create a new modal frame that covers the entire screen
local modalFrame = GUI.Frame(GUI.RectTransform(Vector2(1, 1), frame.RectTransform, GUI.Anchor.Center), nil)
modalFrame.CanBeFocused = false
-- Darkened background
local backgroundButton = GUI.Button(GUI.RectTransform(Vector2(1, 1), modalFrame.RectTransform), "",
GUI.Alignment.Center, nil)
backgroundButton.Color = Color(0, 0, 0, 100)
-- Modal content container - make it consistent with load_gui width (0.4)
local modalContent = GUI.Frame(GUI.RectTransform(Vector2(0.4, 0.6), modalFrame.RectTransform, GUI.Anchor.Center))
local menuList = GUI.ListBox(GUI.RectTransform(Vector2(1, 1), modalContent.RectTransform, GUI.Anchor.BottomCenter))
-- Title - matching load_gui style
local titleText = GUI.TextBlock(GUI.RectTransform(Vector2(1, 0.1), menuList.Content.RectTransform),
"CREATE NEW FOLDER", nil, nil, GUI.Alignment.Center)
titleText.TextScale = 2.0
titleText.Wrap = false
-- Spacer
local spacer1 = GUI.Frame(GUI.RectTransform(Vector2(1, 0.05), menuList.Content.RectTransform))
-- Folder name label
local folderNameText = GUI.TextBlock(GUI.RectTransform(Vector2(1, 0.05), menuList.Content.RectTransform),
"Folder Name:", nil, nil, GUI.Alignment.CenterLeft)
-- Text input - full width like in save_gui
local textBox = GUI.TextBox(GUI.RectTransform(Vector2(1, 0.1), menuList.Content.RectTransform), "")
-- Spacer
local spacer2 = GUI.Frame(GUI.RectTransform(Vector2(1, 0.05), menuList.Content.RectTransform))
-- Button container for proper centering
local buttonContainer = GUI.Frame(GUI.RectTransform(Vector2(1, 0.1), menuList.Content.RectTransform))
-- Create and Cancel buttons with proper spacing
local cancelButton = GUI.Button(
GUI.RectTransform(Vector2(0.45, 1), buttonContainer.RectTransform, GUI.Anchor.CenterLeft),
"Cancel", GUI.Alignment.Center, "GUIButtonSmall")
local createButton = GUI.Button(
GUI.RectTransform(Vector2(0.45, 1), buttonContainer.RectTransform, GUI.Anchor.CenterRight),
"Create", GUI.Alignment.Center, "GUIButtonSmall")
-- Button handlers
cancelButton.OnClicked = function()
modalFrame.Visible = false
modalFrame.RemoveFromGUIUpdateList()
-- Ensure main save window is visible
if blue_prints.current_gui_page then
blue_prints.current_gui_page.Visible = true
end
end
backgroundButton.OnClicked = cancelButton.OnClicked
createButton.OnClicked = function()
if textBox.Text and textBox.Text ~= "" then
local success, result = blue_prints.createNewFolder(textBox.Text)
if success then
-- Close modal
modalFrame.Visible = false
modalFrame.RemoveFromGUIUpdateList()
-- Refresh save window to show new folder
if blue_prints.current_gui_page then
blue_prints.current_gui_page.Visible = false
end
blue_prints.current_gui_page = generate_save_gui()
blue_prints.current_gui_page.Visible = true
GUI.AddMessage("Folder created successfully", Color(0, 255, 0))
else
GUI.AddMessage("Failed to create folder: " .. result, Color(255, 0, 0))
end
else
GUI.AddMessage("Please enter a folder name", Color(255, 0, 0))
end
end
return modalFrame
end
generate_save_gui = function()
blue_prints.current_gui_page = GUI.Frame(GUI.RectTransform(Vector2(1, 1), blue_prints.gui_button_frame.RectTransform, GUI.Anchor.Center),
nil)
blue_prints.current_gui_page.CanBeFocused = false
blue_prints.current_gui_page.Visible = false
-- Background close button
local closeButton = GUI.Button(
GUI.RectTransform(Vector2(1, 1), blue_prints.current_gui_page.RectTransform, GUI.Anchor.Center), "",
GUI.Alignment.Center, nil)
closeButton.OnClicked = function()
blue_prints.current_gui_page.Visible = not blue_prints.current_gui_page.Visible
end
local menuContent = GUI.Frame(GUI.RectTransform(Vector2(0.4, 0.6), blue_prints.current_gui_page.RectTransform,
GUI.Anchor.Center))
local menuList = GUI.ListBox(GUI.RectTransform(Vector2(1, 1), menuContent.RectTransform, GUI.Anchor.BottomCenter))
-- Title
local title_text = GUI.TextBlock(GUI.RectTransform(Vector2(1, 0.1), menuList.Content.RectTransform), "SAVE BLUEPRINT",
nil, nil, GUI.Alignment.Center)
title_text.TextScale = 1.5
title_text.TextColor = Color(200, 200, 200)
title_text.Wrap = false
-- Instructions
local instruction_text = GUI.TextBlock(GUI.RectTransform(Vector2(1, 0.20), menuList.Content.RectTransform),
'Enter a filename and select a folder. If using an existing filename, the old file will be overwritten.\n\n' ..
'A label with the name "Description" will be used as the reminder text when loading.\n\n' ..
'Click anywhere outside this box to cancel.',
nil, nil, GUI.Alignment.TopLeft)
instruction_text.Wrap = true
instruction_text.TextColor = Color(200, 200, 200)
instruction_text.Padding = Vector4(10, 5, 10, 5)
-- Create New Folder Button
local createFolderButton = GUI.Button(GUI.RectTransform(Vector2(1, 0.08), menuList.Content.RectTransform),
"Create New Folder", GUI.Alignment.Center, "GUIButtonSmall")
createFolderButton.OnClicked = function()
-- Hide the save window temporarily
blue_prints.current_gui_page.Visible = false
-- Show the folder creation modal
local modalFrame = create_folder_modal()
end
local spacer1 = GUI.Frame(GUI.RectTransform(Vector2(1, 0.05), menuList.Content.RectTransform))
-- Folder Selection
local folderText = GUI.TextBlock(GUI.RectTransform(Vector2(1, 0.05), menuList.Content.RectTransform),
"Select Folder:", nil, nil, GUI.Alignment.CenterLeft)
local folderDropDown = GUI.DropDown(GUI.RectTransform(Vector2(1, 0.1), menuList.Content.RectTransform),
"Select Folder", nil, nil, false)
-- Add folders to dropdown with numeric indices
local folders = blue_prints.getFolderList()
for i, folder in ipairs(folders) do
folderDropDown.AddItem(folder, i)
end
-- Select the most recently used folder if it exists
local selectedIndex = 1 -- Default to first item
for i, folder in ipairs(folders) do
if folder == blue_prints.most_recent_folder then
selectedIndex = i
break
end
end
folderDropDown.Select(selectedIndex - 1) -- -1 because dropdown uses 0-based indexing
-- Store folder list for reference
local folderLookup = folders
local spacer2 = GUI.Frame(GUI.RectTransform(Vector2(1, 0.05), menuList.Content.RectTransform))
-- Filename Section
local filenameText = GUI.TextBlock(GUI.RectTransform(Vector2(1, 0.05), menuList.Content.RectTransform),
"Filename:", nil, nil, GUI.Alignment.CenterLeft)
local filenameTextBox = GUI.TextBox(GUI.RectTransform(Vector2(1, 0.1), menuList.Content.RectTransform),
"Your filename here")
-- Set the text box to the most recently used name if available
if blue_prints.most_recently_used_blueprint_name ~= nil then
filenameTextBox.Text = blue_prints.most_recently_used_blueprint_name
end
local spacer3 = GUI.Frame(GUI.RectTransform(Vector2(1, 0.05), menuList.Content.RectTransform))
-- Save Button
local save_button = GUI.Button(GUI.RectTransform(Vector2(1, 0.1), menuList.Content.RectTransform),
"Save", GUI.Alignment.Center, "GUIButtonSmall")
save_button.OnClicked = function()
if filenameTextBox.Text and filenameTextBox.Text ~= "" then
-- Get the selected index and use it to look up the folder name
local selectedIndex = (tonumber(folderDropDown.SelectedData) or 1) - 1
local selectedFolder = folderLookup[selectedIndex + 1] -- +1 because Lua arrays start at 1
--print("Selected index:", selectedIndex) -- Debug print
--print("Selected folder:", selectedFolder) -- Debug print
if selectedFolder == "[Root Directory]" then
--print("Saving to root directory")
blue_prints.save_blueprint(filenameTextBox.Text)
else
--print("Saving to folder:", selectedFolder)
blue_prints.save_blueprint(filenameTextBox.Text, selectedFolder)
end
blue_prints.current_gui_page.Visible = false
GUI.AddMessage('File Saved', Color.White)
else
GUI.AddMessage('Please enter a filename', Color(255, 0, 0))
end
end
return blue_prints.current_gui_page
end
check_and_rebuild_frame = function()
local new_resolution = blue_prints.getScreenResolution()
if new_resolution ~= resolution or run_once_at_start == false then
local spacer = GUI.TextBlock(GUI.RectTransform(Vector2(1, 0.04), blue_prints.gui_button_frame_list.Content.RectTransform), "", nil, nil, GUI.Alignment.Center)
local button = GUI.Button(GUI.RectTransform(Vector2(1, 0.1), blue_prints.gui_button_frame_list.Content.RectTransform), "Save Blueprint", GUI.Alignment.Center, "GUIButtonSmall")
button.OnClicked = function()
if blue_prints.current_gui_page ~= nil then
blue_prints.current_gui_page.Visible = false
end
blue_prints.current_gui_page = nil
blue_prints.current_gui_page = generate_save_gui()
blue_prints.current_gui_page.Visible = true
end
resolution = new_resolution
run_once_at_start = true
end
end
Hook.Patch("Barotrauma.Items.Components.CircuitBox", "AddToGUIUpdateList", function()
check_and_rebuild_frame()
end, Hook.HookMethodType.After)

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,19 @@
--used to create netlimitedstrings
LuaUserData.RegisterType("Barotrauma.NetLimitedString")
blue_prints.net_limited_string_type = LuaUserData.CreateStatic("Barotrauma.NetLimitedString", true)
--for accessing component values inside components
LuaUserData.MakeMethodAccessible(Descriptors["Barotrauma.Item"], "GetInGameEditableProperties")
LuaUserData.RegisterType("System.ValueTuple`2[System.Object,Barotrauma.SerializableProperty]")
--used to create immutable arrays
LuaUserData.RegisterType("System.Collections.Immutable.ImmutableArray")
LuaUserData.RegisterType("System.Collections.Immutable.ImmutableArray`1")
blue_prints.immutable_array_type = LuaUserData.CreateStatic("System.Collections.Immutable.ImmutableArray", false)
--register types for components not in lua yet
LuaUserData.RegisterType('Barotrauma.Items.Components.ConnectionSelectorComponent')
LuaUserData.RegisterType('Barotrauma.Items.Components.DemultiplexerComponent')
LuaUserData.RegisterType('Barotrauma.Items.Components.MultiplexerComponent')

View File

@@ -0,0 +1,527 @@
if SERVER then return end --prevents it from running on the server
-- Delimiter constants
local STRING_START = "<<<STRINGSTART>>>"
local STRING_END = "<<<STRINGEND>>>"
-- Encodes a string to be safely stored in XML attributes
local function encodeAttributeString(str)
if str == nil then return "" end
-- First, escape any existing delimiters in the content
local escaped = str:gsub(STRING_START, "\\<<<STRINGSTART>>>")
:gsub(STRING_END, "\\<<<STRINGEND>>>")
-- Clean the string of control characters
escaped = blue_prints.clean_string(escaped)
-- Wrap the escaped content with delimiters
return STRING_START .. escaped .. STRING_END
end
-- Decodes a string that was stored in XML attributes
local function decodeAttributeString(str)
if str == nil then return "" end
-- Check if the string has our delimiters
local content = str:match(STRING_START .. "(.-)" .. STRING_END)
if not content then
-- If no delimiters found, return original string (for backward compatibility)
return str
end
-- Unescape any escaped delimiters
return content:gsub("\\<<<STRINGSTART>>>", STRING_START)
:gsub("\\<<<STRINGEND>>>", STRING_END)
end
local function processLabelStrings(xmlString)
-- Process header and body attributes in Label tags
return xmlString:gsub('(<Label[^>]-)(header="([^"]*)")', function(prefix, full, content)
return prefix .. 'header="' .. encodeAttributeString(content) .. '"'
end):gsub('(<Label[^>]-)(body="([^"]*)")', function(prefix, full, content)
return prefix .. 'body="' .. encodeAttributeString(content) .. '"'
end)
end
-- Function to process input/output node labels
local function processNodeLabels(xmlString)
-- Pattern to match InputNode or OutputNode sections
local function processNodeSection(nodeSection)
-- Process ConnectionLabelOverride tags within the node section
return nodeSection:gsub('(<ConnectionLabelOverride[^>]-value=)"([^"]*)"', function(prefix, content)
-- Don't re-encode if already encoded
if content:match("^" .. STRING_START .. ".*" .. STRING_END .. "$") then
return prefix .. '"' .. content .. '"'
end
return prefix .. '"' .. encodeAttributeString(content) .. '"'
end)
end
-- Process InputNode sections
xmlString = xmlString:gsub("(<InputNode.-</InputNode>)", processNodeSection)
-- Process OutputNode sections
xmlString = xmlString:gsub("(<OutputNode.-</OutputNode>)", processNodeSection)
return xmlString
end
-- Modified function to add encoded attributes to components
local function add_encoded_attribute_to_component(xmlContent, targetId, attributeName, attributeValue)
-- First encode the attribute value
local encodedValue = encodeAttributeString(attributeValue)
-- Function to add the attribute to the specific Component element
local function modifyComponent(componentString)
local id = componentString:match('id="(%d+)"')
if id and tonumber(id) == targetId then
-- Create the full attribute string with the encoded value
local attributeStr = string.format('%s=%s', attributeName, encodedValue)
return componentString:gsub('/>$', ' ' .. blue_prints.escapePercent(attributeStr) .. ' />')
else
return componentString
end
end
-- Find the CircuitBox element
local circuitBoxStart, circuitBoxEnd = xmlContent:find('<CircuitBox.->')
local circuitBoxEndTag = xmlContent:find('</CircuitBox>', circuitBoxEnd)
if not circuitBoxStart or not circuitBoxEndTag then
print("CircuitBox element not found")
return xmlContent
end
-- Extract the CircuitBox content
local circuitBoxContent = xmlContent:sub(circuitBoxEnd + 1, circuitBoxEndTag - 1)
-- Modify the specific Component element
local modifiedCircuitBoxContent = circuitBoxContent:gsub('<Component.-/>', modifyComponent)
-- Replace the original CircuitBox content with the modified content
return xmlContent:sub(1, circuitBoxEnd) .. modifiedCircuitBoxContent .. xmlContent:sub(circuitBoxEndTag)
end
-- Function to extract components and their IDs
local function extractComponents(xmlString)
local components = {}
for id, position in xmlString:gmatch('<Component id="(%d+)" position="([^"]+)"') do
components[#components + 1] = {id = tonumber(id), position = position}
end
table.sort(components, function(a, b) return a.id < b.id end)
return components
end
-- Function to create ID mapping
local function createIdMapping(components)
local idMap = {}
local newId = 0
for _, component in ipairs(components) do
idMap[component.id] = newId
newId = newId + 1
end
return idMap
end
-- Function to update component IDs and wire targets
local function updateXml(xmlString, idMap)
-- Update component IDs
xmlString = xmlString:gsub('<Component id="(%d+)"', function(id)
return string.format('<Component id="%d"', idMap[tonumber(id)])
end)
-- Update wire targets
xmlString = xmlString:gsub('target="(%d*)"', function(target)
if target ~= "" then
local newTarget = idMap[tonumber(target)]
if newTarget then
return string.format('target="%d"', newTarget)
else
print("Warning: No mapping found for target " .. target)
return string.format('target="%s"', target)
end
else
return 'target=""'
end
end)
return xmlString
end
-- Main function to renumber components and update wire targets
local function renumber_components(xmlString)
local components = extractComponents(xmlString)
local idMap = createIdMapping(components)
return updateXml(xmlString, idMap)
end
local function find_id_within_component(component_string)
-- Define the pattern to match the id attribute value
local pattern = 'id="(%d+)"'
-- Extract the id value using string.match
local id_value = string.match(component_string, pattern)
-- Print the result
if id_value then
return id_value
else
return ""
end
end
local function find_specific_component(xmlContent, target_component)
-- Split the XML content into lines
local lines = {}
for line in string.gmatch(xmlContent, "[^\r\n]+") do
table.insert(lines, line)
end
-- Define the pattern to match the component
local pattern = "<Component.-/->"
-- Initialize counters
local count = 0
-- Iterate through each line to find the specific component
for i, line in ipairs(lines) do
-- Check for components in the current line
for component in string.gmatch(line, pattern) do
count = count + 1
if count == target_component then
-- Return both the line number and the line content
return i, line
end
end
end
-- Return nil, nil if the component was not found
return nil, nil
end
local function count_component_number(xmlContent)
-- Define the pattern to match the component
local pattern = "<Component.-/->"
-- Initialize counter
local count = 0
-- Use string.gmatch to iterate over all matches
for _ in string.gmatch(xmlContent, pattern) do
count = count + 1
end
return count
end
local function swap_lines_in_string(xmlContent, line1, line2)
-- Split the text into lines
local lines = {}
for line in string.gmatch(xmlContent, "[^\r\n]+") do
table.insert(lines, line)
end
-- Swap the specified lines
if line1 <= #lines and line2 <= #lines then
lines[line1], lines[line2] = lines[line2], lines[line1]
else
print("Error: Line numbers out of range.")
end
-- Join the lines back into a single string
local swapped_text = table.concat(lines, "\n")
return swapped_text
end
local function put_components_in_order(xmlContent)
local something_in_xml_changed = false
local number_of_components = count_component_number(xmlContent)
for i = 1, number_of_components-1 do
local first_line_number, first_line_content = find_specific_component(xmlContent, i)
local second_line_number, second_line_content = find_specific_component(xmlContent, i+1)
--print(first_line_content)
--print(second_line_content)
--print("-----")
local first_id = find_id_within_component(first_line_content)
local second_id = find_id_within_component(second_line_content)
if tonumber(first_id) > tonumber(second_id) then
--print("comparing" .. first_id .. " to " .. second_id)
xmlContent = swap_lines_in_string(xmlContent, first_line_number, second_line_number)
something_in_xml_changed = true
end
end
if something_in_xml_changed then return put_components_in_order(xmlContent) end
--print(xmlContent)
return xmlContent
end
local function remove_attribute_from_components(xmlContent, attributeName)
-- Function to remove the specified attribute from a component
local function removeAttribute(componentString)
-- Pattern to match traditional XML attributes
local pattern1 = '%s*' .. attributeName .. '="[^"]+"'
-- Pattern to match encoded strings with delimiters
local pattern2 = '%s*' .. attributeName .. '=<<<STRINGSTART>>>[^<]*<<<STRINGEND>>>'
-- Remove both types of attributes
local result = componentString:gsub(pattern1, '')
result = result:gsub(pattern2, '')
return result
end
-- Find all Component elements and process them
local function processComponents(content)
return content:gsub('<Component.-/>', removeAttribute)
end
-- Find the CircuitBox element
local circuitBoxStart, circuitBoxEnd = xmlContent:find('<CircuitBox.->')
local circuitBoxEndTag = xmlContent:find('</CircuitBox>', circuitBoxEnd)
if not circuitBoxStart or not circuitBoxEndTag then
print("CircuitBox element not found")
return xmlContent
end
-- Extract and process the CircuitBox content
local circuitBoxContent = xmlContent:sub(circuitBoxEnd + 1, circuitBoxEndTag - 1)
local modifiedCircuitBoxContent = processComponents(circuitBoxContent)
-- Replace the original CircuitBox content with the modified content
return xmlContent:sub(1, circuitBoxEnd) .. modifiedCircuitBoxContent .. xmlContent:sub(circuitBoxEndTag)
end
local function clean_component_whitespace(xmlContent)
local function cleanComponent(componentString)
-- Don't touch anything between STRINGSTART and STRINGEND
local parts = {}
local lastPos = 1
-- Find start and end positions of all encoded strings
local startPos = componentString:find('<<<STRINGSTART>>>', lastPos, true)
while startPos do
local endPos = componentString:find('<<<STRINGEND>>>', startPos, true)
if not endPos then break end
-- Add the part before the encoded string
local beforePart = componentString:sub(lastPos, startPos - 1)
beforePart = beforePart:gsub('%s+', ' ') -- collapse multiple spaces to single space
table.insert(parts, beforePart)
-- Add the encoded string unchanged
table.insert(parts, componentString:sub(startPos, endPos + 13))
lastPos = endPos + 14
startPos = componentString:find('<<<STRINGSTART>>>', lastPos, true)
end
-- Add any remaining part after the last encoded string
if lastPos <= #componentString then
local remaining = componentString:sub(lastPos)
remaining = remaining:gsub('%s+', ' ')
table.insert(parts, remaining)
end
return table.concat(parts)
end
-- Find all Component tags and process them
local circuitBoxStart, circuitBoxEnd = xmlContent:find('<CircuitBox.->')
local circuitBoxEndTag = xmlContent:find('</CircuitBox>', circuitBoxEnd)
if not circuitBoxStart or not circuitBoxEndTag then
return xmlContent
end
local circuitBoxContent = xmlContent:sub(circuitBoxEnd + 1, circuitBoxEndTag - 1)
local modifiedContent = circuitBoxContent:gsub('<Component.-/>', cleanComponent)
return xmlContent:sub(1, circuitBoxEnd) .. modifiedContent .. xmlContent:sub(circuitBoxEndTag)
end
-- Function to round a number to the nearest integer
local function round(num)
return math.floor(num + 0.5)
end
-- Function to round the position and size attributes in a Label element
local function round_attributes(label)
-- Round the position attribute
if label:find('position="') then
local pos_x, pos_y = label:match('position="([^,]+),([^"]+)"')
local rounded_position = string.format('position="%d,%d"', round(tonumber(pos_x)), round(tonumber(pos_y)))
label = label:gsub('position="[^"]+"', rounded_position)
end
-- Round the size attribute
if label:find('size="') then
local size_x, size_y = label:match('size="([^,]+),([^"]+)"')
local rounded_size = string.format('size="%d,%d"', round(tonumber(size_x)), round(tonumber(size_y)))
label = label:gsub('size="[^"]+"', rounded_size)
end
if label:find('pos="') then
local pos_x, pos_y = label:match('pos="([^,]+),([^"]+)"')
local rounded_position = string.format('pos="%d,%d"', round(tonumber(pos_x)), round(tonumber(pos_y)))
label = label:gsub('pos="[^"]+"', rounded_position)
end
return label
end
-- Function to process the entire XML string
local function round_position_values(xml_string)
local processed_string = ""
-- Process each line of the XML string
for line in xml_string:gmatch("[^\r\n]+") do
-- Find and process Label elements
if line:find("<Label") or line:find("<Component") or line:find("<InputNode") or line:find("<OutputNode") then
line = round_attributes(line)
end
processed_string = processed_string .. line .. "\n"
end
return processed_string
end
function blue_prints.prepare_circuitbox_xml_for_saving()
if blue_prints.most_recent_circuitbox == nil then print("no circuitbox detected") return end
local sacrificial_xml = XElement("Root")
blue_prints.most_recent_circuitbox.Save(sacrificial_xml)
local circuitbox_xml = tostring(sacrificial_xml)
local components = blue_prints.most_recent_circuitbox.GetComponentString("CircuitBox").Components
for i, component in ipairs(components) do
-- Add the class name to component
local class_name = nil
for value in component.Item.Components do
local target_string = tostring(value)
local target_string = target_string:match("([^%.]+)$")
if string.find(target_string, "Component") then
class_name = target_string
break
end
end
if class_name ~= nil then
circuitbox_xml = add_encoded_attribute_to_component(circuitbox_xml, component.ID, 'Class', class_name)
else
print("Error: couldn't find class name! Report this bug on the workshop page please! Component causing the problem: " .. tostring(component.Item.Prefab.Identifier))
end
-- Add any values stored inside the component
local my_editables = component.Item.GetInGameEditableProperties(false)
for tuple in my_editables do
local field_name = tostring(tuple.Item2.name)
local field_value = tostring(tuple.Item2.GetValue(tuple.Item1))
field_name = blue_prints.clean_string(field_name)
field_value = blue_prints.clean_string(field_value)
circuitbox_xml = add_encoded_attribute_to_component(
circuitbox_xml,
component.ID,
field_name,
field_value
)
end
-- Add the item prefab itself
circuitbox_xml = add_encoded_attribute_to_component(
circuitbox_xml,
component.ID,
'item',
tostring(component.Item.Prefab.Identifier)
)
end
--remove stuff that shouldnt be there that gets added inside sub editor only
circuitbox_xml = remove_attribute_from_components(circuitbox_xml, "InventoryIconColor")
circuitbox_xml = remove_attribute_from_components(circuitbox_xml, "ContainerColor")
circuitbox_xml = remove_attribute_from_components(circuitbox_xml, "SpriteDepthWhenDropped")
-- Process Label strings
circuitbox_xml = processLabelStrings(circuitbox_xml)
-- Process Input/Output node labels
circuitbox_xml = processNodeLabels(circuitbox_xml)
-- Cleanup and formatting
circuitbox_xml = put_components_in_order(circuitbox_xml)
circuitbox_xml = renumber_components(circuitbox_xml)
circuitbox_xml = round_position_values(circuitbox_xml)
circuitbox_xml = clean_component_whitespace(circuitbox_xml)
return circuitbox_xml
end
function blue_prints.save_blueprint(provided_path, folder)
if Character.Controlled == nil then print("you dont have a character") return end
if blue_prints.most_recent_circuitbox == nil then print("no circuitbox detected") return end
-- Default to "General" folder if none specified
folder = folder or "General"
-- Store the folder and filename for future use
blue_prints.most_recent_folder = folder
blue_prints.most_recently_used_blueprint_name = provided_path:gsub("%.txt$", "") -- Remove .txt if present
-- Prepare the circuit box XML
local circuitbox_xml = blue_prints.prepare_circuitbox_xml_for_saving()
-- Save using the folder-aware function
return blue_prints.saveWithFolder(provided_path, folder, circuitbox_xml)
end

View File

@@ -0,0 +1,223 @@
local function all_lines_the_same(filename, comparison_string)
local file = io.open(filename, "r")
if not file then
print("Error: Unable to open file")
return false
end
local file_lines = {}
for line in file:lines() do
table.insert(file_lines, line)
end
file:close()
local string_lines = {}
for line in comparison_string:gmatch("[^\r\n]+") do
table.insert(string_lines, line)
end
local max_lines = math.max(#file_lines, #string_lines)
local all_lines_same = true
for i = 1, max_lines do
local file_line = file_lines[i] or ""
local string_line = string_lines[i] or ""
if file_line ~= string_line then
all_lines_same = false
print(string.format("Mismatch at line %d:", i))
print("File: " .. file_line)
print("String: " .. string_line)
print()
end
end
return all_lines_same
end
--[[
local function check_file_against_string(file_path, comparison_string)
local ignore_prefixes = {'<link', '<Item name="" identifier="circuitbox"', '<input name="signal_', '<output name="signal_', '<ItemContainer QuickUseMovesItemsInside="False"', '<Holdable Attached="True"', '<requireditem items="wrench"', '<Wire id=', '</input', '</output'}
local ignore_anywhere = {'header="Blueprints" body="Circuit made with Blueprints. &#xA; &#xA; Get it now on the steam workshop!"'}
local file = io.open(file_path, "r")
if not file then
return false, "Unable to open file"
end
local file_lines = {}
for line in file:lines() do
table.insert(file_lines, line:match("^%s*(.+)"))
end
file:close()
local comparison_lines = {}
for line in comparison_string:gmatch("[^\r\n]+") do
local should_ignore = false
for _, prefix in ipairs(ignore_prefixes) do
if line:match("^%s*" .. prefix) then
should_ignore = true
break
end
end
if not should_ignore then
for _, ignore_string in ipairs(ignore_anywhere) do
if line:match(ignore_string) then
should_ignore = true
break
end
end
if not should_ignore then
table.insert(comparison_lines, line:match("^%s*(.+)"))
end
end
end
for _, comparison_line in ipairs(comparison_lines) do
local found = false
for _, file_line in ipairs(file_lines) do
if file_line == comparison_line then
found = true
break
end
end
if not found then
print("Line not found in file: " .. comparison_line)
return false, "Line not found in file: " .. comparison_line
end
end
return true, "All lines found in file"
end
--]]
local function clean_string_for_comparison(string_to_clean)
string_to_clean = string_to_clean:gsub(' ', '') --remove all whitespace
string_to_clean = string_to_clean:gsub('LinkToChat="false"', '') --link to chat is a special case because some servers do not allow it.
string_to_clean = string_to_clean:gsub('LinkToChat="true"', '')
return string_to_clean
end
function check_file_against_string(file_path, comparison_string)
local file_path = blue_prints.normalizePath(file_path)
local file_content = blue_prints.readFileContents(file_path)
if not file_content then
return false, "Unable to open file"
end
local include_prefixes = {'<InputNode' , '<OutputNode' , '<ConnectionLabelOverride', '<Component', '<From name=' , '<To name=', '<Label id='}
local ignore_anywhere = {'header="Blueprints" body="Circuit made with Blueprints. &#xA; &#xA; Get it now on the steam workshop!"'}
local file_lines = {}
for line in file:lines() do
local should_ignore = false
for _, ignore_string in ipairs(ignore_anywhere) do
if line:match(ignore_string) then
should_ignore = true
break
end
end
for _, prefix in ipairs(include_prefixes) do
if line:match("^%s*" .. prefix) and should_ignore == false then
table.insert(file_lines, line:match("^%s*(.+)"))
--print("including line from file: " .. line)
break
end
end
end
file:close()
local comparison_lines = {}
for line in comparison_string:gmatch("[^\r\n]+") do
local should_ignore = false
for _, ignore_string in ipairs(ignore_anywhere) do
if line:match(ignore_string) then
should_ignore = true
break
end
end
for _, prefix in ipairs(include_prefixes) do
if line:match("^%s*" .. prefix) and should_ignore == false then
table.insert(comparison_lines, line:match("^%s*(.+)"))
--print("including line from string: " .. line)
break
end
end
end
for _, comparison_line in ipairs(comparison_lines) do
comparison_line = clean_string_for_comparison(comparison_line) --remove anything that should be ignored
local found = false
for _, file_line in ipairs(file_lines) do
file_line = clean_string_for_comparison(file_line) --remove anything that should be ignored
if file_line == comparison_line then
found = true
break
end
end
if not found then
print("Line not found in file: " .. comparison_line)
return false, "Line not found in file: " .. comparison_line
end
end
for _, file_line in ipairs(file_lines) do
file_line = clean_string_for_comparison(file_line) --remove anything that should be ignored
local found = false
for _, comparison_line in ipairs(comparison_lines) do
comparison_line = clean_string_for_comparison(comparison_line) --remove anything that should be ignored
if file_line == comparison_line then
found = true
break
end
end
if not found then
print("Line not found in string: " .. file_line)
return false, "Line not found in string: " .. file_line
end
end
return true, "All lines found in file"
end
function blue_prints.loading_complete_unit_test(path_to_loaded_file, loaded_circuit_xml)
local result = check_file_against_string(path_to_loaded_file, loaded_circuit_xml)
if not result then
print('‖color:red‖"blueprint that failed its test: "' .. path_to_loaded_file .. '‖end‖')
end
return result
end
function blue_prints.unit_test_all_blueprint_files()
local saved_files = blue_prints.getFiles(blue_prints.save_path)
current_delay = 0
for name, value in pairs(saved_files) do
if string.match(value, "%.txt$") then
local filename = value:match("([^/\\]+)$")
local filename = string.gsub(filename, "%.txt$", "")
Timer.Wait(function() blue_prints.construct_blueprint(filename) end, current_delay)
current_delay = current_delay + 12000
end
end
end

View File

@@ -0,0 +1,300 @@
if SERVER then return end --prevents it from running on the server
-- Platform-agnostic file path handling
function blue_prints.normalizePath(path)
-- Replace Windows backslashes with forward slashes
path = path:gsub("\\", "/")
-- Remove any double slashes
path = path:gsub("//+", "/")
-- Remove trailing slash if present
path = path:gsub("/$", "")
return path
end
-- Safe file operations with error handling
function blue_prints.safeFileOperation(operation, ...)
local success, result = pcall(operation, ...)
if not success then
print("File operation failed: " .. tostring(result))
return nil
end
return result
end
-- Enhanced file reading with platform checks
function blue_prints.readFileContents(path)
path = blue_prints.normalizePath(path)
-- Try direct read first
local file = io.open(path, "r")
if file then
local content = file:read("*all")
file:close()
return content
end
-- If direct read fails, try alternate path format
local alt_path = path:gsub("LocalMods/", "local_mods/")
file = io.open(alt_path, "r")
if file then
local content = file:read("*all")
file:close()
return content
end
return nil
end
-- Enhanced directory checking
function blue_prints.checkDirectory(path)
path = blue_prints.normalizePath(path)
if not File.DirectoryExists(path) then
-- Try creating with normalized path
local success = blue_prints.safeFileOperation(File.CreateDirectory, path)
if not success then
-- Try alternate path format
local alt_path = path:gsub("LocalMods/", "local_mods/")
success = blue_prints.safeFileOperation(File.CreateDirectory, alt_path)
if not success then
print("Failed to create directory: " .. path)
return false
end
end
end
return true
end
-- Enhanced file listing that handles both Windows and Linux paths
function blue_prints.getFiles(path)
path = blue_prints.normalizePath(path)
-- Try to get files with current path format
local success, files = pcall(function() return File.GetFiles(path) end)
if success and files and #files > 0 then
return files
end
-- If that fails, try with alternate path format
success, files = pcall(function()
return File.GetFiles(path:gsub("LocalMods/", "local_mods/"))
end)
if success and files and #files > 0 then
return files
end
-- If no files found, return empty table instead of nil
return {}
end
-- Write file with platform compatibility
function blue_prints.writeFile(path, content)
path = blue_prints.normalizePath(path)
path = path:gsub("%s", "_") -- Replace spaces with underscores
local file, err = io.open(path, "w")
if file then
file:write(content)
file:close()
return true
end
-- Log the error
print("Failed to write file: " .. path .. " with error: " .. tostring(err))
-- Try alternate path if direct write fails
local alt_path = path:gsub("LocalMods/", "local_mods/")
file, err = io.open(alt_path, "w")
if file then
file:write(content)
file:close()
return true
end
-- Log the error
print("Failed to write file: " .. alt_path .. " with error: " .. tostring(err))
return false
end
-- Debug helper function
function blue_prints.printSystemInfo()
print("Operating system: " .. (package.config:sub(1, 1) == '\\' and "Windows" or "Unix-like"))
print("Save path: " .. blue_prints.save_path)
print("Normalized save path: " .. blue_prints.normalizePath(blue_prints.save_path))
end
-- Creates a folder if it doesn't exist, handling platform-specific path issues
function blue_prints.createFolder(folderPath)
folderPath = blue_prints.normalizePath(folderPath)
if not File.DirectoryExists(folderPath) then
-- Try creating with normalized path
local success = blue_prints.safeFileOperation(File.CreateDirectory, folderPath)
if not success then
-- Try alternate path format for different platforms
local alt_path = folderPath:gsub("LocalMods/", "local_mods/")
success = blue_prints.safeFileOperation(File.CreateDirectory, alt_path)
if not success then
print("Failed to create folder: " .. folderPath)
return false
end
end
end
return true
end
-- Modified save function that handles folder paths
function blue_prints.saveWithFolder(provided_path, folder, content)
if not provided_path or not folder or not content then
print("Error: Missing required parameters for save")
return false
end
-- Create a complete path including the folder
local fullPath
-- If folder is [Root Directory], save directly in the base save path
if folder == "[Root Directory]" then
fullPath = blue_prints.save_path
else
-- Clean and normalize the folder name
folder = folder:gsub("[^%w%s%-_]", "") -- Remove special characters
folder = folder:gsub("%s+", "_") -- Replace spaces with underscores
fullPath = blue_prints.normalizePath(blue_prints.save_path .. "/" .. folder)
end
-- Ensure the folder exists
if not blue_prints.createFolder(fullPath) then
print("Error: Could not create or access folder: " .. folder)
return false
end
-- Add .txt extension if not present
if not string.match(provided_path, "%.txt$") then
provided_path = provided_path .. ".txt"
end
-- Create the full file path
local file_path = blue_prints.normalizePath(fullPath .. "/" .. provided_path)
-- Write the file
if blue_prints.writeFile(file_path, content) then
print("Blueprint saved to " .. file_path)
return true
else
print("Error: Could not save blueprint")
return false
end
end
-- Ensures the base blueprints directory exists
function blue_prints.ensureBaseDirectory()
local basePath = blue_prints.normalizePath(blue_prints.save_path)
if not File.DirectoryExists(basePath) then
local success = blue_prints.safeFileOperation(File.CreateDirectory, basePath)
if not success then
-- Try alternate path format
local alt_path = basePath:gsub("LocalMods/", "local_mods/")
success = blue_prints.safeFileOperation(File.CreateDirectory, alt_path)
if not success then
print("Failed to create base directory: " .. basePath)
return false
end
end
end
return true
end
-- Gets all folders in the blueprints directory
function blue_prints.getFolders(path)
-- Ensure base directory exists first
if not blue_prints.ensureBaseDirectory() then
return {}
end
path = blue_prints.normalizePath(path)
local folders = {}
local success = pcall(function()
folders = File.GetDirectories(path)
end)
if not success or not folders or #folders == 0 then
-- Try alternate path format
local alt_path = path:gsub("LocalMods/", "local_mods/")
success = pcall(function()
folders = File.GetDirectories(alt_path)
end)
end
return folders or {}
end
-- Gets list of folder names in a user-friendly format
function blue_prints.getFolderList()
local folders = blue_prints.getFolders(blue_prints.save_path)
local folderNames = { "[Root Directory]" } -- Allow saving in root directory
for _, fullPath in pairs(folders) do
-- Extract just the folder name from the full path
local folderName = fullPath:match("([^/\\]+)$")
if folderName then
table.insert(folderNames, folderName)
end
end
return folderNames
end
-- Creates a new folder in the blueprints directory
function blue_prints.createNewFolder(folderName)
if not blue_prints.ensureBaseDirectory() then
return false, "Cannot create base directory"
end
if not folderName or folderName == "" then
return false, "Invalid folder name"
end
-- Clean the folder name
folderName = folderName:gsub("[^%w%s%-_]", ""):gsub("%s+", "_")
local folderPath = blue_prints.normalizePath(blue_prints.save_path .. "/" .. folderName)
-- Check if folder already exists
if File.DirectoryExists(folderPath) then
return false, "Folder already exists"
end
-- Create the folder
if blue_prints.createFolder(folderPath) then
return true, folderName
else
return false, "Failed to create folder"
end
end
-- Modified Directory Functions
function blue_prints.getDirectories(path)
path = blue_prints.normalizePath(path)
-- Try to get directories with current path format
local success, dirs = pcall(function() return File.GetDirectories(path) end)
if success and dirs and #dirs > 0 then
return dirs
end
-- If that fails, try with alternate path format
success, dirs = pcall(function()
return File.GetDirectories(path:gsub("LocalMods/", "local_mods/"))
end)
if success and dirs and #dirs > 0 then
return dirs
end
-- If no directories found, return empty table instead of nil
return {}
end

View File

@@ -0,0 +1,156 @@
if SERVER then return end --prevents it from running on the server
function blue_prints.validate_blueprint_xml(xmlString)
if not xmlString then
return false, "No XML content provided"
end
-- Check for required main elements
if not xmlString:match("<CircuitBox.-</CircuitBox>") then
return false, "Missing CircuitBox element"
end
if not xmlString:match("<InputNode[^>]+>") then
return false, "Missing InputNode"
end
if not xmlString:match("<OutputNode[^>]+>") then
return false, "Missing OutputNode"
end
-- Validate input/output node positions
local inputNode = xmlString:match("<InputNode[^>]+>")
local outputNode = xmlString:match("<OutputNode[^>]+>")
if inputNode then
local posX, posY = inputNode:match('pos="([%d%.%-]+),([%d%.%-]+)"')
if not (posX and posY and tonumber(posX) and tonumber(posY)) then
return false, "Invalid InputNode position"
end
end
if outputNode then
local posX, posY = outputNode:match('pos="([%d%.%-]+),([%d%.%-]+)"')
if not (posX and posY and tonumber(posX) and tonumber(posY)) then
return false, "Invalid OutputNode position"
end
end
-- Validate components
for component in xmlString:gmatch('<Component.-/>') do
-- Check required attributes
local id = component:match('id="(%d+)"')
local posX, posY = component:match('position="([%-%d%.]+),([%-%d%.]+)"')
-- Check both old and new format for item and class
local item = component:match('item="([^"]+)"') or
component:match('item=<<<STRINGSTART>>>([^<]+)<<<STRINGEND>>>')
local class = component:match('Class="([^"]+)"') or
component:match('Class=<<<STRINGSTART>>>([^<]+)<<<STRINGEND>>>')
if not (id and posX and posY and item and class) then
return false, "Invalid component definition - missing required attributes"
end
-- Validate position values are numbers
if not (tonumber(posX) and tonumber(posY)) then
return false, "Invalid component position values"
end
-- Validate ID is a number
if not tonumber(id) then
return false, "Invalid component ID"
end
-- Check if item prefab exists
if item == "oscillatorcomponent" then item = "oscillator"
elseif item == "concatenationcomponent" then item = "concatcomponent"
elseif item == "exponentiationcomponent" then item = "powcomponent"
elseif item == "regexfind" then item = "regexcomponent"
elseif item == "signalcheck" then item = "signalcheckcomponent"
elseif item == "squareroot" then item = "squarerootcomponent" end
local itemPrefab = ItemPrefab.GetItemPrefab(item)
if not itemPrefab then
return false, "Invalid item prefab: " .. tostring(item)
end
end
-- Validate wire connections
for wire in xmlString:gmatch("<Wire.-</Wire>") do
local id = wire:match('id="(%d+)"')
if not id or not tonumber(id) then
return false, "Invalid wire ID"
end
-- Check wire prefab
local prefab = wire:match('prefab="([^"]+)"')
if not prefab then
return false, "Missing wire prefab"
end
local wirePrefab = ItemPrefab.GetItemPrefab(prefab)
if not wirePrefab then
return false, "Invalid wire prefab: " .. tostring(prefab)
end
-- Check connections
local fromName, fromTarget = wire:match('<From name="([^"]+)" target="([^"]*)"')
local toName, toTarget = wire:match('<To name="([^"]+)" target="([^"]*)"')
if not (fromName and toName) then
return false, "Invalid wire connections"
end
-- Validate signal names format
if fromTarget == "" and not fromName:match("^signal_%w+%d+$") then
return false, "Invalid input signal name format"
end
if toTarget == "" and not toName:match("^signal_%w+%d+$") then
return false, "Invalid output signal name format"
end
end
-- Validate labels
for label in xmlString:gmatch('<Label.-/>') do
local id = label:match('id="(%d+)"')
local posX, posY = label:match('position="([%d%.%-]+),([%d%.%-]+)"')
local sizeW, sizeH = label:match('size="([%d%.%-]+),([%d%.%-]+)"')
local color = label:match('color="([^"]+)"')
if not (id and posX and posY and sizeW and sizeH and color) then
return false, "Invalid label definition - missing required attributes"
end
-- Validate numeric values
if not (tonumber(posX) and tonumber(posY) and
tonumber(sizeW) and tonumber(sizeH)) then
return false, "Invalid label position or size values"
end
-- Validate color format (basic check for hex color)
if not color:match("^#[0-9A-Fa-f]+$") then
return false, "Invalid label color format"
end
end
-- If all checks pass
return true, "Blueprint XML is valid"
end
function blue_prints.validate_blueprint_file(provided_path)
-- Add .txt extension if not present
if not string.match(provided_path, "%.txt$") then
provided_path = provided_path .. ".txt"
end
-- Normalize the path
local file_path = blue_prints.normalizePath(blue_prints.save_path .. "/" .. provided_path)
-- Try to read the file content using our safe read function
local xmlContent = blue_prints.readFileContents(file_path)
if not xmlContent then
return false, "Could not read file: " .. provided_path
end
-- Use the XML string validator to check the content
return blue_prints.validate_blueprint_xml(xmlContent)
end

View File

@@ -0,0 +1,439 @@
if SERVER then return end --prevents it from running on the server
function blue_prints.clean_string(str)
local cleaned = str
-- Common control characters
cleaned = cleaned:gsub("\r", "") -- Carriage Return
cleaned = cleaned:gsub("\n", "") -- Line Feed
cleaned = cleaned:gsub("\t", "") -- Tab
cleaned = cleaned:gsub("\f", "") -- Form Feed
cleaned = cleaned:gsub("\b", "") -- Backspace
cleaned = cleaned:gsub("\v", "") -- Vertical Tab
cleaned = cleaned:gsub("\a", "") -- Bell (Alert)
cleaned = cleaned:gsub("\027", "") -- Escape
cleaned = cleaned:gsub("\000", "") -- Null byte
cleaned = cleaned:gsub("\x1A", "") -- EOF (Control-Z)
cleaned = cleaned:gsub("%z", "") -- Additional null bytes
cleaned = cleaned:gsub("%c", "") -- Any remaining control characters
-- Remove Unicode zero-width characters
cleaned = cleaned:gsub("\u{200B}", "") -- Zero-width space
cleaned = cleaned:gsub("\u{200C}", "") -- Zero-width non-joiner
cleaned = cleaned:gsub("\u{200D}", "") -- Zero-width joiner
cleaned = cleaned:gsub("\u{FEFF}", "") -- Zero-width no-break space (BOM)
return cleaned
end
function blue_prints.escapePercent(str)
return str:gsub("%%", "%%%%")
end
function blue_prints.getScreenResolution()
if Screen and Screen.Selected and Screen.Selected.Cam then
return Screen.Selected.Cam.Resolution
end
return nil
end
function blue_prints.get_description_from_xml(xmlString)
--remove all whitespace
local function trim(s)
return s:match("^%s*(.-)%s*$")
end
-- Define both patterns
local newFormatPattern = '<Label[^>]-header="<<<STRINGSTART>>>[dD][eE][sS][cC][rR][iI][pP][tT][iI][oO][nN]<<<STRINGEND>>>"[^>]-body="([^"]*)"[^>]*/>'
local oldFormatPattern = '<Label[^>]-header="[dD][eE][sS][cC][rR][iI][pP][tT][iI][oO][nN]"[^>]-body="([^"]*)"[^>]*/>'
-- Try matching new format first
local body = xmlString:match(newFormatPattern)
-- If not found, try old format
if not body then
body = xmlString:match(oldFormatPattern)
end
if not body then
return nil
end
--The trim(body) call takes the description text we found and removes any whitespace (spaces, tabs, newlines) from the beginning and end of the text before returning it.
return trim(body)
end
function blue_prints.get_component_count_from_xml(xmlString)
local count = 0
for _ in xmlString:gmatch('<Component.-/>') do
count = count + 1
end
return count
end
function blue_prints.get_xml_content_as_string_from_path(provided_path)
-- Check if the filename already ends with .txt
if not string.match(provided_path, "%.txt$") then
-- Add .txt if it's not already present
provided_path = provided_path .. ".txt"
end
local file_path = (blue_prints.save_path .. "/" .. provided_path)
local xmlContent, err = blue_prints.readFile(file_path)
if xmlContent then
return xmlContent
else
print("file not found")
print("saved designs:")
blue_prints.print_all_saved_files()
end
end
function blue_prints.print_requirements_of_circuit(provided_path)
local xmlContent = blue_prints.get_xml_content_as_string_from_path(provided_path)
if xmlContent then
-- In the usage section:
local inputs, outputs, components, wires, labels, inputNodePos, outputNodePos = blue_prints.parseXML(xmlContent)
print("This circuit uses: ")
local identifierCounts = {}
-- Count occurrences
for _, component in ipairs(components) do
--local prefab = ItemPrefab.GetItemPrefab(component.item)
local identifier = component.item
identifierCounts[identifier] = (identifierCounts[identifier] or 0) + 1
end
-- Print the counts
for identifier, count in pairs(identifierCounts) do
print(identifier .. ": " .. count)
end
end
end
function blue_prints.check_what_is_needed_for_blueprint(provided_path)
local xmlContent = blue_prints.get_xml_content_as_string_from_path(provided_path)
if xmlContent then
-- In the usage section:
local inputs, outputs, components, wires, labels, inputNodePos, outputNodePos = blue_prints.parseXML(xmlContent)
-- Check inventory for required components
local missing_components = blue_prints.check_inventory_for_requirements(components)
local all_needed_items_are_present = true
for _, count in pairs(missing_components) do
if count > 0 then
all_needed_items_are_present = false
break
end
end
if all_needed_items_are_present then
print("All required components are present!")
else
print("You are missing: ")
for name, count in pairs(missing_components) do
if count > 0 then
print(name .. ": " .. count)
end
end
print("You can also use an equivalent amount of FPGAs")
end
end
end
function blue_prints.get_components_currently_in_circuitbox(passed_circuitbox)
local components = passed_circuitbox.GetComponentString("CircuitBox").Components
local resourceCounts = {}
for i, component in pairs(components) do
--print(tostring(component.UsedResource.Identifier))
resourceCounts[tostring(component.UsedResource.Identifier)] = (resourceCounts[tostring(component.UsedResource.Identifier)] or 0) + 1
end
return resourceCounts
end
function blue_prints.getFurthestRightElement(components, labels, inputNodePos, outputNodePos)
local furthestRight = {x = -math.huge, y = 0, element = nil, type = nil}
local offset_adjustment = 256
-- Check components
for _, component in ipairs(components) do
if component.position.x + offset_adjustment > furthestRight.x then
furthestRight.x = component.position.x + offset_adjustment
furthestRight.y = component.position.y
furthestRight.element = component
furthestRight.type = "component"
end
end
-- Check labels
for _, label in ipairs(labels) do
--print(label.header, label.size.width)
local rightEdge = label.position.x + (label.size.width / 2)
if rightEdge > furthestRight.x then
furthestRight.x = rightEdge
furthestRight.y = label.position.y
furthestRight.element = label
furthestRight.type = "label"
end
end
-- Check input node
if inputNodePos and inputNodePos.x + offset_adjustment > furthestRight.x then --input and output nodes dont check width
furthestRight.x = inputNodePos.x + offset_adjustment
furthestRight.y = inputNodePos.y
furthestRight.element = inputNodePos
furthestRight.type = "inputNode"
end
-- Check output node
if outputNodePos and outputNodePos.x + offset_adjustment > furthestRight.x then
furthestRight.x = outputNodePos.x + offset_adjustment
furthestRight.y = outputNodePos.y
furthestRight.element = outputNodePos
furthestRight.type = "outputNode"
end
return furthestRight
end
function blue_prints.print_all_saved_files()
local saved_files = blue_prints.getFiles(blue_prints.save_path)
for name, value in pairs(saved_files) do
if string.match(value, "%.txt$") then
local filename = value:match("([^/\\]+)$") -- Match after last slash or backslash
local filename = string.gsub(filename, "%.txt$", "") --cut out the .txt at the end
local xml_of_file = blue_prints.readFileContents(value)
local description_of_file = blue_prints.get_description_from_xml(xml_of_file)
local number_of_components_in_file = blue_prints.get_component_count_from_xml(xml_of_file)
print("-------------")
local print_string = '‖color:white‖' .. filename .. '‖end‖' .. " - (" .. number_of_components_in_file .. " fpgas) "
if description_of_file ~= nil then
print_string = print_string .. " - " .. '‖color:yellow‖' .. description_of_file .. '‖end‖'
end
print(print_string)
end
end
end
function blue_prints.readFile(path)
local file = io.open(path, "r")
if not file then
return nil, "Failed to open file: " .. path
end
local content = file:read("*all")
file:close()
return content
end
function blue_prints.isInteger(str)
return str and not (str == "" or str:find("%D"))
end
function blue_prints.isFloat(str)
local n = tonumber(str)
return n ~= nil and math.floor(n) ~= n
end
function blue_prints.removeKeyFromTable(tbl, keyToRemove)
local newTable = {}
for k, v in pairs(tbl) do
if k ~= keyToRemove then
newTable[k] = v
end
end
return newTable
end
function blue_prints.hexToRGBA(hex)
-- Remove the '#' if present
hex = hex:gsub("#", "")
-- Check if it's a valid hex color
if #hex ~= 6 and #hex ~= 8 then
return nil, "Invalid hex color format"
end
-- Convert hex to decimal
local r = tonumber(hex:sub(1, 2), 16)
local g = tonumber(hex:sub(3, 4), 16)
local b = tonumber(hex:sub(5, 6), 16)
local a = 255
-- If alpha channel is provided
if #hex == 8 then
a = tonumber(hex:sub(7, 8), 16)
end
-- Return Color object
return Color(r, g, b, a)
end
function blue_prints.getNthValue(tbl, n)
local count = 0
for key, value in pairs(tbl) do
count = count + 1
if count == n then
return value
end
end
return nil -- Return nil if there are fewer than n items
end
function blue_prints.string_to_bool(passed_string)
-- Convert to lower case for case-insensitive comparison
local lower_string = string.lower(passed_string)
-- Check for common true values
if lower_string == "true" or lower_string == "1" then
return true
elseif lower_string == "false" or lower_string == "0" then
return false
else
-- Handle unexpected cases (could also return nil or error)
error("Invalid string for boolean conversion: " .. passed_string)
end
end
function blue_prints.get_circuit_box_lock_status()
-- First verify we have a selected circuitbox
if blue_prints.most_recent_circuitbox == nil then
print("No circuitbox detected")
return false
end
-- Get the CircuitBox component
local circuit_box = blue_prints.most_recent_circuitbox.GetComponentString("CircuitBox")
if circuit_box == nil then
print("Could not find CircuitBox component")
return false
end
-- Return true if either permanently or temporarily locked
return circuit_box.Locked or circuit_box.TemporarilyLocked
end
-- Usage example:
-- local is_locked = blue_prints.get_circuit_box_lock_status()
function blue_prints.count_circuit_elements_in_box()
if blue_prints.most_recent_circuitbox == nil then
print("no circuitbox detected")
return 0, 0, 0
end
local circuit_box = blue_prints.most_recent_circuitbox.GetComponentString("CircuitBox")
-- Initialize counts to 0
local num_components = 0
local num_labels = 0
local num_wires = 0
-- Only count if the collections exist
if circuit_box.Components then
num_components = #circuit_box.Components
end
if circuit_box.Labels then
num_labels = #circuit_box.Labels
end
if circuit_box.Wires then
num_wires = #circuit_box.Wires
end
return num_components, num_labels, num_wires
end
function blue_prints.calculate_clear_delay()
-- Get current counts of all elements
local num_components, num_labels, num_wires = blue_prints.count_circuit_elements()
-- Calculate individual delays for each type
local component_delay = 0
local label_delay = 0
local wire_delay = 0
-- Only calculate delays if we have elements to clear
if num_components > 0 then
component_delay = math.ceil(num_components / blue_prints.component_batch_size) * blue_prints.time_delay_between_loops
end
if num_labels > 0 then
label_delay = math.ceil(num_labels / blue_prints.component_batch_size) * blue_prints.time_delay_between_loops
end
if num_wires > 0 then
wire_delay = math.ceil(num_wires / blue_prints.component_batch_size) * blue_prints.time_delay_between_loops
end
-- Get minimum delay and add buffer time
--wire delay doesnt matter because wires are automatically removed when comps are removed
--there is some extra buffer in there to account for wires directly from input to output
local time_delay = component_delay + label_delay + 500
return time_delay
end
--save a reference to the most recently interacted circuit box
Hook.Patch("Barotrauma.Items.Components.CircuitBox", "AddToGUIUpdateList", function(instance, ptable)
blue_prints.most_recent_circuitbox = instance.Item
end, Hook.HookMethodType.After)