Guest User

cgfxBridge.lua

a guest
Sep 17th, 2024
93
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Lua 17.01 KB | None | 0 0
  1. --[[
  2. cgfxBridge.lua
  3. v0.3.5
  4. experimental level-agnostic cgfx system that piggybacks off folders.ini
  5. written by rixithechao
  6. special thanks to Chipss for help with testing this
  7.  
  8.  
  9.  
  10.     IMPORTANT WARNING!
  11.  
  12.     In order for this library to work properly, it MUST be loaded and run (via calling cgfxBridge.apply() ) outside of any lunalua events.  So the top of your luna.lua file should look similar to this:
  13.    
  14.    
  15.     local cgfxBridge = require("scripts/cgfxBridge")
  16.  
  17.     cgfxBridge.requiredKeys = {"config-block","config-background","config-npc","config-effect"}
  18.     cgfxBridge.registerManualWhitelist("all", {"plucktime","explosionradius"})
  19.  
  20.     cgfxBridge.debug = true
  21.     cgfxBridge.registerDebugExclusion({"block","npc"}, "hpbarstyle")
  22.  
  23.     cgfxBridge.apply()
  24.  
  25. --]]
  26.  
  27.  
  28. -- Main library table and configurable properties
  29. local cgfxBridge = {
  30.  
  31.     -- A list of keys that cgfxBridge will always load
  32.     requiredKeys = {},
  33.  
  34.     -- If cgfxBridge seems to favor the episode's folders.ini over the ones in the level's folders.ini for you, try setting this to true.  This fix really shouldn't work, but it does, so
  35.     -- ¯\_(ツ)_/¯
  36.     reverseAppend = true,
  37.  
  38.     -- If this is set to true, cgfxBridge will report any properties it's unable to apply that aren't excluded via cgfxBridge.registerDebugExclusion()
  39.     debug = false
  40. }
  41.  
  42.  
  43.  
  44. local iniparse = require("configFileReader")
  45. local npcManager = require("npcManager")
  46. local blockManager = require("blockManager")
  47.  
  48.  
  49.  
  50. local debugExclude = {
  51.     block = {
  52.         all = {}
  53.     },
  54.     background = {
  55.         all = {}
  56.     },
  57.     npc = {
  58.         all = {}
  59.     },
  60.     effect = {
  61.         all = {}
  62.     }
  63. }
  64. local manualWhitelist = {
  65.     block = {
  66.         all = {}
  67.     },
  68.     background = {
  69.         all = {}
  70.     },
  71.     npc = {
  72.         all = {}
  73.     },
  74.     effect = {
  75.         all = {}
  76.     }
  77. }
  78. local function registerProperties(tbl, objKeys, properties, value, ids)
  79.     if  objKeys == nil  or  objKeys == "all"  then
  80.         objKeys = {"block", "background", "npc", "effect"}
  81.     elseif  type(objKeys) ~= "table"  then
  82.         objKeys = {objKeys}
  83.     end
  84.  
  85.     if  type(properties) ~= "table"  then
  86.         properties = {properties}
  87.     end
  88.  
  89.     if  type(ids) ~= "table"  then
  90.         ids = {ids}
  91.     end
  92.  
  93.     for  _,objKey in ipairs(objKeys)  do
  94.         for  _,property in ipairs(properties)  do
  95.             if  ids == nil  or  #ids == 0  then
  96.                 tbl[objKey].all[property] = value
  97.             else
  98.                 for  _,id in ipairs(ids)  do
  99.                     tbl[objKey][id] = tbl[objKey][id]  or  {}
  100.                     tbl[objKey][id][property] = value
  101.                 end
  102.             end
  103.         end
  104.     end
  105. end
  106.  
  107.  
  108. --[[ The following applies to both of these functions:
  109.       - objKeys, properties, and ids can all be either a string or an ordered list of strings
  110.       - ids is optional, if not specified it will default to registering the properties for all IDs
  111. --]]
  112.  
  113. -- Exclude one or more properties from being reported when debugging is enabled
  114. function cgfxBridge.registerDebugExclusion(objKeys, properties, ids)
  115.     registerProperties(debugExclude, objKeys, properties, true, ids)
  116. end
  117. -- Any custom config properties for BGOs and effects need to be registered with this function
  118. function cgfxBridge.registerManualWhitelist(objKeys, properties, ids)
  119.     registerProperties(manualWhitelist, objKeys, properties, true, ids)
  120. end
  121.  
  122.  
  123.  
  124.  
  125. -- EVERYTHING BEYOND THIS POINT IS JUST SETUP (and cgfxBridge.apply()) SO NO NEED TO SCROLL FURTHER
  126.  
  127. -- Whitelist these properties for manual handling
  128. cgfxBridge.registerManualWhitelist("all", {
  129.     "lightoffsetx",
  130.     "lightoffsety",
  131.     "lightradius",
  132.     "lightbrightness",
  133.     "lightflicker",
  134.     "lightcolor"
  135. })
  136. cgfxBridge.registerManualWhitelist("block", {"noshadows"}) -- pretty sure this line doesn't do anything but gonna keep it just in case
  137. cgfxBridge.registerManualWhitelist("effect", { -- literally every effect property needs to be whitelisted because of how the basegame effect config works
  138.     "img",
  139.     "delay",
  140.     "xAlign","yAlign",
  141.     "spawnBindX","spawnBindY",
  142.     "xOffset","yOffset",
  143.     "lifetime",
  144.     "sound",
  145.     "frames","framespeed",
  146.     "width","height",
  147.     "priority",
  148.     "opacity",
  149.     "direction",
  150.     "variants","variant",
  151.     "import",
  152.     "onInit","onTick","onDeath",
  153.     "template",
  154.     "speedX","speedY",
  155.     "gravity",
  156.     "maxSpeedX","maxSpeedY",
  157.     "angle",
  158.     "rotation"
  159. })
  160.  
  161.  
  162. -- Exclude editor properties from debug reporting
  163. cgfxBridge.registerDebugExclusion("all", {"name", "group", "category", "icon", "grid", "gridoffsetx", "gridoffsety", "gridalign"}) -- editor properties
  164. cgfxBridge.registerDebugExclusion("block", {"default-slippery", "content_id"})
  165. cgfxBridge.registerDebugExclusion({"block","background","npc"}, {"image"})
  166.  
  167.  
  168.  
  169.  
  170.  
  171. local classData = {
  172.     {"npc", NPC, npcManager.setNpcSettings},
  173.     {"block", Block, blockManager.setBlockSettings},
  174.     {"background", BGO},
  175.     {"effect", Effect},
  176. }
  177.  
  178. local fileData = {
  179.     all = {},
  180.     images = {},
  181.     sounds = {},
  182.     txt = {}
  183. }
  184.  
  185. local cgfx = {}
  186. local cgfxMap = {}
  187.  
  188.  
  189.  
  190. -- These two functions copied/modified from configFileReader
  191. local function parseLine(line, enums, allowranges, keephex)
  192.     -- ignore headings and comments
  193.     if string.match(line, "^%s*%[.*%]%s*$") or string.match(line, "^%s*[;#].*$") then
  194.         return nil, nil, false
  195.     end
  196.  
  197.     -- Can't use match to split because match is always greedy
  198.     local splitidx = string.find(line, "=")
  199.     if splitidx == nil then
  200.         return nil, nil, true
  201.     end
  202.     local key = string.match(string.sub(line, 1, splitidx-1), "^%s*(%S+)%s*$")
  203.     local value = string.match(string.sub(line, splitidx+1, -1), "^%s*(%S+.-)%s*$")
  204.    
  205.     if key ~= nil and value ~= nil then
  206.         if string.match(value, "^\".*\"$") or string.match(value, "^'.*'$") then --string surrounded by ' ' or " "
  207.             value = string.sub(value, 2, -2)
  208.         elseif allowranges and string.match(value, "%s*(.-)%s*:%s*(.-)%s*") then --number ranges
  209.             value = string.split(value, ":", true)
  210.             value[1] = tonumber(value[1])
  211.             value[2] = tonumber(value[2])
  212.         elseif keephex and string.match(value, "%s*(0x[0-9a-fA-F]+)%s*") then
  213.             value = value;
  214.         elseif tonumber(value) then --numbers/decimals
  215.             value = tonumber(value)
  216.         elseif value == "true" then --booleans
  217.             value = true
  218.         elseif value == "false" then
  219.             value = false
  220.         elseif enums ~= nil then
  221.             for k,v in pairs(enums) do
  222.                 if value == k then
  223.                     value = v
  224.                     break
  225.                 end
  226.             end
  227.         else
  228.             -- throw error?
  229.         end
  230.        
  231.         return key, value, false
  232.     else
  233.         -- Error
  234.         return nil, nil, true
  235.     end
  236. end
  237.  
  238. local function parseFoldersIni(path)
  239.     local objectPath = Misc.resolveFile(path)
  240.  
  241.     local finalArray = {}
  242.     local finalKeys = {}
  243.     if objectPath ~= nil then  
  244.    
  245.     local lns = io.readFileLines(objectPath)
  246.     if lns == nil then
  247.         error("Error loading config file "..objectPath, 2)
  248.     end
  249.         for _,line in ipairs(lns) do
  250.             if not string.match(line, "^%s*$") then
  251.                 local key, value, err = parseLine(line, nil, false, keephex);
  252.                 if(err) then
  253.                     local i = string.match(objectPath, '^.*()[/\\]');
  254.                     Misc.warn("Invalid line was passed to config file "..string.sub(objectPath,i-#objectPath)..": "..line,2);
  255.                 elseif key then
  256.                     finalArray[key] = value;
  257.                     finalKeys[#finalKeys+1] = key;
  258.                 end
  259.             end
  260.         end
  261.         return finalArray,finalKeys;
  262.     else
  263.         return nil,nil;
  264.     end
  265. end
  266.  
  267.  
  268.  
  269. local function processFile(v,v2)
  270.     local fileInfo
  271.  
  272.     -- Check via filename and extension capture;  pattern set up to exclude tileset inis
  273.     string.gsub(v2, "^([^%.]+)%.(%a+)$", function (nam,ext)
  274.  
  275.         -- Okay, this is a valid file
  276.         fileInfo = {
  277.             filename = v2,
  278.             noExt = nam,
  279.             path = v.."/"..v2,
  280.         }
  281.  
  282.         -- Txt
  283.         if      ext == "txt"
  284.         --or      ext == "ini"
  285.         then
  286.             fileInfo.ftype = "txt"
  287.             fileData.txt[#fileData.txt+1] = fileInfo
  288.  
  289.         -- Image
  290.         elseif  ext == "png"
  291.         or      ext == "gif"
  292.         then
  293.             fileInfo.ftype = "image"
  294.             fileData.images[#fileData.images+1] = fileInfo
  295.  
  296.         -- Audio
  297.         elseif  ext == "wav"
  298.         or      ext == "voc"
  299.         or      ext == "mp3"
  300.         or      ext == "ogg"
  301.         or      ext == "opus"
  302.         or      ext == "flac"
  303.         then
  304.             fileInfo.ftype = "audio"
  305.             fileData.sounds[#fileData.sounds+1] = fileInfo
  306.         end
  307.     end)
  308.  
  309.     if  fileInfo ~= nil  and  fileInfo.ftype ~= nil  then
  310.         fileData.all[#fileData.all+1] = fileInfo
  311.  
  312.         -- Get asset-specific info
  313.         if  fileInfo.ftype ~= "audio"  then
  314.  
  315.             string.gsub(fileInfo.filename, "(.+)%-(%d+)%.%a+$", function (assType,id)
  316.                 fileInfo.assetType = assType
  317.                 fileInfo.assetId = tonumber(id)
  318.             end)
  319.  
  320.             -- Apply the cgfx data
  321.             if  fileInfo.assetType ~= nil  and  fileInfo.assetId ~= nil  then
  322.                 local aType = fileInfo.assetType
  323.                 local aId = fileInfo.assetId
  324.  
  325.                 cgfx[aType] = cgfx[aType]  or  {}
  326.                 cgfxMap[aType] = cgfxMap[aType]  or  {}
  327.  
  328.                 local tblMap = cgfxMap[aType]
  329.                 if  cgfx[aType][aId] == nil  then
  330.                     tblMap[#tblMap+1] = aId
  331.                 end
  332.  
  333.                 cgfx[aType][aId] = cgfx[aType][aId]  or  {}
  334.  
  335.                 local tbl = cgfx[aType][aId]
  336.  
  337.                 tbl[fileInfo.ftype] = fileInfo
  338.             end
  339.         end
  340.  
  341.         return fileInfo
  342.  
  343.     else
  344.         return nil
  345.     end
  346. end
  347.  
  348.  
  349. -- derive key-path dictionaries and ordered key lists from both the level and episode folders.ini files
  350. local levelFoldersFile, levelFoldersKeys = parseFoldersIni(Misc.levelFolder().."/folders.ini")
  351. local episodeFoldersFile, episodeFoldersKeys = parseFoldersIni(Misc.levelFolder().."/../../folders.ini")
  352. --local levelFoldersFile = iniparse.parseTxt(Misc.levelFolder().."/folders.ini")
  353. --local episodeFoldersFile = iniparse.parseTxt(Misc.levelFolder().."/../../folders.ini")
  354. local joinedFoldersFile = levelFoldersFile  or  episodeFoldersFile
  355. local joinedFoldersKeys = levelFoldersKeys  or  episodeFoldersKeys
  356. local levelUniquePaths = {}
  357.  
  358.  
  359.  
  360.  
  361.  
  362.  
  363. function cgfxBridge.apply()
  364.  
  365.     -- Start checking folders.ini stuff
  366.     -- If both folders.ini files exist, combine them and their keys properly
  367.     if  levelFoldersFile ~= nil  and  episodeFoldersFile ~= nil  then
  368.         joinedFoldersFile = table.join(levelFoldersFile, episodeFoldersFile)
  369.        
  370.         local joinedKeysAlready = {}
  371.         joinedFoldersKeys = {}
  372.  
  373.  
  374.         local appended = table.append(levelFoldersKeys, episodeFoldersKeys)
  375.         if  cgfxBridge.reverseAppend  then
  376.             appended = table.append(episodeFoldersKeys, levelFoldersKeys)
  377.         end
  378.  
  379.         for  _,v in ipairs(appended)  do
  380.             if  not joinedKeysAlready[v]  then
  381.                 joinedKeysAlready[v] = true
  382.                 joinedFoldersKeys[#joinedFoldersKeys+1] = v
  383.             end
  384.         end
  385.     end
  386.  
  387.     -- Only proceed if a folders.ini exists at all
  388.     if  joinedFoldersFile  then
  389.         episodeFoldersFile = episodeFoldersFile  or  {}
  390.         levelFoldersFile = levelFoldersFile  or  {}
  391.  
  392.  
  393.         -- Get a list of required keys from a special key
  394.         local forcedKeysMap = table.map(cgfxBridge.requiredKeys)
  395.         --[[
  396.         if  joinedFoldersFile.__requiredkeys ~= nil  then
  397.             local __ = string.gsub(joinedFoldersFile.__requiredkeys, "%s*([%-%_%w]*)%s*,?", function (keyString)
  398.                 --Misc.dialog("FORCED KEY CAPTURE:", keyString)
  399.                 forcedKeysMap[keyString] = true
  400.             end)
  401.         end
  402.         --]]
  403.         --Misc.dialog("REQUIRED KEYS LIST: ",joinedFoldersFile.__requiredkeys, forcedKeysMap)
  404.        
  405.  
  406.         -- Go through every key-path pair in the combined dictionary
  407.         for  _,k in ipairs(joinedFoldersKeys)  do
  408.             local v = joinedFoldersFile[k]
  409.            
  410.             -- If the path in this pair doesn't match the path under the same key in either file (or it's one of the required keys), consider it a level override and add it to the list of paths to pull replacements and configs from
  411.             if  k ~= "__requiredkeys"  and  (v ~= episodeFoldersFile[k]  or  v ~= levelFoldersFile[k]  or  forcedKeysMap[k])  then
  412.                 levelUniquePaths[#levelUniquePaths+1] = v
  413.             end
  414.         end
  415.  
  416.  
  417.         -- Go through all unique paths and get the file data
  418.         for  _,v in ipairs(levelUniquePaths)  do
  419.            
  420.             -- try to process it as a file first
  421.             local fileInfo = nil
  422.             local rejectedFilename = nil
  423.             local __ = string.gsub(v, "(.-/?)([^/]+%..+)$", function (folder,filename)
  424.                 fileInfo = processFile(folder,filename)
  425.                 rejectedFilename = filename
  426.             end)
  427.  
  428.             -- Turns out it's a folder, not a file
  429.             if  fileInfo == nil  and  rejectedFilename == nil  then
  430.                
  431.                 local files = Misc.listFiles(Misc.episodePath()..v)
  432.  
  433.                 for  _,v2 in ipairs(files)  do
  434.                     processFile(v,v2)
  435.                 end
  436.             end
  437.         end
  438.  
  439.  
  440.         -- Loop through and apply the graphics and configs
  441.         local failedConfigs = {}
  442.  
  443.         for  k,v in ipairs(classData)  do
  444.             local objKey = v[1]
  445.             local objClass = v[2]
  446.             local setConfigFunc = v[3]
  447.             local showDialog = v[4]
  448.  
  449.             if  cgfxMap[objKey]  then
  450.                 for  _,id in ipairs(cgfxMap[objKey])  do
  451.                     local replacement = cgfx[objKey][id]
  452.                    
  453.                     -- Replace images
  454.                     if  replacement.image  then
  455.                         Graphics.sprites[objKey][id].img = Graphics.loadImageResolved(replacement.image.path)
  456.                     end
  457.  
  458.                     -- Apply txt configs
  459.                     if  replacement.txt  then
  460.                         local props = iniparse.parseTxt(replacement.txt.path)
  461.  
  462.                         local classConfig = objClass.config
  463.                         local config = classConfig[id]
  464.                        
  465.                         -- Use any existing config set functions
  466.                         if  setConfigFunc  then
  467.                             if  showDialog  then
  468.                                 Misc.dialog("APPLYING CONFIG:", objKey, id, replacement.txt.path,"",props)
  469.                             end
  470.                             props.id = id
  471.                             setConfigFunc(props)
  472.                        
  473.                         -- Do it manually
  474.                         elseif  config  then
  475.                             for  k2,v2 in pairs(props)  do
  476.                                 k2 = k2:lower()
  477.                                 if  (
  478.                                         classConfig.propertiesMap
  479.                                     and classConfig.propertiesMap[k2]
  480.                                     )  
  481.                                 or  config[k2] ~= nil
  482.                                 or  (
  483.                                         manualWhitelist[objKey] ~= nil
  484.                                     and (
  485.                                             manualWhitelist[objKey].all[k2] ~= nil
  486.                                         or  (
  487.                                                 manualWhitelist[objKey][id] ~= nil
  488.                                             and manualWhitelist[objKey][id][k2] ~= nil
  489.                                             )
  490.                                         )
  491.                                     )
  492.                                 then
  493.                                     config[k2] = v2
  494.  
  495.                                 -- Debug fallback
  496.                                 elseif  cgfxBridge.debug  and  (debugExclude[objKey].all[k2] == nil  and  (debugExclude[objKey][id] == nil  or  debugExclude[objKey][id][k2] == nil))  then
  497.                                     if  failedConfigs[objKey] == nil  then
  498.                                         failedConfigs[objKey] = {}
  499.                                     end
  500.                                     local failedTbl = failedConfigs[objKey]
  501.                                     failedTbl[#failedTbl+1] = tostring(id)..":   "..k2.." = "..tostring(v2).."   ("..replacement.txt.path..")"
  502.                                 end
  503.                             end
  504.                         end
  505.                     end
  506.                 end
  507.             end
  508.         end
  509.  
  510.         if  cgfxBridge.debug  then
  511.             for  k,v in pairs(failedConfigs)  do
  512.                 Misc.dialog("Could not apply the following properties for the following "..k.." IDs:",unpack(v))
  513.             end
  514.         end
  515.     end
  516. end
  517.  
  518. --[[
  519. function cgfxBridge.onInitAPI()
  520.     registerEvent(cgfxBridge, "onStart")
  521. end
  522.  
  523. function cgfxBridge.onStart()
  524.     -- In case anything ever needs to be done this late in the process
  525. end
  526. --]]
  527.  
  528.  
  529.  
  530. return cgfxBridge
Advertisement
Add Comment
Please, Sign In to add comment