Guest User

cgfxBridge.lua v0.4

a guest
May 27th, 2025
37
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Lua 19.27 KB | None | 0 0
  1. --[[
  2. cgfxBridge.lua
  3. v0.4
  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.  
  304.  
  305. -- These two functions copied/modified from configFileReader
  306. local function parseLine(line, enums, allowranges, keephex)
  307.     -- ignore headings and comments
  308.     if string.match(line, "^%s*%[.*%]%s*$") or string.match(line, "^%s*[;#].*$") then
  309.         return nil, nil, false
  310.     end
  311.  
  312.     -- Can't use match to split because match is always greedy
  313.     local splitidx = string.find(line, "=")
  314.     if splitidx == nil then
  315.         return nil, nil, true
  316.     end
  317.     local key = string.match(string.sub(line, 1, splitidx-1), "^%s*(%S+)%s*$")
  318.     local value = string.match(string.sub(line, splitidx+1, -1), "^%s*(%S+.-)%s*$")
  319.    
  320.     if key ~= nil and value ~= nil then
  321.         if string.match(value, "^\".*\"$") or string.match(value, "^'.*'$") then --string surrounded by ' ' or " "
  322.             value = string.sub(value, 2, -2)
  323.         elseif allowranges and string.match(value, "%s*(.-)%s*:%s*(.-)%s*") then --number ranges
  324.             value = string.split(value, ":", true)
  325.             value[1] = tonumber(value[1])
  326.             value[2] = tonumber(value[2])
  327.         elseif keephex and string.match(value, "%s*(0x[0-9a-fA-F]+)%s*") then
  328.             value = value;
  329.         elseif tonumber(value) then --numbers/decimals
  330.             value = tonumber(value)
  331.         elseif value == "true" then --booleans
  332.             value = true
  333.         elseif value == "false" then
  334.             value = false
  335.         elseif enums ~= nil then
  336.             for k,v in pairs(enums) do
  337.                 if value == k then
  338.                     value = v
  339.                     break
  340.                 end
  341.             end
  342.         else
  343.             -- throw error?
  344.         end
  345.        
  346.         return key, value, false
  347.     else
  348.         -- Error
  349.         return nil, nil, true
  350.     end
  351. end
  352.  
  353. local function parseFoldersIni(path)
  354.     local objectPath = Misc.resolveFile(path)
  355.  
  356.     local finalArray = {}
  357.     local finalKeys = {}
  358.     if objectPath ~= nil then  
  359.    
  360.     local lns = io.readFileLines(objectPath)
  361.     if lns == nil then
  362.         error("Error loading config file "..objectPath, 2)
  363.     end
  364.         for _,line in ipairs(lns) do
  365.             if not string.match(line, "^%s*$") then
  366.                 local key, value, err = parseLine(line, nil, false, keephex);
  367.                 if(err) then
  368.                     local i = string.match(objectPath, '^.*()[/\\]');
  369.                     Misc.warn("Invalid line was passed to config file "..string.sub(objectPath,i-#objectPath)..": "..line,2);
  370.                 elseif key then
  371.                     finalArray[key] = value;
  372.                     finalKeys[#finalKeys+1] = key;
  373.                 end
  374.             end
  375.         end
  376.         return finalArray,finalKeys;
  377.     else
  378.         return nil,nil;
  379.     end
  380. end
  381.  
  382.  
  383.  
  384. local function processFile(v,v2)
  385.     local fileInfo
  386.  
  387.     -- Check via filename and extension capture;  pattern set up to exclude tileset inis
  388.     string.gsub(v2, "^([^%.]+)%.(%a+)$", function (nam,ext)
  389.  
  390.         -- Okay, this is a valid file
  391.         fileInfo = {
  392.             filename = v2,
  393.             noExt = nam,
  394.             path = v.."/"..v2,
  395.         }
  396.  
  397.         -- Txt
  398.         if      ext == "txt"
  399.         --or      ext == "ini"
  400.         then
  401.             fileInfo.ftype = "txt"
  402.             fileData.txt[#fileData.txt+1] = fileInfo
  403.  
  404.         -- Image
  405.         elseif  ext == "png"
  406.         or      ext == "gif"
  407.         then
  408.             fileInfo.ftype = "image"
  409.             fileData.images[#fileData.images+1] = fileInfo
  410.  
  411.         -- Audio
  412.         elseif  ext == "wav"
  413.         or      ext == "voc"
  414.         or      ext == "mp3"
  415.         or      ext == "ogg"
  416.         or      ext == "opus"
  417.         or      ext == "flac"
  418.         then
  419.             fileInfo.ftype = "audio"
  420.             fileData.sounds[#fileData.sounds+1] = fileInfo
  421.         end
  422.     end)
  423.  
  424.     if  fileInfo ~= nil  and  fileInfo.ftype ~= nil  then
  425.         fileData.all[#fileData.all+1] = fileInfo
  426.  
  427.         -- Get asset-specific info
  428.         if  fileInfo.ftype ~= "audio"  then
  429.  
  430.             string.gsub(fileInfo.filename, "(.+)%-(%d+)%.%a+$", function (assType,id)
  431.                 fileInfo.assetType = assType
  432.                 fileInfo.assetId = tonumber(id)
  433.             end)
  434.  
  435.             -- Apply the cgfx data
  436.             if  fileInfo.assetType ~= nil  and  fileInfo.assetId ~= nil  then
  437.                 local aType = fileInfo.assetType
  438.                 local aId = fileInfo.assetId
  439.  
  440.                 cgfx[aType] = cgfx[aType]  or  {}
  441.                 cgfxMap[aType] = cgfxMap[aType]  or  {}
  442.  
  443.                 local tblMap = cgfxMap[aType]
  444.                 if  cgfx[aType][aId] == nil  then
  445.                     tblMap[#tblMap+1] = aId
  446.                 end
  447.  
  448.                 cgfx[aType][aId] = cgfx[aType][aId]  or  {}
  449.  
  450.                 local tbl = cgfx[aType][aId]
  451.  
  452.                 tbl[fileInfo.ftype] = fileInfo
  453.             end
  454.         end
  455.  
  456.         return fileInfo
  457.  
  458.     else
  459.         return nil
  460.     end
  461. end
  462.  
  463.  
  464. -- derive key-path dictionaries and ordered key lists from both the level and episode folders.ini files
  465. local levelFoldersFile, levelFoldersKeys = parseFoldersIni(Misc.levelFolder().."/folders.ini")
  466. local episodeFoldersFile, episodeFoldersKeys = parseFoldersIni(Misc.levelFolder().."/../../folders.ini")
  467. --local levelFoldersFile = iniparse.parseTxt(Misc.levelFolder().."/folders.ini")
  468. --local episodeFoldersFile = iniparse.parseTxt(Misc.levelFolder().."/../../folders.ini")
  469. local joinedFoldersFile = levelFoldersFile  or  episodeFoldersFile
  470. local joinedFoldersKeys = levelFoldersKeys  or  episodeFoldersKeys
  471. local levelUniquePaths = {}
  472.  
  473.  
  474.  
  475.  
  476.  
  477.  
  478. function cgfxBridge.apply()
  479.  
  480.     -- Start checking folders.ini stuff
  481.     -- If both folders.ini files exist, combine them and their keys properly
  482.     if  levelFoldersFile ~= nil  and  episodeFoldersFile ~= nil  then
  483.         joinedFoldersFile = table.join(levelFoldersFile, episodeFoldersFile)
  484.        
  485.         local joinedKeysAlready = {}
  486.         joinedFoldersKeys = {}
  487.  
  488.  
  489.         local appended = table.append(levelFoldersKeys, episodeFoldersKeys)
  490.         if  cgfxBridge.reverseAppend  then
  491.             appended = table.append(episodeFoldersKeys, levelFoldersKeys)
  492.         end
  493.  
  494.         for  _,v in ipairs(appended)  do
  495.             if  not joinedKeysAlready[v]  then
  496.                 joinedKeysAlready[v] = true
  497.                 joinedFoldersKeys[#joinedFoldersKeys+1] = v
  498.             end
  499.         end
  500.     end
  501.  
  502.     -- Only proceed if a folders.ini exists at all
  503.     if  joinedFoldersFile  then
  504.         episodeFoldersFile = episodeFoldersFile  or  {}
  505.         levelFoldersFile = levelFoldersFile  or  {}
  506.  
  507.  
  508.         -- Get a list of required keys from a special key
  509.         local forcedKeysMap = table.map(cgfxBridge.requiredKeys)
  510.         --[[
  511.         if  joinedFoldersFile.__requiredkeys ~= nil  then
  512.             local __ = string.gsub(joinedFoldersFile.__requiredkeys, "%s*([%-%_%w]*)%s*,?", function (keyString)
  513.                 --Misc.dialog("FORCED KEY CAPTURE:", keyString)
  514.                 forcedKeysMap[keyString] = true
  515.             end)
  516.         end
  517.         --]]
  518.         --Misc.dialog("REQUIRED KEYS LIST: ",joinedFoldersFile.__requiredkeys, forcedKeysMap)
  519.        
  520.  
  521.         -- Go through every key-path pair in the combined dictionary
  522.         for  _,k in ipairs(joinedFoldersKeys)  do
  523.             local v = joinedFoldersFile[k]
  524.            
  525.             -- 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
  526.             if  k ~= "__requiredkeys"  and  (v ~= episodeFoldersFile[k]  or  v ~= levelFoldersFile[k]  or  forcedKeysMap[k])  then
  527.                 levelUniquePaths[#levelUniquePaths+1] = v
  528.             end
  529.         end
  530.  
  531.  
  532.         -- Go through all unique paths and get the file data
  533.         for  _,v in ipairs(levelUniquePaths)  do
  534.            
  535.             -- try to process it as a file first
  536.             local fileInfo = nil
  537.             local rejectedFilename = nil
  538.             local __ = string.gsub(v, "(.-/?)([^/]+%..+)$", function (folder,filename)
  539.                 fileInfo = processFile(folder,filename)
  540.                 rejectedFilename = filename
  541.             end)
  542.  
  543.             -- Turns out it's a folder, not a file
  544.             if  fileInfo == nil  and  rejectedFilename == nil  then
  545.                
  546.                 local files = Misc.listFiles(Misc.episodePath()..v)
  547.  
  548.                 for  _,v2 in ipairs(files)  do
  549.                     processFile(v,v2)
  550.                 end
  551.             end
  552.         end
  553.  
  554.  
  555.         -- Loop through and apply the graphics and configs
  556.         local failedConfigs = {}
  557.  
  558.         for  k,v in ipairs(classData)  do
  559.             local objKey = v[1]
  560.             local objClass = v[2]
  561.             local setConfigFunc = v[3]
  562.             local showDialog = v[4]
  563.  
  564.             if  cgfxMap[objKey]  then
  565.                 for  _,id in ipairs(cgfxMap[objKey])  do
  566.                     local replacement = cgfx[objKey][id]
  567.                    
  568.                     -- Replace images
  569.                     if  replacement.image  then
  570.                         Graphics.sprites[objKey][id].img = Graphics.loadImageResolved(replacement.image.path)
  571.                     end
  572.  
  573.                     -- Apply txt configs
  574.                     if  replacement.txt  then
  575.                         local props = iniparse.parseTxt(replacement.txt.path)
  576.  
  577.                         local classConfig = objClass.config
  578.                         local config = classConfig[id]
  579.                        
  580.                         -- Use any existing config set functions
  581.                         if  setConfigFunc  then
  582.                             if  showDialog  then
  583.                                 Misc.dialog("APPLYING CONFIG:", objKey, id, replacement.txt.path,"",props)
  584.                             end
  585.                             props.id = id
  586.                             setConfigFunc(props)
  587.                        
  588.                         -- Do it manually
  589.                         elseif  config  then
  590.                             for  k2,v2 in pairs(props)  do
  591.                                 k2 = k2:lower()
  592.                                 if  (
  593.                                         classConfig.propertiesMap
  594.                                     and classConfig.propertiesMap[k2]
  595.                                     )  
  596.                                 or  config[k2] ~= nil
  597.                                 or  (
  598.                                         manualWhitelist[objKey] ~= nil
  599.                                     and (
  600.                                             manualWhitelist[objKey].all[k2] ~= nil
  601.                                         or  (
  602.                                                 manualWhitelist[objKey][id] ~= nil
  603.                                             and manualWhitelist[objKey][id][k2] ~= nil
  604.                                             )
  605.                                         )
  606.                                     )
  607.                                 then
  608.                                     config[k2] = v2
  609.  
  610.                                 -- Debug fallback
  611.                                 elseif  cgfxBridge.debug  and  (debugExclude[objKey].all[k2] == nil  and  (debugExclude[objKey][id] == nil  or  debugExclude[objKey][id][k2] == nil))  then
  612.                                     if  failedConfigs[objKey] == nil  then
  613.                                         failedConfigs[objKey] = {}
  614.                                     end
  615.                                     local failedTbl = failedConfigs[objKey]
  616.                                     failedTbl[#failedTbl+1] = tostring(id)..":   "..k2.." = "..tostring(v2).."   ("..replacement.txt.path..")"
  617.                                 end
  618.                             end
  619.                         end
  620.                     end
  621.                 end
  622.             end
  623.         end
  624.  
  625.         -- Sound effects
  626.         for  k,v in ipairs(fileData.sounds)  do
  627.             local filename = v.filename
  628.             local noExt = v.noExt
  629.             local path = v.path
  630.             local id = sfxIDMap[noExt]
  631.  
  632.             if  id ~= nil  then
  633.                 Audio.sounds[id].sfx = SFX.open(path)
  634.             end
  635.         end
  636.  
  637.         if  cgfxBridge.debug  then
  638.             for  k,v in pairs(failedConfigs)  do
  639.                 Misc.dialog("Could not apply the following properties for the following "..k.." IDs:",unpack(v))
  640.             end
  641.         end
  642.     end
  643. end
  644.  
  645. --[[
  646. function cgfxBridge.onInitAPI()
  647.     registerEvent(cgfxBridge, "onStart")
  648. end
  649.  
  650. function cgfxBridge.onStart()
  651.     -- In case anything ever needs to be done this late in the process
  652. end
  653. --]]
  654.  
  655.  
  656.  
  657. return cgfxBridge
Advertisement
Add Comment
Please, Sign In to add comment