Moderators IIYAMA Posted July 28 Moderators Share Posted July 28 Resource script bundler In some cases you want to intergrade one resource into another as if it is a kind of module. Currently we do not have many ways to create re-useable code. But in this example/snippet I will explain how to bundle resource files. You could in theory use this as a base to make your own resource importable like DGS DX Library that does. But importing is beyond the scope of this topic. Side note: This tutorial is created for programmers in mind. Some basic knowledge about Lua is required. Some comment lines starting with ---@, those are type annotations. Here you can find more information about those. I could have removed them, but I noticed that even without Lua Language Server some people still find them helpful. Spoiler ---@alias <newType> <type> Reuse able type ---@type <type> The type of the following variable or function ---@see <type> Refering to a specific type ---@cast <variable> <type> Replacing a type ---@cast <variable> -<type> Remove a specific type of a multi type. string|number|table > -number > = string|table Only scripts are copied in this bundler. The other files have to be moved manually. While using the bundler: you need to have the debug console open, else you will not see the error/logs. Lets get started! The first step is to create a serverside file called bundler.lua and add it to the meta.xml of the resource you want to bundle. <script src="bundler.lua" /> Now we have our script, we can start with writing code for opening the meta.xml file. -- function () local metaFile = xmlLoadFile("meta.xml", true) -- true = read only if not metaFile then return end --- reading the meta.xml here ... xmlUnloadFile(metaFile) -- end To read the meta.xml we call the function xmlNodeGetChildren, which gets all the direct children of the meta.xml root: local nodes = xmlNodeGetChildren(metaFile) for index, node in ipairs(nodes) do -- Going through all nodes end While going through the nodes, we need to validate if each node/file is OK to be bundled. We need to check if the node is a <script> node. And we also do not want to bundle all files. For example, we do not want to bundle the the current bundler.lua . By adding the attribute bundle="true", we can selective pick the correct scripts. In this example I am using an IIFE (Immediately Invoked Function Expression), to be able to use a guard clauses inside of loops. local nodes = xmlNodeGetChildren(metaFile) for index, node in ipairs(nodes) do (function() -- IIFE -- File validation if xmlNodeGetName(node) ~= "script" or xmlNodeGetAttribute(node, "bundle") ~= "true" then return end -- File is OK to be added to the bundle end)() end IIFE (Immediately Invoked Function Expression) Spoiler (function () end)() Spoiler IIFE in loops: Not good for performance, but good for readability. In this case performance doesn't really matter. Guard clause Spoiler function test () if a == true then return end -- <<< end If the files are bundle able, we need to: Get the type attribute. If none, the script is a serverside script. Get and check the src (source code) attribute And check if the script file actually exists on the disk. ---@alias scriptTypeClient "client" ---@alias scriptTypeServer "server" ---@alias scriptTypeShared "shared" --- ... (function() if xmlNodeGetName(node) ~= "script" or xmlNodeGetAttribute(node, "bundle") ~= "true" then return end ---@type scriptTypeClient|scriptTypeServer|scriptTypeShared local fileType = xmlNodeGetAttribute(node, "type") or "server" local src = xmlNodeGetAttribute(node, "src") if not src or src == "" or not fileExists(src) then outputDebugString("File is missing, index: " .. index .. ", src:" .. tostring(src), 2) return end end)() --- ... If that is OK, we can open the script ---@alias scriptTypeClient "client" ---@alias scriptTypeServer "server" ---@alias scriptTypeShared "shared" --- ... (function() if xmlNodeGetName(node) ~= "script" or xmlNodeGetAttribute(node, "bundle") ~= "true" then return end ---@type scriptTypeClient|scriptTypeServer|scriptTypeShared local fileType = xmlNodeGetAttribute(node, "type") or "server" local src = xmlNodeGetAttribute(node, "src") if not src or src == "" or not fileExists(src) then outputDebugString("File is missing, index: " .. index .. ", src:" .. tostring(src), 2) return end -- Here we open the script local scriptFile = fileOpen(src) if not scriptFile then outputDebugString("Unable to open file, index: " .. index .. ", src:" .. tostring(src), 2) return end fileClose(scriptFile) end)() --- ... Example meta.xml file: Spoiler <meta> <info author="IIYAMA" type="script" name="MTA-Communication-Enhancement" version="2.0.0" /> <min_mta_version client="1.5.4" server="1.5.4" /> <script bundle="true" src="sync/shared/eventNameObstruct.lua" type="shared" /> <script bundle="true" src="sync/shared/argumentValidation.lua" type="shared" /> <script bundle="true" src="sync/shared/callback.lua" type="shared" /> <script bundle="true" src="sync/shared/sync.lua" type="shared" /> <script bundle="true" src="sync/shared/debug.lua" type="shared" /> <script bundle="true" src="sync/server/remoteClientAccessPoints.lua" type="server" /> <script bundle="true" src="sync/server/sync.lua" type="server" /> <script bundle="true" src="sync/client/remoteClientAccessPoints.lua" type="client" /> <script bundle="true" src="sync/client/sync.lua" type="client" /> <script src="bundler/bundler.lua" type="server" /> </meta> To bundle we need to read the files and save the content inside of memory. But some small tweaks have to be made. ---@alias scriptTypeClient "client" ---@alias scriptTypeServer "server" ---@alias scriptTypeShared "shared" ---@alias bundleType {client: string[], server: string[]} ---@type bundleType local bundleContent = { client = {}, server = {}, } --- ... (function() --- ... --- Start reading here ! local content = "do--FILE:" .. src .. "\n" .. fileRead(scriptFile, fileGetSize(scriptFile)) .. "\nend" if fileType == "shared" then ---@cast fileType -scriptTypeClient, -scriptTypeServer -- Bundle shared files in clientside and serverside to maintain startup order bundleContent.server[#bundleContent.server + 1] = content bundleContent.client[#bundleContent.client + 1] = content else ---@cast fileType -scriptTypeShared bundleContent[fileType][#bundleContent[fileType] + 1] = content end end)() --- ... Each file have to start with an additional do and ends with an extra end . This is will create a new block scope for each files, make sure that the local variables are not exposed in other files. The string "\n" represents a new line. The file content is saved inside of the table bundleContent.client/server. Concatenating the strings for each file might cause lag, better to do it in one go later. Putting it together + command handler: ---@alias scriptTypeClient "client" ---@alias scriptTypeServer "server" ---@alias scriptTypeShared "shared" addCommandHandler("bundle", function(playerSource, commandName, ...) local metaFile = xmlLoadFile("meta.xml", true) if not metaFile then return end ---@alias bundleType {client: string[], server: string[]} ---@type bundleType local bundleContent = { client = {}, server = {}, } --[[ START META.XML file read ]] local nodes = xmlNodeGetChildren(metaFile) for index, node in ipairs(nodes) do (function() if xmlNodeGetName(node) ~= "script" or xmlNodeGetAttribute(node, "bundle") ~= "true" then return end ---@type scriptTypeClient|scriptTypeServer|scriptTypeShared local fileType = xmlNodeGetAttribute(node, "type") or "server" local src = xmlNodeGetAttribute(node, "src") if not src or src == "" or not fileExists(src) then outputDebugString("File is missing, index: " .. index .. ", src:" .. tostring(src), 2) return end local scriptFile = fileOpen(src) if not scriptFile then outputDebugString("Unable to open file, index: " .. index .. ", src:" .. tostring(src), 2) return end local content = "do--FILE:" .. src .. "\n" .. fileRead(scriptFile, fileGetSize(scriptFile)) .. "\nend" if fileType == "shared" then ---@cast fileType -scriptTypeClient, -scriptTypeServer -- Bundle shared files in clientside and serverside to maintain startup order bundleContent.server[#bundleContent.server + 1] = content bundleContent.client[#bundleContent.client + 1] = content else ---@cast fileType -scriptTypeShared bundleContent[fileType][#bundleContent[fileType] + 1] = content end fileClose(scriptFile) end)() end xmlUnloadFile(metaFile) end) Functions used to bundle clientside/serverside: local bundleFilePath = "example_bundle" .. "/" --[[ * Returns 0 if there is no file to be deleted. * Returns 1 if the file is deleted. * Returns 2 if the file is unable to be deleted. ]] ---@alias fileDeleteState 0|1|2 ---@param bundleContent bundleType The bundle ---@param typeOfFile scriptTypeServer|scriptTypeClient String 'client' or 'server ---@return boolean function createBundleFile(bundleContent, typeOfFile) local file = fileCreate(bundleFilePath .. typeOfFile .. ".lua") if not file then return false end local bundleFile = table.concat(bundleContent[typeOfFile], "\n") fileWrite(file, bundleFile) fileFlush(file) fileClose(file) return true end ---@see fileDeleteState ---@param typeOfFile scriptTypeServer|scriptTypeClient String 'client' or 'server ---@return fileDeleteState state The delete state: 0, 1, 2 function deleteBundleFile(typeOfFile) if not fileExists(bundleFilePath .. typeOfFile .. ".lua") then return 0 end return fileDelete(bundleFilePath .. typeOfFile .. ".lua") and 1 or 2 end Functions used to generate meta.xml script lines. local bundleFilePath = "example_bundle" .. "/" local metaFileName = "meta_fragment" --[[ * Returns 0 if there is no file to be deleted. * Returns 1 if the file is deleted. * Returns 2 if the file is unable to be deleted. ]] ---@alias fileDeleteState 0|1|2 --[[ Creates the meta fragment file, which contains the generated meta.xml script lines ]] ---@type fun(): boolean function createMetaFragment() local file = xmlCreateFile(bundleFilePath .. metaFileName .. ".xml", "meta") if not file then return false end local serverNode = xmlCreateChild(file, "script") xmlNodeSetAttribute(serverNode, "src", bundleFilePath .. "server.lua") xmlNodeSetAttribute(serverNode, "type", "server") local clientNode = xmlCreateChild(file, "script") xmlNodeSetAttribute(clientNode, "src", bundleFilePath .. "client.lua") xmlNodeSetAttribute(clientNode, "type", "client") xmlSaveFile(file) xmlUnloadFile(file) return true end --[[ Delete the meta fragment file ]] ---@see fileDeleteState ---@return fileDeleteState state The delete state: 0, 1, 2 function deleteMetaFragment() if not fileExists(bundleFilePath .. metaFileName .. ".xml") then return 0 end return fileDelete(bundleFilePath .. metaFileName .. ".xml") and 1 or 2 end And now using the functions from above to make the bundle. if deleteBundleFile("server") == 2 then error("Unable to replace bundle file: server.lua") elseif createBundleFile(bundleContent, "server") then outputDebugString("Created bundle file: server.lua", 4) end if deleteBundleFile("client") == 2 then error("Unable to replace bundle file: client.lua") elseif createBundleFile(bundleContent, "client") then outputDebugString("Created bundle file: client.lua", 4) end if deleteMetaFragment() == 2 then error("Unable to replace bundle file: " .. metaFileName .. ".xml") elseif createMetaFragment() then outputDebugString("Created file: " .. metaFileName .. ".xml", 4) end This happens in the following order: Delete the bundle if already exist If there is a problem with deleting: stop with an error. This is the point were your text editor might be blocking the deletion of the current bundle files. You need to resolve this manually. You need to have the debug console open, else you will not see the error/logs. Create bundle files Full script: Spoiler --[[ Add attribute bundle="true", to add files to the bundle. -- Shared <script bundle="true" src="shared.lua" type="shared" /> -- Serverside files <script bundle="true" src="server.lua" type="server" /> <script bundle="true" src="server.lua" /> -- Clientside <script bundle="true" src="client.lua" type="client" /> ]] -- The root directory of the bundles: local bundleFilePath = "example_bundle" .. "/" local metaFileName = "meta_fragment" ---@alias scriptTypeClient "client" ---@alias scriptTypeServer "server" ---@alias scriptTypeShared "shared" addCommandHandler("bundle", function(playerSource, commandName, ...) local metaFile = xmlLoadFile("meta.xml", true) if not metaFile then return end ---@alias bundleType {client: string[], server: string[]} ---@type bundleType local bundleContent = { client = {}, server = {}, } --[[ START META.XML file read ]] local nodes = xmlNodeGetChildren(metaFile) for index, node in ipairs(nodes) do (function() if xmlNodeGetName(node) ~= "script" or xmlNodeGetAttribute(node, "bundle") ~= "true" then return end ---@type scriptTypeClient|scriptTypeServer|scriptTypeShared local fileType = xmlNodeGetAttribute(node, "type") or "server" local src = xmlNodeGetAttribute(node, "src") if not src or src == "" or not fileExists(src) then outputDebugString("File is missing, index: " .. index .. ", src:" .. tostring(src), 2) return end local scriptFile = fileOpen(src) if not scriptFile then outputDebugString("Unable to open file, index: " .. index .. ", src:" .. tostring(src), 2) return end local content = "do--FILE:" .. src .. "\n" .. fileRead(scriptFile, fileGetSize(scriptFile)) .. "\nend" if fileType == "shared" then ---@cast fileType -scriptTypeClient, -scriptTypeServer -- Bundle shared files in clientside and serverside to maintain startup order bundleContent.server[#bundleContent.server + 1] = content bundleContent.client[#bundleContent.client + 1] = content else ---@cast fileType -scriptTypeShared bundleContent[fileType][#bundleContent[fileType] + 1] = content end fileClose(scriptFile) end)() end xmlUnloadFile(metaFile) --[[ END META.XML file read ]] --[[ START FILE WRITE ]] if deleteBundleFile("server") == 2 then error("Unable to replace bundle file: server.lua") elseif createBundleFile(bundleContent, "server") then outputDebugString("Created bundle file: server.lua", 4) end if deleteBundleFile("client") == 2 then error("Unable to replace bundle file: client.lua") elseif createBundleFile(bundleContent, "client") then outputDebugString("Created bundle file: client.lua", 4) end if deleteMetaFragment() == 2 then error("Unable to replace bundle file: " .. metaFileName .. ".xml") elseif createMetaFragment() then outputDebugString("Created file: " .. metaFileName .. ".xml", 4) end --[[ END FILE WRITE ]] outputDebugString("Bundle created!", 4) end) --[[ * Returns 0 if there is no file to be deleted. * Returns 1 if the file is deleted. * Returns 2 if the file is unable to be deleted. ]] ---@alias fileDeleteState 0|1|2 ---@param bundleContent bundleType The bundle ---@param typeOfFile scriptTypeServer|scriptTypeClient String 'client' or 'server ---@return boolean function createBundleFile(bundleContent, typeOfFile) local file = fileCreate(bundleFilePath .. typeOfFile .. ".lua") if not file then return false end local bundleFile = table.concat(bundleContent[typeOfFile], "\n") fileWrite(file, bundleFile) fileFlush(file) fileClose(file) return true end ---@see fileDeleteState ---@param typeOfFile scriptTypeServer|scriptTypeClient String 'client' or 'server ---@return fileDeleteState state The delete state: 0, 1, 2 function deleteBundleFile(typeOfFile) if not fileExists(bundleFilePath .. typeOfFile .. ".lua") then return 0 end return fileDelete(bundleFilePath .. typeOfFile .. ".lua") and 1 or 2 end --[[ Creates the meta fragment file, which contains the generated meta.xml script lines ]] ---@type fun(): boolean function createMetaFragment() local file = xmlCreateFile(bundleFilePath .. metaFileName .. ".xml", "meta") if not file then return false end local serverNode = xmlCreateChild(file, "script") xmlNodeSetAttribute(serverNode, "src", bundleFilePath .. "server.lua") xmlNodeSetAttribute(serverNode, "type", "server") local clientNode = xmlCreateChild(file, "script") xmlNodeSetAttribute(clientNode, "src", bundleFilePath .. "client.lua") xmlNodeSetAttribute(clientNode, "type", "client") xmlSaveFile(file) xmlUnloadFile(file) return true end --[[ Delete the meta fragment file ]] ---@see fileDeleteState ---@return fileDeleteState state The delete state: 0, 1, 2 function deleteMetaFragment() if not fileExists(bundleFilePath .. metaFileName .. ".xml") then return 0 end return fileDelete(bundleFilePath .. metaFileName .. ".xml") and 1 or 2 end 1 Link to comment
Recommended Posts