Jump to content

[TUT][CODE SNIPPET] Resource script bundler


Recommended Posts

  • Moderators

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:

  1. 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.
  2. 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

 

 

  • Thanks 1
Link to comment
  • IIYAMA changed the title to [TUT][CODE SNIPPET] Resource script bundler
  • Recently Browsing   0 members

    • No registered users viewing this page.
×
×
  • Create New...