Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- --[[
- cgfxBridge.lua
- v0.4
- experimental level-agnostic cgfx system that piggybacks off folders.ini
- written by rixithechao
- special thanks to Chipss for help with testing this
- IMPORTANT WARNING!
- 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:
- local cgfxBridge = require("scripts/cgfxBridge")
- cgfxBridge.requiredKeys = {"config-block","config-background","config-npc","config-effect"}
- cgfxBridge.registerManualWhitelist("all", {"plucktime","explosionradius"})
- cgfxBridge.debug = true
- cgfxBridge.registerDebugExclusion({"block","npc"}, "hpbarstyle")
- cgfxBridge.apply()
- --]]
- -- Main library table and configurable properties
- local cgfxBridge = {
- -- A list of keys that cgfxBridge will always load
- requiredKeys = {},
- -- 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
- -- ¯\_(ツ)_/¯
- reverseAppend = true,
- -- If this is set to true, cgfxBridge will report any properties it's unable to apply that aren't excluded via cgfxBridge.registerDebugExclusion()
- debug = false
- }
- local iniparse = require("configFileReader")
- local npcManager = require("npcManager")
- local blockManager = require("blockManager")
- local debugExclude = {
- block = {
- all = {}
- },
- background = {
- all = {}
- },
- npc = {
- all = {}
- },
- effect = {
- all = {}
- },
- yoshit = {
- all = {}
- },
- yoshib = {
- all = {}
- }
- }
- local manualWhitelist = {
- block = {
- all = {}
- },
- background = {
- all = {}
- },
- npc = {
- all = {}
- },
- effect = {
- all = {}
- },
- yoshit = {
- all = {}
- },
- yoshib = {
- all = {}
- }
- }
- local function registerProperties(tbl, objKeys, properties, value, ids)
- if objKeys == nil or objKeys == "all" then
- objKeys = {"block", "background", "npc", "effect", "yoshit", "yoshib"}
- elseif type(objKeys) ~= "table" then
- objKeys = {objKeys}
- end
- if type(properties) ~= "table" then
- properties = {properties}
- end
- if type(ids) ~= "table" then
- ids = {ids}
- end
- for _,objKey in ipairs(objKeys) do
- for _,property in ipairs(properties) do
- if ids == nil or #ids == 0 then
- tbl[objKey].all[property] = value
- else
- for _,id in ipairs(ids) do
- tbl[objKey][id] = tbl[objKey][id] or {}
- tbl[objKey][id][property] = value
- end
- end
- end
- end
- end
- --[[ The following applies to both of these functions:
- - objKeys, properties, and ids can all be either a string or an ordered list of strings
- - ids is optional, if not specified it will default to registering the properties for all IDs
- --]]
- -- Exclude one or more properties from being reported when debugging is enabled
- function cgfxBridge.registerDebugExclusion(objKeys, properties, ids)
- registerProperties(debugExclude, objKeys, properties, true, ids)
- end
- -- Any custom config properties for BGOs and effects need to be registered with this function
- function cgfxBridge.registerManualWhitelist(objKeys, properties, ids)
- registerProperties(manualWhitelist, objKeys, properties, true, ids)
- end
- -- EVERYTHING BEYOND THIS POINT IS JUST SETUP (and cgfxBridge.apply()) SO NO NEED TO SCROLL FURTHER
- -- Whitelist these properties for manual handling
- cgfxBridge.registerManualWhitelist("all", {
- "lightoffsetx",
- "lightoffsety",
- "lightradius",
- "lightbrightness",
- "lightflicker",
- "lightcolor"
- })
- cgfxBridge.registerManualWhitelist("block", {"noshadows"}) -- pretty sure this line doesn't do anything but gonna keep it just in case
- cgfxBridge.registerManualWhitelist("effect", { -- literally every effect property needs to be whitelisted because of how the basegame effect config works
- "img",
- "delay",
- "xAlign","yAlign",
- "spawnBindX","spawnBindY",
- "xOffset","yOffset",
- "lifetime",
- "sound",
- "frames","framespeed",
- "width","height",
- "priority",
- "opacity",
- "direction",
- "variants","variant",
- "import",
- "onInit","onTick","onDeath",
- "template",
- "speedX","speedY",
- "gravity",
- "maxSpeedX","maxSpeedY",
- "angle",
- "rotation"
- })
- -- Exclude editor properties from debug reporting
- cgfxBridge.registerDebugExclusion("all", {"name", "group", "category", "icon", "grid", "gridoffsetx", "gridoffsety", "gridalign"}) -- editor properties
- cgfxBridge.registerDebugExclusion("block", {"default-slippery", "content_id"})
- cgfxBridge.registerDebugExclusion({"block","background","npc"}, {"image"})
- -- SFX ID map
- local sfxNameList = {
- "player-jump",
- "stomped",
- "block-hit",
- "block-smash",
- "player-shrink",
- "player-grow",
- "mushroom",
- "player-died",
- "shell-hit",
- "player-slide",
- "item-dropped",
- "has-item",
- "camera-change",
- "coin",
- "1up",
- "lava",
- "warp",
- "fireball",
- "level-win",
- "boss-beat",
- "dungeon-win",
- "bullet-bill",
- "grab",
- "spring",
- "hammer",
- "slide",
- "newpath",
- "level-select",
- "do",
- "pause",
- "key",
- "pswitch",
- "tail",
- "racoon",
- "boot",
- "smash",
- "thwomp",
- "birdo-spit",
- "birdo-hit",
- "smb2-exit",
- "birdo-beat",
- "npc-fireball",
- "fireworks",
- "bowser-killed",
- "game-beat",
- "door",
- "message",
- "yoshi",
- "yoshi-hurt",
- "yoshi-tongue",
- "yoshi-egg",
- "got-star",
- "zelda-kill",
- "player-died2",
- "yoshi-swallow",
- "ring",
- "dry-bones",
- "smw-checkpoint",
- "dragon-coin",
- "smw-exit",
- "smw-blaarg",
- "wart-bubble",
- "wart-die",
- "sm-block-hit",
- "sm-killed",
- "sm-hurt",
- "sm-glass",
- "sm-boss-hit",
- "sm-cry",
- "sm-explosion",
- "climbing",
- "swim",
- "grab2",
- "smw-saw",
- "smb2-throw",
- "smb2-hit",
- "zelda-stab",
- "zelda-hurt",
- "zelda-heart",
- "zelda-died",
- "zelda-rupee",
- "zelda-fire",
- "zelda-item",
- "zelda-key",
- "zelda-shield",
- "zelda-dash",
- "zelda-fairy",
- "zelda-grass",
- "zelda-hit",
- "zelda-sword-beam",
- "bubble"
- }
- local sfxIDMap = {}
- for k,v in ipairs(sfxNameList) do
- sfxIDMap[v] = k
- end
- local classData = {
- {"npc", NPC, npcManager.setNpcSettings},
- {"block", Block, blockManager.setBlockSettings},
- {"background", BGO},
- {"effect", Effect},
- {"yoshit"},
- {"yoshib"},
- {""}
- }
- local fileData = {
- all = {},
- images = {},
- sounds = {},
- txt = {}
- }
- local cgfx = {}
- local cgfxMap = {}
- -- These two functions copied/modified from configFileReader
- local function parseLine(line, enums, allowranges, keephex)
- -- ignore headings and comments
- if string.match(line, "^%s*%[.*%]%s*$") or string.match(line, "^%s*[;#].*$") then
- return nil, nil, false
- end
- -- Can't use match to split because match is always greedy
- local splitidx = string.find(line, "=")
- if splitidx == nil then
- return nil, nil, true
- end
- local key = string.match(string.sub(line, 1, splitidx-1), "^%s*(%S+)%s*$")
- local value = string.match(string.sub(line, splitidx+1, -1), "^%s*(%S+.-)%s*$")
- if key ~= nil and value ~= nil then
- if string.match(value, "^\".*\"$") or string.match(value, "^'.*'$") then --string surrounded by ' ' or " "
- value = string.sub(value, 2, -2)
- elseif allowranges and string.match(value, "%s*(.-)%s*:%s*(.-)%s*") then --number ranges
- value = string.split(value, ":", true)
- value[1] = tonumber(value[1])
- value[2] = tonumber(value[2])
- elseif keephex and string.match(value, "%s*(0x[0-9a-fA-F]+)%s*") then
- value = value;
- elseif tonumber(value) then --numbers/decimals
- value = tonumber(value)
- elseif value == "true" then --booleans
- value = true
- elseif value == "false" then
- value = false
- elseif enums ~= nil then
- for k,v in pairs(enums) do
- if value == k then
- value = v
- break
- end
- end
- else
- -- throw error?
- end
- return key, value, false
- else
- -- Error
- return nil, nil, true
- end
- end
- local function parseFoldersIni(path)
- local objectPath = Misc.resolveFile(path)
- local finalArray = {}
- local finalKeys = {}
- if objectPath ~= nil then
- local lns = io.readFileLines(objectPath)
- if lns == nil then
- error("Error loading config file "..objectPath, 2)
- end
- for _,line in ipairs(lns) do
- if not string.match(line, "^%s*$") then
- local key, value, err = parseLine(line, nil, false, keephex);
- if(err) then
- local i = string.match(objectPath, '^.*()[/\\]');
- Misc.warn("Invalid line was passed to config file "..string.sub(objectPath,i-#objectPath)..": "..line,2);
- elseif key then
- finalArray[key] = value;
- finalKeys[#finalKeys+1] = key;
- end
- end
- end
- return finalArray,finalKeys;
- else
- return nil,nil;
- end
- end
- local function processFile(v,v2)
- local fileInfo
- -- Check via filename and extension capture; pattern set up to exclude tileset inis
- string.gsub(v2, "^([^%.]+)%.(%a+)$", function (nam,ext)
- -- Okay, this is a valid file
- fileInfo = {
- filename = v2,
- noExt = nam,
- path = v.."/"..v2,
- }
- -- Txt
- if ext == "txt"
- --or ext == "ini"
- then
- fileInfo.ftype = "txt"
- fileData.txt[#fileData.txt+1] = fileInfo
- -- Image
- elseif ext == "png"
- or ext == "gif"
- then
- fileInfo.ftype = "image"
- fileData.images[#fileData.images+1] = fileInfo
- -- Audio
- elseif ext == "wav"
- or ext == "voc"
- or ext == "mp3"
- or ext == "ogg"
- or ext == "opus"
- or ext == "flac"
- then
- fileInfo.ftype = "audio"
- fileData.sounds[#fileData.sounds+1] = fileInfo
- end
- end)
- if fileInfo ~= nil and fileInfo.ftype ~= nil then
- fileData.all[#fileData.all+1] = fileInfo
- -- Get asset-specific info
- if fileInfo.ftype ~= "audio" then
- string.gsub(fileInfo.filename, "(.+)%-(%d+)%.%a+$", function (assType,id)
- fileInfo.assetType = assType
- fileInfo.assetId = tonumber(id)
- end)
- -- Apply the cgfx data
- if fileInfo.assetType ~= nil and fileInfo.assetId ~= nil then
- local aType = fileInfo.assetType
- local aId = fileInfo.assetId
- cgfx[aType] = cgfx[aType] or {}
- cgfxMap[aType] = cgfxMap[aType] or {}
- local tblMap = cgfxMap[aType]
- if cgfx[aType][aId] == nil then
- tblMap[#tblMap+1] = aId
- end
- cgfx[aType][aId] = cgfx[aType][aId] or {}
- local tbl = cgfx[aType][aId]
- tbl[fileInfo.ftype] = fileInfo
- end
- end
- return fileInfo
- else
- return nil
- end
- end
- -- derive key-path dictionaries and ordered key lists from both the level and episode folders.ini files
- local levelFoldersFile, levelFoldersKeys = parseFoldersIni(Misc.levelFolder().."/folders.ini")
- local episodeFoldersFile, episodeFoldersKeys = parseFoldersIni(Misc.levelFolder().."/../../folders.ini")
- --local levelFoldersFile = iniparse.parseTxt(Misc.levelFolder().."/folders.ini")
- --local episodeFoldersFile = iniparse.parseTxt(Misc.levelFolder().."/../../folders.ini")
- local joinedFoldersFile = levelFoldersFile or episodeFoldersFile
- local joinedFoldersKeys = levelFoldersKeys or episodeFoldersKeys
- local levelUniquePaths = {}
- function cgfxBridge.apply()
- -- Start checking folders.ini stuff
- -- If both folders.ini files exist, combine them and their keys properly
- if levelFoldersFile ~= nil and episodeFoldersFile ~= nil then
- joinedFoldersFile = table.join(levelFoldersFile, episodeFoldersFile)
- local joinedKeysAlready = {}
- joinedFoldersKeys = {}
- local appended = table.append(levelFoldersKeys, episodeFoldersKeys)
- if cgfxBridge.reverseAppend then
- appended = table.append(episodeFoldersKeys, levelFoldersKeys)
- end
- for _,v in ipairs(appended) do
- if not joinedKeysAlready[v] then
- joinedKeysAlready[v] = true
- joinedFoldersKeys[#joinedFoldersKeys+1] = v
- end
- end
- end
- -- Only proceed if a folders.ini exists at all
- if joinedFoldersFile then
- episodeFoldersFile = episodeFoldersFile or {}
- levelFoldersFile = levelFoldersFile or {}
- -- Get a list of required keys from a special key
- local forcedKeysMap = table.map(cgfxBridge.requiredKeys)
- --[[
- if joinedFoldersFile.__requiredkeys ~= nil then
- local __ = string.gsub(joinedFoldersFile.__requiredkeys, "%s*([%-%_%w]*)%s*,?", function (keyString)
- --Misc.dialog("FORCED KEY CAPTURE:", keyString)
- forcedKeysMap[keyString] = true
- end)
- end
- --]]
- --Misc.dialog("REQUIRED KEYS LIST: ",joinedFoldersFile.__requiredkeys, forcedKeysMap)
- -- Go through every key-path pair in the combined dictionary
- for _,k in ipairs(joinedFoldersKeys) do
- local v = joinedFoldersFile[k]
- -- 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
- if k ~= "__requiredkeys" and (v ~= episodeFoldersFile[k] or v ~= levelFoldersFile[k] or forcedKeysMap[k]) then
- levelUniquePaths[#levelUniquePaths+1] = v
- end
- end
- -- Go through all unique paths and get the file data
- for _,v in ipairs(levelUniquePaths) do
- -- try to process it as a file first
- local fileInfo = nil
- local rejectedFilename = nil
- local __ = string.gsub(v, "(.-/?)([^/]+%..+)$", function (folder,filename)
- fileInfo = processFile(folder,filename)
- rejectedFilename = filename
- end)
- -- Turns out it's a folder, not a file
- if fileInfo == nil and rejectedFilename == nil then
- local files = Misc.listFiles(Misc.episodePath()..v)
- for _,v2 in ipairs(files) do
- processFile(v,v2)
- end
- end
- end
- -- Loop through and apply the graphics and configs
- local failedConfigs = {}
- for k,v in ipairs(classData) do
- local objKey = v[1]
- local objClass = v[2]
- local setConfigFunc = v[3]
- local showDialog = v[4]
- if cgfxMap[objKey] then
- for _,id in ipairs(cgfxMap[objKey]) do
- local replacement = cgfx[objKey][id]
- -- Replace images
- if replacement.image then
- Graphics.sprites[objKey][id].img = Graphics.loadImageResolved(replacement.image.path)
- end
- -- Apply txt configs
- if replacement.txt then
- local props = iniparse.parseTxt(replacement.txt.path)
- local classConfig = objClass.config
- local config = classConfig[id]
- -- Use any existing config set functions
- if setConfigFunc then
- if showDialog then
- Misc.dialog("APPLYING CONFIG:", objKey, id, replacement.txt.path,"",props)
- end
- props.id = id
- setConfigFunc(props)
- -- Do it manually
- elseif config then
- for k2,v2 in pairs(props) do
- k2 = k2:lower()
- if (
- classConfig.propertiesMap
- and classConfig.propertiesMap[k2]
- )
- or config[k2] ~= nil
- or (
- manualWhitelist[objKey] ~= nil
- and (
- manualWhitelist[objKey].all[k2] ~= nil
- or (
- manualWhitelist[objKey][id] ~= nil
- and manualWhitelist[objKey][id][k2] ~= nil
- )
- )
- )
- then
- config[k2] = v2
- -- Debug fallback
- elseif cgfxBridge.debug and (debugExclude[objKey].all[k2] == nil and (debugExclude[objKey][id] == nil or debugExclude[objKey][id][k2] == nil)) then
- if failedConfigs[objKey] == nil then
- failedConfigs[objKey] = {}
- end
- local failedTbl = failedConfigs[objKey]
- failedTbl[#failedTbl+1] = tostring(id)..": "..k2.." = "..tostring(v2).." ("..replacement.txt.path..")"
- end
- end
- end
- end
- end
- end
- end
- -- Sound effects
- for k,v in ipairs(fileData.sounds) do
- local filename = v.filename
- local noExt = v.noExt
- local path = v.path
- local id = sfxIDMap[noExt]
- if id ~= nil then
- Audio.sounds[id].sfx = SFX.open(path)
- end
- end
- if cgfxBridge.debug then
- for k,v in pairs(failedConfigs) do
- Misc.dialog("Could not apply the following properties for the following "..k.." IDs:",unpack(v))
- end
- end
- end
- end
- --[[
- function cgfxBridge.onInitAPI()
- registerEvent(cgfxBridge, "onStart")
- end
- function cgfxBridge.onStart()
- -- In case anything ever needs to be done this late in the process
- end
- --]]
- return cgfxBridge
Advertisement
Add Comment
Please, Sign In to add comment