Jump to content

IIYAMA

Moderators
  • Posts

    6,056
  • Joined

  • Last visited

  • Days Won

    208

Posts posted by IIYAMA

  1. Additional context:

    • Serverside is code executed on the server.
      • The application all players are connected to.
         
    • Clientside is code executed on each client/player his game.
      • There are as many clientsides as there are players in the server. They all run a copy of all clientside scripts.
      • Each player downloads the clientside code from the server. When a player joins the server, in most cases the copy of the code will only run until the download is finished. (with an exception if priority is given to a specific resource)
         
    • Shared is code executed
      • On the server
      • And a copy of the code is executed on each client / player his game

        ⚠️
      • Nothing is shared between them, except for running the same copy of the code.

     

    • Thanks 1
  2. 3 hours ago, Snow-Man2 said:

    leaderboard data:

    It looks like you got some unparsed JSON in your JSON result.

    console.log(typeof(leaderboardStats))
    
    // If type is object:
    const result = JSON.parse(leaderboardStats[0])
    
    // or if the type is a string:
    const result = JSON.parse(leaderboardStats)

     

    To make things a bit easier, use back-ticks so you do not have to escape your own JSON:

    local script = string.format("populateLeaderboard(`%s`);", escapedTestJSON)

     

     

  3. 9 minutes ago, Perseus said:

    I recorded another video, this time trying to compensate the latency:

    What if you switch roles and film the one that gets hit? That way you can verify if it is a bug.

    Also:

    • Double check if bullet sync is active.

     

    16 minutes ago, Perseus said:

    Is there a way to code a resource for lag compensation?

    It is indeed possible to override the bullet/damage system, but lag compensation is very complex and often result in unexpected results.

     

  4. 1 hour ago, Perseus said:

    As you can see in the video, the player will not get damage from the sides while sprinting

    What is the ping of the other player?

    It is normal to compensate your aim for latency, when the damage is registered clientside. You should aim at the location where the player is moving to. The higher the ping. the more you need to compensate.

    If you shoot a player in the back/front, latency matters less, it just delays the inevitable.

     

    Having 0 recoil makes the latency issue even worse, because the spread normally makes sure that at least some bullets hit.

    I recommend to put the recoil not to 0. It is fine to put it very low, but if you put it to 0, some calculations might be miscalculated.

     

    1 hour ago, Perseus said:

    I tried changing player_sync_interval from 100 to 50 but nothing.

    Increasing the interval does reduce some of the extra latency. To be specific from 100 > 50 could potential update the player position 50ms faster. But that is only the case with a stable internet connection. In case of an unstable internet connection, it might even increase latency when the network is being blocked by too many unreceived position updates.

  5. 3 minutes ago, Spc said:

    Yes, i'm using addDebugHook for detecting and blocking client-side functions as a form of anti-cheat, for example:

    This function is able to mimic that behaviour you just mentioned. Just be aware.

     

     

    3 minutes ago, Spc said:

    Do you mean adding multiple addEvent functions with same event name? I use this event in multiple resources.

    It might be worthed to look in to it and test if it has any impact if any of those are restarted.

    Also double check if the remote access is disabled for all of them. (2e argument of addEvent)

  6. 3 hours ago, ChvjCieObchodzi said:

    do you have any methods of downloading an image by URL without using local files and using only the client side

    If:

    • clientside only
    • without local files

    It might be possible with:

    fetchRemote + requestBrowserDomains

    Quote

    Wiki:

    fetchRemote

    Note: Client side function only works with the server the player is connected to unless the domain has been accepted with requestBrowserDomains

    But only very small images because of JSON limitations. fetchRemote only accepts JSON format. I do not recommend this method unless it is only for small avatars.

    You also need an webserver/host where you save the images in JSON format and send it to the client.

  7. 20 hours ago, RekZ said:

    it's very difficult to know which event is being continuously executed

    There is not really a non expensive way except for the debughook, which is CPU expensive to use. From my perspective, the more complex you make this threshold mechanism, the faster your server will be downed.

     

    For sensitive events attached to for example a database, you want to have some kind of firewall, for example: 

    checkPassiveTimer (utility with clean-up)

     

    • Like 1
  8. 23 minutes ago, DarkStalker30 said:
            if x and y and z and world and dimension and rotation then
                player = client
                setElementData(player, "player.activeMarker", false)

     

            if x and y and z and world and dimension and rotation then
                local player = client -- < HERE
                setElementData(player, "player.activeMarker", false)

    Make `player` a local variable. Else all players will share the variable `player` for the (delayed) timer functions.

     

    You can also pass the predefined `client` variable like this:

    setTimer(function (player)
    
    end, 1500, 1, client)

     

    • Thanks 1
  9. 27 minutes ago, Hiding said:

    if it needs to save for example 10 players at the same time?

    It depends on the amount of players and the server.

    10 players should be fine.

    More players? It is fine to save the the current km's inside of the memory. But writing it immediately to the database will definitely be a waste of performance, that can be spend else were.

    • Like 1
  10. 4 minutes ago, Hiding said:

    and I would like to save it to db when the player leaves the server to avoid unnecessary dbExec

    For example trigger every 10 seconds a latent event. Those last <10 seconds do not really matter. You want to balance performance and accuracy.

    To reduce server load and less db crash exploit able: Save it inside of the memory and write every 10/X minutes updates to the database. + onResourceStop

     

    • Like 1
  11. 7 hours ago, Hiding said:

    but doesn't work with the onClientPlayerQuit, why?

    Multiple reasons

    • onClientPlayerQuit is for remote players only. (Other players than yourself)
      • Currently when another player leaves, the stats of all other players are being saved, except yours.
    • When you quit the game, the client code on your system will stop.
      • Not sure if there is enough time to send data on onClientResourceStop + resourceRoot. But I wouldn't rely on it.
      • Using a timer to delay sending data, makes things really impossible.

     

    Other issues:

    • onClientPlayerWasted should be attached to the localPlayer, not the root element. Else it also triggers for remote players.

    • addEventHandler("onClientPlayerWasted", localPlayer, onPlayerWasted)

       

    • The source from remote events can't be trusted, use the predefined variable client instead:
    • if not exports.login:isPlayerLoggedIn(client) then return end 
      	
      exports.login:updatePlayerStat(client, "stat", "stat", data)

     

     

     

    • Like 1
  12. 21 hours ago, RekZ said:

    I can't really find any useful information that works.

    How about screenSource?

    Here a few lines of code I copied out of my smartphone remote control app. I can vaguely remember that there were some limits I ran in to, which is why the resolution is so low. But not sure which component was affected by it -> probably the JSON format by callRemote. I probably should have used fetchRemote instead, but didn't know better at that time.

     

    Client

    myScreenSource = dxCreateScreenSource( 640 / 2, 480 / 2 )
    
    --- ...
    
    dxUpdateScreenSource( myScreenSource )
    local pixels = dxGetTexturePixels( myScreenSource )
    local jpegPixels = dxConvertPixels( pixels, 'jpeg', 20 )
    triggerServerEvent( "screen-stream", localPlayer, jpegPixels )
    
    --- ...

     

    Server

    addEvent( "screen-stream", true )
    addEventHandler( "screen-stream", root, function( jpegPixels )
    
    	callRemote( "http://127.0.0.1:3134/stream", result, base64Encode( jpegPixels ) ) -- 
    
    end )

     

    App

    http.createServer(function (req, res) {
        if (req.url == "/stream") {
            const chunks = [];
            req.on('data', chunk => chunks.push(chunk));
            req.on('end', () => {
                const data = JSON.parse(Buffer.concat(chunks).toString('utf8'));
    
                if (data != undefined && data[0] != undefined) {
                   // I used socket IO to send the screenshots to my Phone
                   // io.emit('broadcast', data[0].toString('base64'));
                   // base64 image: "data:image/jpeg;base64," + data[0].toString('base64')
                }
            })
        }
        res.end();
    }).listen(3134, "127.0.0.1");

     

    Web browser:

    const image = new Image(2560, 1440);
    // Socket IO
    socket.on("broadcast", data => {
    	image.src = "data:image/jpeg;base64," + data // Replace an image with my screen
    });

     

     

  13. image.png.fd7917510361235186b5d2705921b1b2.png

    Did you know that the definition file counts up to a whopping 1364 unique functions? While the actual value might be higher or lower (missing, unknown, class/OOP or deprecated functions). Having this amount of functions in MTA is an incredible accomplish by this community. 🥳

    Some other statistics:

    • 125 client unique events
    • 88 server unique events

     

    • Like 1
  14. 2 hours ago, Whizz said:

    When it starts and when it finds ended.

    Downloads are not always done in a straight line. They are bound to resources. Even if the client finished downloading, a new resource can be started by the server admin.

     

    Clientside:

    Downloads are often busy when the transferbox is visible.

    https://wiki.multitheftauto.com/wiki/IsTransferBoxVisible

    According to the wiki, it is possible to forcefully show the transferbox at all times. But I doubt anybody would be willing to create a worse experience for themselves.

    https://wiki.multitheftauto.com/wiki/IsTransferBoxAlwaysVisible

     

    Serverside:

    Downloads often start at https://wiki.multitheftauto.com/wiki/OnPlayerJoin

    And end for each resource at:

    https://wiki.multitheftauto.com/wiki/OnPlayerResourceStart

     

     

     

    • Thanks 1
  15.  

    45 minutes ago, Mersad said:

    "Note: Calls may incur a performance overhead - they are not equivalent in performance to calling functions in the same resource."
    I felt that this might cause delays on the server.

    The code is ran single threaded so it doesn't really matter, it is not as if the CPU is doing nothing during that delay. Trigger events will move the execution to the next (server) frame, it is just delayed.

     

    Why that extra overhead? (for events and exports)

    • Another environment has to be accessed.
    • The data arguments/parameters have to be cloned. If tables are passed I can imagine that will be even slower.

     

    Here are some performance browser details + examples. Results will probably be different on your system.

    I expected the events to be much worse, but that doesn't really seems to be the case. Might be because exports do also return values back to the original caller. (for better or worse)

     

    function exportMe(a, b, c)
    
    end
    
    local thisResource = getThisResource()
    setTimer(function()
        for i = 1, 10000, 1 do
            call(thisResource, "exportMe", "a", "b", "c")
        end
    end, 50, 0)

    Export:
    58% > 60% CPU

     

    function exportMe(a, b, c)
    
    end
    
    addEvent("blabla", true)
    addEventHandler("blabla", resourceRoot, exportMe)
    
    setTimer(function()
        for i = 1, 10000, 1 do
            triggerEvent("blabla", resourceRoot, "a", "b", "c")
        end
    end, 50, 0)

    Event:
    62% > 64% CPU

    Context: This resourceRoot has no extra elements.

     

    function exportMe(a, b, c)
    
    end
    
    addEvent("blabla", true)
    addEventHandler("blabla", resourceRoot, exportMe)
    
    -- 400 extra children
    for i = 1, 400, 1 do
        createElement("test")
    end
    
    setTimer(function()
        for i = 1, 10000, 1 do
            triggerEvent("blabla", resourceRoot, "a", "b", "c")
        end
    end, 50, 0)

    Event:
    67 > 68% CPU

    Context: Source element with 400 extra children

     

     

     

  16. 7 minutes ago, Mersad said:

    If I were to use classic color placement (outputChatBox), it would be difficult, and editing the code would be harder. Is there a better way to achieve this? Additionally, if not, if I implement this with events, could it slow down or lag my server? What steps should I take to prevent this issue?

    I would also go for the suggestion @FernandoMTA  made, but with the syntax that works for you.

    If possible move some of the events that are triggered on the same side (client/server) to export functions.

     

    Serverside

    function customOutputChatBox  () -- export function
    	-- Add rate limit here for each player
    	-- Show message
    end

    How to create/call export functions?

     

     

    Clientside (if available)

    If you use remote access.

    function customOutputChatBox  () -- export function
    	-- Add rate limit here, protecting the server from spam
    	triggerServerEvent(...)
    end

     

     

  17. 1 hour ago, Mersad said:

    Is it possible that extensive use of events could cause the server to slow down or lag?

    It is possible, but it depends...

    There are a few ways to prevent possible lag:

    • Not trigger events with the source set to root.
    • Adding rate limits on clientside, to prevent (non cheating) players to abuse spamming events.
    • Being aware that elementdata also triggers events.

    And as TMTMTL said, some more context would be nice.

     

     

  18. 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
×
×
  • Create New...