Guest User

cgfxBridge.lua v0.5

a guest
Aug 18th, 2025
19
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Lua 19.50 KB | None | 0 0
  1. --[[
  2. cgfxBridge.lua
  3. v0.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.     yoshit = {
  64.         all = {}
  65.     },
  66.     yoshib = {
  67.         all = {}
  68.     }
  69. }
  70. local manualWhitelist = {
  71.     block = {
  72.         all = {}
  73.     },
  74.     background = {
  75.         all = {}
  76.     },
  77.     npc = {
  78.         all = {}
  79.     },
  80.     effect = {
  81.         all = {}
  82.     },
  83.     yoshit = {
  84.         all = {}
  85.     },
  86.     yoshib = {
  87.         all = {}
  88.     }
  89. }
  90. local function registerProperties(tbl, objKeys, properties, value, ids)
  91.     if  objKeys == nil  or  objKeys == "all"  then
  92.         objKeys = {"block", "background", "npc", "effect", "yoshit", "yoshib"}
  93.     elseif  type(objKeys) ~= "table"  then
  94.         objKeys = {objKeys}
  95.     end
  96.  
  97.     if  type(properties) ~= "table"  then
  98.         properties = {properties}
  99.     end
  100.  
  101.     if  type(ids) ~= "table"  then
  102.         ids = {ids}
  103.     end
  104.  
  105.     for  _,objKey in ipairs(objKeys)  do
  106.         for  _,property in ipairs(properties)  do
  107.             if  ids == nil  or  #ids == 0  then
  108.                 tbl[objKey].all[property] = value
  109.             else
  110.                 for  _,id in ipairs(ids)  do
  111.                     tbl[objKey][id] = tbl[objKey][id]  or  {}
  112.                     tbl[objKey][id][property] = value
  113.                 end
  114.             end
  115.         end
  116.     end
  117. end
  118.  
  119.  
  120. --[[ The following applies to both of these functions:
  121.       - objKeys, properties, and ids can all be either a string or an ordered list of strings
  122.       - ids is optional, if not specified it will default to registering the properties for all IDs
  123. --]]
  124.  
  125. -- Exclude one or more properties from being reported when debugging is enabled
  126. function cgfxBridge.registerDebugExclusion(objKeys, properties, ids)
  127.     registerProperties(debugExclude, objKeys, properties, true, ids)
  128. end
  129. -- Any custom config properties for BGOs and effects need to be registered with this function
  130. function cgfxBridge.registerManualWhitelist(objKeys, properties, ids)
  131.     registerProperties(manualWhitelist, objKeys, properties, true, ids)
  132. end
  133.  
  134.  
  135.  
  136.  
  137. -- EVERYTHING BEYOND THIS POINT IS JUST SETUP (and cgfxBridge.apply()) SO NO NEED TO SCROLL FURTHER
  138.  
  139. -- Whitelist these properties for manual handling
  140. cgfxBridge.registerManualWhitelist("all", {
  141.     "lightoffsetx",
  142.     "lightoffsety",
  143.     "lightradius",
  144.     "lightbrightness",
  145.     "lightflicker",
  146.     "lightcolor"
  147. })
  148. cgfxBridge.registerManualWhitelist("block", {"noshadows"}) -- pretty sure this line doesn't do anything but gonna keep it just in case
  149. cgfxBridge.registerManualWhitelist("effect", { -- literally every effect property needs to be whitelisted because of how the basegame effect config works
  150.     "img",
  151.     "delay",
  152.     "xAlign","yAlign",
  153.     "spawnBindX","spawnBindY",
  154.     "xOffset","yOffset",
  155.     "lifetime",
  156.     "sound",
  157.     "frames","framespeed",
  158.     "width","height",
  159.     "priority",
  160.     "opacity",
  161.     "direction",
  162.     "variants","variant",
  163.     "import",
  164.     "onInit","onTick","onDeath",
  165.     "template",
  166.     "speedX","speedY",
  167.     "gravity",
  168.     "maxSpeedX","maxSpeedY",
  169.     "angle",
  170.     "rotation"
  171. })
  172.  
  173.  
  174. -- Exclude editor properties from debug reporting
  175. cgfxBridge.registerDebugExclusion("all", {"name", "group", "category", "icon", "grid", "gridoffsetx", "gridoffsety", "gridalign"}) -- editor properties
  176. cgfxBridge.registerDebugExclusion("block", {"default-slippery", "content_id"})
  177. cgfxBridge.registerDebugExclusion({"block","background","npc"}, {"image"})
  178.  
  179.  
  180.  
  181. -- SFX ID map
  182. local sfxNameList = {
  183.     "player-jump",
  184.     "stomped",
  185.     "block-hit",
  186.     "block-smash",
  187.     "player-shrink",
  188.     "player-grow",
  189.     "mushroom",
  190.     "player-died",
  191.     "shell-hit",
  192.     "player-slide",
  193.     "item-dropped",
  194.     "has-item",
  195.     "camera-change",
  196.     "coin",
  197.     "1up",
  198.     "lava",
  199.     "warp",
  200.     "fireball",
  201.     "level-win",
  202.     "boss-beat",
  203.     "dungeon-win",
  204.     "bullet-bill",
  205.     "grab",
  206.     "spring",
  207.     "hammer",
  208.     "slide",
  209.     "newpath",
  210.     "level-select",
  211.     "do",
  212.     "pause",
  213.     "key",
  214.     "pswitch",
  215.     "tail",
  216.     "racoon",
  217.     "boot",
  218.     "smash",
  219.     "thwomp",
  220.     "birdo-spit",
  221.     "birdo-hit",
  222.     "smb2-exit",
  223.     "birdo-beat",
  224.     "npc-fireball",
  225.     "fireworks",
  226.     "bowser-killed",
  227.     "game-beat",
  228.     "door",
  229.     "message",
  230.     "yoshi",
  231.     "yoshi-hurt",
  232.     "yoshi-tongue",
  233.     "yoshi-egg",
  234.     "got-star",
  235.     "zelda-kill",
  236.     "player-died2",
  237.     "yoshi-swallow",
  238.     "ring",
  239.     "dry-bones",
  240.     "smw-checkpoint",
  241.     "dragon-coin",
  242.     "smw-exit",
  243.     "smw-blaarg",
  244.     "wart-bubble",
  245.     "wart-die",
  246.     "sm-block-hit",
  247.     "sm-killed",
  248.     "sm-hurt",
  249.     "sm-glass",
  250.     "sm-boss-hit",
  251.     "sm-cry",
  252.     "sm-explosion",
  253.     "climbing",
  254.     "swim",
  255.     "grab2",
  256.     "smw-saw",
  257.     "smb2-throw",
  258.     "smb2-hit",
  259.     "zelda-stab",
  260.     "zelda-hurt",
  261.     "zelda-heart",
  262.     "zelda-died",
  263.     "zelda-rupee",
  264.     "zelda-fire",
  265.     "zelda-item",
  266.     "zelda-key",
  267.     "zelda-shield",
  268.     "zelda-dash",
  269.     "zelda-fairy",
  270.     "zelda-grass",
  271.     "zelda-hit",
  272.     "zelda-sword-beam",
  273.     "bubble"
  274. }
  275. local sfxIDMap = {}
  276. for  k,v in ipairs(sfxNameList)  do
  277.     sfxIDMap[v] = k
  278. end
  279.  
  280.  
  281.  
  282.  
  283. local classData = {
  284.     {"npc", NPC, npcManager.setNpcSettings},
  285.     {"block", Block, blockManager.setBlockSettings},
  286.     {"background", BGO},
  287.     {"effect", Effect},
  288.     {"yoshit"},
  289.     {"yoshib"},
  290.     {""}
  291. }
  292.  
  293. local fileData = {
  294.     all = {},
  295.     images = {},
  296.     sounds = {},
  297.     txt = {}
  298. }
  299.  
  300. local cgfx = {}
  301. local cgfxMap = {}
  302.  
  303. local resourceFolderPaths
  304. --local resourceFolderPathMap
  305.  
  306.  
  307.  
  308. -- These two functions copied/modified from configFileReader
  309. local function parseLine(line, enums, allowranges, keephex)
  310.     -- ignore headings and comments
  311.     if string.match(line, "^%s*%[.*%]%s*$") or string.match(line, "^%s*[;#].*$") then
  312.         return nil, nil, false
  313.     end
  314.  
  315.     -- Can't use match to split because match is always greedy
  316.     local splitidx = string.find(line, "=")
  317.     if splitidx == nil then
  318.         return nil, nil, true
  319.     end
  320.     local key = string.match(string.sub(line, 1, splitidx-1), "^%s*(%S+)%s*$")
  321.     local value = string.match(string.sub(line, splitidx+1, -1), "^%s*(%S+.-)%s*$")
  322.    
  323.     if key ~= nil and value ~= nil then
  324.         if string.match(value, "^\".*\"$") or string.match(value, "^'.*'$") then --string surrounded by ' ' or " "
  325.             value = string.sub(value, 2, -2)
  326.         elseif allowranges and string.match(value, "%s*(.-)%s*:%s*(.-)%s*") then --number ranges
  327.             value = string.split(value, ":", true)
  328.             value[1] = tonumber(value[1])
  329.             value[2] = tonumber(value[2])
  330.         elseif keephex and string.match(value, "%s*(0x[0-9a-fA-F]+)%s*") then
  331.             value = value;
  332.         elseif tonumber(value) then --numbers/decimals
  333.             value = tonumber(value)
  334.         elseif value == "true" then --booleans
  335.             value = true
  336.         elseif value == "false" then
  337.             value = false
  338.         elseif enums ~= nil then
  339.             for k,v in pairs(enums) do
  340.                 if value == k then
  341.                     value = v
  342.                     break
  343.                 end
  344.             end
  345.         else
  346.             -- throw error?
  347.         end
  348.        
  349.         return key, value, false
  350.     else
  351.         -- Error
  352.         return nil, nil, true
  353.     end
  354. end
  355.  
  356. local function parseFoldersIni(path)
  357.     local objectPath = Misc.resolveFile(path)
  358.  
  359.     local finalArray = {}
  360.     local finalKeys = {}
  361.     if objectPath ~= nil then  
  362.    
  363.     local lns = io.readFileLines(objectPath)
  364.     if lns == nil then
  365.         error("Error loading config file "..objectPath, 2)
  366.     end
  367.         for _,line in ipairs(lns) do
  368.             if not string.match(line, "^%s*$") then
  369.                 local key, value, err = parseLine(line, nil, false, keephex);
  370.                 if(err) then
  371.                     local i = string.match(objectPath, '^.*()[/\\]');
  372.                     Misc.warn("Invalid line was passed to config file "..string.sub(objectPath,i-#objectPath)..": "..line,2);
  373.                 elseif key then
  374.                     finalArray[key] = value;
  375.                     finalKeys[#finalKeys+1] = key;
  376.                 end
  377.             end
  378.         end
  379.         return finalArray,finalKeys;
  380.     else
  381.         return nil,nil;
  382.     end
  383. end
  384.  
  385.  
  386.  
  387. local function processFile(v,v2)
  388.     local fileInfo
  389.  
  390.     -- Check via filename and extension capture;  pattern set up to exclude tileset inis
  391.     string.gsub(v2, "^([^%.]+)%.(%a+)$", function (nam,ext)
  392.  
  393.         -- Okay, this is a valid file
  394.         fileInfo = {
  395.             filename = v2,
  396.             noExt = nam,
  397.             path = v.."/"..v2,
  398.         }
  399.  
  400.         -- Txt
  401.         if      ext == "txt"
  402.         --or      ext == "ini"
  403.         then
  404.             fileInfo.ftype = "txt"
  405.             fileData.txt[#fileData.txt+1] = fileInfo
  406.  
  407.         -- Image
  408.         elseif  ext == "png"
  409.         or      ext == "gif"
  410.         then
  411.             fileInfo.ftype = "image"
  412.             fileData.images[#fileData.images+1] = fileInfo
  413.  
  414.         -- Audio
  415.         elseif  ext == "wav"
  416.         or      ext == "voc"
  417.         or      ext == "mp3"
  418.         or      ext == "ogg"
  419.         or      ext == "opus"
  420.         or      ext == "flac"
  421.         then
  422.             fileInfo.ftype = "audio"
  423.             fileData.sounds[#fileData.sounds+1] = fileInfo
  424.         end
  425.     end)
  426.  
  427.     if  fileInfo ~= nil  and  fileInfo.ftype ~= nil  then
  428.         fileData.all[#fileData.all+1] = fileInfo
  429.  
  430.         -- Get asset-specific info
  431.         if  fileInfo.ftype ~= "audio"  then
  432.  
  433.             string.gsub(fileInfo.filename, "(.+)%-(%d+)%.%a+$", function (assType,id)
  434.                 fileInfo.assetType = assType
  435.                 fileInfo.assetId = tonumber(id)
  436.             end)
  437.  
  438.             -- Apply the cgfx data
  439.             if  fileInfo.assetType ~= nil  and  fileInfo.assetId ~= nil  then
  440.                 local aType = fileInfo.assetType
  441.                 local aId = fileInfo.assetId
  442.  
  443.                 cgfx[aType] = cgfx[aType]  or  {}
  444.                 cgfxMap[aType] = cgfxMap[aType]  or  {}
  445.  
  446.                 local tblMap = cgfxMap[aType]
  447.                 if  cgfx[aType][aId] == nil  then
  448.                     tblMap[#tblMap+1] = aId
  449.                 end
  450.  
  451.                 cgfx[aType][aId] = cgfx[aType][aId]  or  {}
  452.  
  453.                 local tbl = cgfx[aType][aId]
  454.  
  455.                 tbl[fileInfo.ftype] = fileInfo
  456.             end
  457.         end
  458.  
  459.         return fileInfo
  460.  
  461.     else
  462.         return nil
  463.     end
  464. end
  465.  
  466.  
  467. -- derive key-path dictionaries and ordered key lists from both the level and episode folders.ini files
  468. local levelFoldersFile, levelFoldersKeys = parseFoldersIni(Misc.levelFolder().."/folders.ini")
  469. local episodeFoldersFile, episodeFoldersKeys = parseFoldersIni(Misc.levelFolder().."/../../folders.ini")
  470. --local levelFoldersFile = iniparse.parseTxt(Misc.levelFolder().."/folders.ini")
  471. --local episodeFoldersFile = iniparse.parseTxt(Misc.levelFolder().."/../../folders.ini")
  472. local joinedFoldersFile = levelFoldersFile  or  episodeFoldersFile
  473. local joinedFoldersKeys = levelFoldersKeys  or  episodeFoldersKeys
  474. local levelUniquePaths = {}
  475.  
  476.  
  477.  
  478.  
  479.  
  480. function cgfxBridge.apply()
  481.  
  482.     resourceFolderPaths = Misc.listFiles(Misc.levelPath()  or  Misc.episodePath())
  483.     --resourceFolderPathMap = table.map(resourceFolderPaths)
  484.  
  485.     -- Start checking folders.ini stuff
  486.     -- If both folders.ini files exist, combine them and their keys properly
  487.     if  levelFoldersFile ~= nil  and  episodeFoldersFile ~= nil  then
  488.         joinedFoldersFile = table.join(levelFoldersFile, episodeFoldersFile)
  489.        
  490.         local joinedKeysAlready = {}
  491.         joinedFoldersKeys = {}
  492.  
  493.  
  494.         local appended = table.append(levelFoldersKeys, episodeFoldersKeys)
  495.         if  cgfxBridge.reverseAppend  then
  496.             appended = table.append(episodeFoldersKeys, levelFoldersKeys)
  497.         end
  498.  
  499.         for  _,v in ipairs(appended)  do
  500.             if  not joinedKeysAlready[v]  then
  501.                 joinedKeysAlready[v] = true
  502.                 joinedFoldersKeys[#joinedFoldersKeys+1] = v
  503.             end
  504.         end
  505.     end
  506.  
  507.     -- Only proceed if a folders.ini exists at all
  508.     if  joinedFoldersFile  then
  509.         episodeFoldersFile = episodeFoldersFile  or  {}
  510.         levelFoldersFile = levelFoldersFile  or  {}
  511.  
  512.  
  513.         -- Get a list of required keys from a special key
  514.         local forcedKeysMap = table.map(cgfxBridge.requiredKeys)
  515.         --[[
  516.         if  joinedFoldersFile.__requiredkeys ~= nil  then
  517.             local __ = string.gsub(joinedFoldersFile.__requiredkeys, "%s*([%-%_%w]*)%s*,?", function (keyString)
  518.                 --Misc.dialog("FORCED KEY CAPTURE:", keyString)
  519.                 forcedKeysMap[keyString] = true
  520.             end)
  521.         end
  522.         --]]
  523.         --Misc.dialog("REQUIRED KEYS LIST: ",joinedFoldersFile.__requiredkeys, forcedKeysMap)
  524.        
  525.  
  526.         -- Go through every key-path pair in the combined dictionary
  527.         for  _,k in ipairs(joinedFoldersKeys)  do
  528.             local v = joinedFoldersFile[k]
  529.            
  530.             -- 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
  531.             if  k ~= "__requiredkeys"  and  (v ~= episodeFoldersFile[k]  or  v ~= levelFoldersFile[k]  or  forcedKeysMap[k])  then
  532.                 levelUniquePaths[#levelUniquePaths+1] = v
  533.             end
  534.         end
  535.  
  536.  
  537.         -- Go through all unique paths and get the file data
  538.         for  _,v in ipairs(table.append(levelUniquePaths, resourceFolderPaths))  do
  539.            
  540.             -- try to process it as a file first
  541.             local fileInfo = nil
  542.             local rejectedFilename = nil
  543.             local __ = string.gsub(v, "(.-/?)([^/]+%..+)$", function (folder,filename)
  544.                 fileInfo = processFile(folder,filename)
  545.                 rejectedFilename = filename
  546.             end)
  547.  
  548.             -- Turns out it's a folder, not a file
  549.             if  fileInfo == nil  and  rejectedFilename == nil  then
  550.                
  551.                 local files = Misc.listFiles(Misc.episodePath()..v)
  552.  
  553.                 for  _,v2 in ipairs(files)  do
  554.                     processFile(v,v2)
  555.                 end
  556.             end
  557.         end
  558.  
  559.  
  560.         -- Loop through and apply the graphics and configs
  561.         local failedConfigs = {}
  562.  
  563.         for  k,v in ipairs(classData)  do
  564.             local objKey = v[1]
  565.             local objClass = v[2]
  566.             local setConfigFunc = v[3]
  567.             local showDialog = v[4]
  568.  
  569.             if  cgfxMap[objKey]  then
  570.                 for  _,id in ipairs(cgfxMap[objKey])  do
  571.                     local replacement = cgfx[objKey][id]
  572.                    
  573.                     -- Replace images
  574.                     if  replacement.image  then
  575.                         Graphics.sprites[objKey][id].img = Graphics.loadImageResolved(replacement.image.path)
  576.                     end
  577.  
  578.                     -- Apply txt configs
  579.                     if  replacement.txt  then
  580.                         local props = iniparse.parseTxt(replacement.txt.path)
  581.  
  582.                         local classConfig = objClass.config
  583.                         local config = classConfig[id]
  584.                        
  585.                         -- Use any existing config set functions
  586.                         if  setConfigFunc  then
  587.                             if  showDialog  then
  588.                                 Misc.dialog("APPLYING CONFIG:", objKey, id, replacement.txt.path,"",props)
  589.                             end
  590.                             props.id = id
  591.                             setConfigFunc(props)
  592.                        
  593.                         -- Do it manually
  594.                         elseif  config  then
  595.                             for  k2,v2 in pairs(props)  do
  596.                                 k2 = k2:lower()
  597.                                 if  (
  598.                                         classConfig.propertiesMap
  599.                                     and classConfig.propertiesMap[k2]
  600.                                     )  
  601.                                 or  config[k2] ~= nil
  602.                                 or  (
  603.                                         manualWhitelist[objKey] ~= nil
  604.                                     and (
  605.                                             manualWhitelist[objKey].all[k2] ~= nil
  606.                                         or  (
  607.                                                 manualWhitelist[objKey][id] ~= nil
  608.                                             and manualWhitelist[objKey][id][k2] ~= nil
  609.                                             )
  610.                                         )
  611.                                     )
  612.                                 then
  613.                                     config[k2] = v2
  614.  
  615.                                 -- Debug fallback
  616.                                 elseif  cgfxBridge.debug  and  (debugExclude[objKey].all[k2] == nil  and  (debugExclude[objKey][id] == nil  or  debugExclude[objKey][id][k2] == nil))  then
  617.                                     if  failedConfigs[objKey] == nil  then
  618.                                         failedConfigs[objKey] = {}
  619.                                     end
  620.                                     local failedTbl = failedConfigs[objKey]
  621.                                     failedTbl[#failedTbl+1] = tostring(id)..":   "..k2.." = "..tostring(v2).."   ("..replacement.txt.path..")"
  622.                                 end
  623.                             end
  624.                         end
  625.                     end
  626.                 end
  627.             end
  628.         end
  629.  
  630.         -- Sound effects
  631.         for  k,v in ipairs(fileData.sounds)  do
  632.             local filename = v.filename
  633.             local noExt = v.noExt
  634.             local path = v.path
  635.             local id = sfxIDMap[noExt]
  636.  
  637.             if  id ~= nil  then
  638.                 Audio.sounds[id].sfx = SFX.open(path)
  639.             end
  640.         end
  641.  
  642.         if  cgfxBridge.debug  then
  643.             for  k,v in pairs(failedConfigs)  do
  644.                 Misc.dialog("Could not apply the following properties for the following "..k.." IDs:",unpack(v))
  645.             end
  646.         end
  647.     end
  648. end
  649.  
  650. --[[
  651. function cgfxBridge.onInitAPI()
  652.     registerEvent(cgfxBridge, "onStart")
  653. end
  654.  
  655. function cgfxBridge.onStart()
  656.     -- In case anything ever needs to be done this late in the process
  657. end
  658. --]]
  659.  
  660.  
  661.  
  662. return cgfxBridge
Advertisement
Add Comment
Please, Sign In to add comment