-- name: Warp Node Randomizer -- description: Swaps every warp node with another one anywhere in the game. Good luck navigating!\n\nMod By EmilyEmmi\nRNG Code by Blocky gGlobalSyncTable.twoWayWarp = true gGlobalSyncTable.excludeStarWarps = true gGlobalSyncTable.excludeDeathWarps = false gGlobalSyncTable.connectDoubleDoors = true gGlobalSyncTable.excludeBowser = false gGlobalSyncTable.excludeExitCastle = true gGlobalSyncTable.seed = 0 local DO_MANUAL_GEN = false local DO_MANUAL_DOORS = false -- Every warp node pair in the game. Key is input, value is output local defaultWarpNodeMap = { ["24 1 240"] = "6 1 52", ["14 1 240"] = "6 2 53", ["6 2 289"] = "14 1 10", ["16 1 241"] = "16 1 3", ["6 1 259"] = "5 1 10", ["6 1 4"] = "6 2 1", ["6 1 242"] = "29 1 10", ["10 2 241"] = "6 2 104", ["36 1 21"] = "36 1 22", ["27 1 241"] = "6 1 35", ["11 1 12"] = "11 1 11", ["6 3 277"] = "23 1 10", ["8 1 20"] = "8 2 10", ["15 1 11"] = "15 1 12", ["6 3 271"] = "8 1 10", ["20 1 241"] = "6 1 40", ["6 2 10"] = "31 1 10", ["16 1 2"] = "6 3 2", ["9 1 14"] = "9 1 13", ["11 2 241"] = "6 2 100", ["6 1 262"] = "24 1 10", ["10 1 12"] = "10 2 10", ["36 1 240"] = "6 2 52", ["13 2 51"] = "13 1 51", ["6 1 256"] = "9 1 10", ["8 2 240"] = "6 3 51", ["6 3 268"] = "22 1 10", ["5 2 240"] = "6 1 51", ["12 1 243"] = "12 2 10", ["36 4 240"] = "6 2 52", ["15 1 241"] = "6 2 108", ["6 3 1"] = "6 1 6", ["12 2 241"] = "6 1 103", ["22 2 241"] = "6 3 100", ["36 2 240"] = "6 2 52", ["36 1 256"] = "36 2 10", ["6 1 1"] = "16 1 1", ["6 1 3"] = "6 2 0", ["5 1 31"] = "5 1 32", ["6 2 286"] = "36 1 10", ["6 1 12"] = "20 1 10", ["13 1 13"] = "13 3 11", ["13 1 12"] = "13 3 10", ["36 4 241"] = "6 2 102", ["13 2 241"] = "6 2 101", ["18 1 240"] = "16 1 8", ["36 4 10"] = "36 1 20", ["36 3 241"] = "6 2 102", ["7 1 11"] = "28 1 10", ["9 1 13"] = "9 1 14", ["6 1 11"] = "17 1 10", ["10 1 240"] = "6 2 54", ["8 2 21"] = "8 2 22", ["8 2 22"] = "8 2 21", ["36 1 241"] = "6 2 102", ["36 1 22"] = "36 1 21", ["6 3 0"] = "6 1 5", ["22 1 13"] = "22 1 12", ["10 1 14"] = "10 1 13", ["13 2 52"] = "13 1 52", ["33 1 241"] = "19 1 12", ["6 2 241"] = "16 1 3", ["5 2 241"] = "6 1 101", ["8 2 241"] = "6 3 101", ["13 3 241"] = "6 2 105", ["23 1 241"] = "6 3 103", ["6 1 265"] = "12 1 10", ["28 1 241"] = "6 3 102", ["30 1 241"] = "17 1 12", ["5 2 20"] = "5 1 20", ["30 1 240"] = "6 1 36", ["29 1 243"] = "6 1 32", ["29 1 241"] = "6 1 35", ["6 1 2"] = "26 1 1", ["26 1 241"] = "16 1 3", ["13 1 241"] = "6 2 105", ["10 1 13"] = "10 1 14", ["28 1 243"] = "16 1 20", ["31 1 240"] = "6 2 56", ["28 1 240"] = "6 3 52", ["10 2 11"] = "10 1 11", ["13 1 50"] = "13 2 50", ["27 1 240"] = "6 1 38", ["29 1 240"] = "6 1 38", ["6 3 241"] = "16 1 3", ["19 1 11"] = "33 1 10", ["26 1 1"] = "6 1 2", ["24 1 241"] = "6 1 102", ["16 1 0"] = "6 1 0", ["24 1 11"] = "24 1 12", ["13 1 52"] = "13 2 52", ["23 2 243"] = "16 1 30", ["11 1 240"] = "6 2 50", ["23 2 241"] = "6 3 103", ["6 2 280"] = "11 1 10", ["11 1 11"] = "11 1 12", ["21 1 11"] = "34 1 10", ["10 2 240"] = "6 2 54", ["23 2 240"] = "6 3 53", ["31 1 243"] = "16 1 10", ["14 1 241"] = "6 2 103", ["22 2 240"] = "6 3 50", ["8 1 31"] = "8 1 32", ["9 1 11"] = "9 1 12", ["8 3 240"] = "6 3 51", ["15 1 12"] = "15 1 11", ["6 1 0"] = "16 1 0", ["6 2 298"] = "15 1 10", ["34 1 241"] = "21 1 12", ["22 1 12"] = "22 1 13", ["22 1 240"] = "6 3 50", ["6 2 295"] = "13 1 10", ["36 3 240"] = "6 2 52", ["9 1 241"] = "6 1 100", ["6 3 298"] = "7 1 10", ["10 1 241"] = "6 2 104", ["6 3 24"] = "19 1 10", ["5 1 241"] = "6 1 101", ["6 1 241"] = "16 1 3", ["7 1 241"] = "6 3 102", ["8 1 241"] = "6 3 101", ["18 1 241"] = "16 1 6", ["22 1 11"] = "22 2 10", ["12 1 240"] = "6 1 53", ["4 1 241"] = "26 1 11", ["6 2 11"] = "21 1 10", ["13 2 240"] = "6 2 51", ["6 3 2"] = "16 1 2", ["13 2 11"] = "13 2 12", ["20 1 240"] = "6 1 39", ["19 1 241"] = "6 3 104", ["15 1 240"] = "6 2 58", ["18 1 243"] = "16 1 7", ["21 1 241"] = "6 2 107", ["31 1 241"] = "6 2 109", ["17 1 241"] = "6 1 37", ["16 1 1"] = "6 1 1", ["6 1 6"] = "6 3 1", ["16 1 5"] = "18 1 10", ["24 1 12"] = "24 1 11", ["6 2 283"] = "13 2 10", ["6 1 5"] = "6 3 0", ["13 1 240"] = "6 2 55", ["23 1 240"] = "6 3 53", ["33 1 240"] = "6 3 54", ["13 1 51"] = "13 2 51", ["36 2 241"] = "6 2 102", ["13 3 12"] = "13 1 11", ["11 1 241"] = "6 2 100", ["7 1 240"] = "6 3 52", ["8 1 240"] = "6 3 51", ["9 1 240"] = "6 1 50", ["13 3 240"] = "6 2 55", ["6 2 0"] = "6 1 3", ["6 2 292"] = "10 1 10", ["5 1 240"] = "6 1 51", ["6 2 1"] = "6 1 4", ["27 1 243"] = "6 1 32", ["5 1 30"] = "5 2 10", ["8 1 30"] = "8 2 20", ["6 1 10"] = "27 1 10", ["8 3 241"] = "6 3 101", ["5 1 32"] = "5 1 31", ["8 1 32"] = "8 1 31", ["17 1 11"] = "30 1 10", ["13 2 50"] = "13 1 50", ["26 1 5"] = "4 1 10", ["12 2 240"] = "6 1 53", ["11 2 240"] = "6 2 50", ["9 1 12"] = "9 1 11", ["4 1 240"] = "26 1 10", ["22 1 241"] = "6 3 100", ["12 1 241"] = "6 1 103", ["6 1 999"] = "6 1 31", } -- Paired doors local warpPair = { ["6 3 1"] = 0, ["16 1 1"] = 0, ["6 1 3"] = 4, ["16 1 0"] = 1, ["6 3 0"] = 1, ["6 1 5"] = 6, ["6 1 4"] = 3, ["6 1 1"] = 0, ["6 2 1"] = 0, ["6 1 6"] = 5, ["6 1 0"] = 1, ["6 2 0"] = 1, } -- warps that require key doors local warpHasKey = { ["6 3 0"] = 2, ["6 3 1"] = 2, ["6 2 1"] = 1, ["6 2 0"] = 1, } -- A name for EVERY warp. Note that the key is the normal "output" (ie 16 1 3 is "Castle Death" because that's where Castle Death sends you) -- Some of these were editing to be more clear (such as "THI Cave Exit" instead of "THI Cave to THI Huge") local warpNames = { ["6 1 52"] = "WF Star", ["6 3 2"] = "Grounds Door to Basement", ["24 1 11"] = "WF Fading Warp 2", ["14 1 10"] = "TTC Entrance", ["6 1 36"] = "Bowser 1 Key", ["34 1 10"] = "Bowser 3 Entrance", ["24 1 10"] = "WF Entrance", ["6 1 40"] = "SA Death", ["6 1 102"] = "WF Death", ["16 1 8"] = "VCutM Star", ["5 2 10"] = "CCM Slide Entrance", ["21 1 12"] = "Bowser 3 Death", ["11 1 12"] = "WDW Fading Warp", ["6 2 58"] = "RR Star", ["5 1 20"] = "CCM Slide Exit", ["23 1 10"] = "DDD Entrance", ["15 1 11"] = "RR Fading Warp 2", ["13 1 10"] = "THI Huge Entrance", ["8 2 10"] = "SSL Pyramid Entrance", ["16 1 0"] = "Main Floor Door to Grounds 2", ["16 1 2"] = "Basement Door to Grounds", ["9 1 14"] = "BoB Fading Warp 3", ["13 3 10"] = "THI Cave Entrance", ["6 1 53"] = "JRB Star", ["6 3 51"] = "SSL Star", ["6 3 54"] = "Bowser 2 Key", ["13 2 51"] = "THI Huge Pipe to THI Tiny 2", ["26 1 10"] = "BBH Star", ["16 1 10"] = "WMotR Fall", ["6 3 101"] = "SSL Death", ["6 2 108"] = "RR Death", ["13 2 10"] = "THI Tiny Entrance", ["8 2 22"] = "SSL Pyramid Fading Warp", ["8 2 21"] = "SSL Pyramid Fading Warp 2", ["6 3 1"] = "Main Floor Door to Basement", ["6 1 101"] = "CCM Death", ["22 2 10"] = "LLL Volcano Entrance", ["6 3 102"] = "HMC Death", ["6 1 1"] = "Grounds Door to Main Floor", ["6 2 53"] = "TTC Star", ["13 1 11"] = "THI Cave Exit", ["15 1 10"] = "RR Entrance", ["6 1 3"] = "Upper Floor Door to Main Floor 2", ["12 2 10"] = "JRB Ship Entrance", ["5 1 31"] = "CCM Fading Warp 2", ["8 1 31"] = "SSL Fading Warp 2", ["9 1 11"] = "BoB Fading Warp 2", ["6 3 0"] = "Main Floor Door to Basement 2", ["10 1 14"] = "SL Fading Warp", ["13 2 52"] = "THI Huge Pipe to THI Tiny 3", ["28 1 10"] = "CotMC Entrance", ["18 1 10"] = "VCutM Entrance", ["20 1 10"] = "SA Entrance", ["10 1 10"] = "SL Entrance", ["30 1 10"] = "Bowser 1 Entrance", ["6 2 103"] = "TTC Death", ["6 3 52"] = "HMC Star", ["6 2 104"] = "SL Death", ["22 1 12"] = "LLL Fading Warp 2", ["8 2 20"] = "SSL Pyramid Upper Entrance", ["10 1 13"] = "SL Fading Warp 2", ["6 2 101"] = "THI Tiny Death", ["13 1 52"] = "THI Tiny Pipe to THI Huge 3", ["6 1 103"] = "JRB Death", ["27 1 10"] = "PSS Entrance", ["11 1 11"] = "WDW Fading Warp 2", ["10 2 10"] = "SL Igloo Entrance", ["36 1 21"] = "TTM Fading Warp 2", ["6 1 38"] = "PSS Star", ["36 2 10"] = "TTM Slide Entrance", ["6 2 102"] = "TTM Death", ["19 1 12"] = "Bowser 2 Death", ["15 1 12"] = "RR Fading Warp", ["6 1 0"] = "Grounds Door to Main Floor 2", ["36 1 20"] = "TTM Slide Exit", ["7 1 10"] = "HMC Entrance", ["6 2 52"] = "TTM Star", ["6 2 109"] = "WMotR Death", ["6 2 56"] = "WMotR Star", ["16 1 20"] = "CotMC Fall", ["6 1 32"] = "PSS Fall", ["9 1 13"] = "BoB Fading Warp 4", ["6 3 103"] = "DDD Death", ["6 2 50"] = "WDW Star", ["31 1 10"] = "WMotR Entrance", ["6 1 50"] = "BoB Star", ["16 1 1"] = "Main Floor Door to Grounds", ["26 1 1"] = "Main Floor Door to Courtyard", ["6 1 2"] = "Courtyard Door to Main Floor", ["24 1 12"] = "WF Fading Warp", ["16 1 30"] = "DDD Sub Fall", ["6 1 4"] = "Upper Floor Door to Main Floor", ["10 1 11"] = "SL Igloo Exit", ["6 3 53"] = "DDD Star", ["16 1 3"] = "Castle Death", ["6 3 100"] = "LLL Death", ["6 1 35"] = "PSS Death", ["22 1 10"] = "LLL Entrance", ["12 1 10"] = "JRB Entrance", ["26 1 11"] = "BBH Death", ["22 1 13"] = "LLL Fading Warp", ["6 1 51"] = "CCM Star", ["9 1 12"] = "BoB Fading Warp", ["6 2 107"] = "BitS Death", ["6 1 6"] = "Basement Door to Main Floor", ["6 1 39"] = "SA Star", ["6 2 105"] = "THI Huge Death", ["6 3 104"] = "BitFS Death", ["6 1 5"] = "Basement Door to Main Floor 2", ["6 2 54"] = "SL Star", ["33 1 10"] = "Bowser 2 Entrance", ["16 1 7"] = "VCutM Fall", ["13 1 51"] = "THI Tiny Pipe to THI Huge 2", ["6 1 100"] = "BoB Death", ["16 1 6"] = "VCutM Death", ["17 1 10"] = "BitDW Entrance", ["6 2 0"] = "Main Floor Door to Upper Floor 2", ["6 1 37"] = "BitDW Death", ["6 2 1"] = "Main Floor Door to Upper Floor", ["13 2 12"] = "THI Tiny Fading Warp", ["13 3 11"] = "THI Cave Upper Entrance", ["6 2 51"] = "THI Tiny Star", ["6 2 55"] = "THI Huge Star", ["13 1 50"] = "THI Tiny Pipe to THI Huge", ["9 1 10"] = "BoB Entrance", ["8 1 10"] = "SSL Entrance", ["19 1 10"] = "BitFS Entrance", ["29 1 10"] = "TotWC Entrance", ["11 1 10"] = "WDW Entrance", ["21 1 10"] = "BitS Entrance", ["8 1 32"] = "SSL Fading Warp", ["36 1 10"] = "TTM Entrance", ["13 2 50"] = "THI Huge Pipe to THI Tiny", ["17 1 12"] = "Bowser 1 Death", ["6 2 100"] = "WDW Death", ["6 3 50"] = "LLL Star", ["5 1 10"] = "CCM Entrance", ["4 1 10"] = "BBH Entrance", ["36 1 22"] = "TTM Fading Warp", ["5 1 32"] = "CCM Fading Warp", ["6 1 31"] = "Exit To Castle", } -- which side of the door is correct local manualDoorActive = true local manualDoorArg = { ["6 1 2"] = 1, ["6 1 1"] = 1, ["6 3 0"] = 1, ["16 1 1"] = 0, ["26 1 1"] = 0, ["6 2 1"] = 1, ["6 1 5"] = 0, ["6 1 0"] = 0, ["6 1 6"] = 1, ["6 1 3"] = 1, ["5 2 20"] = 1, ["6 3 2"] = 1, ["16 1 2"] = 0, ["5 1 20"] = 0, ["6 1 4"] = 0, ["6 3 1"] = 0, ["6 2 0"] = 0, ["16 1 0"] = 1, } -- constants local LEVEL_TO_COURSE = { [LEVEL_CASTLE_GROUNDS] = COURSE_NONE, -- Course 0 [LEVEL_CASTLE] = COURSE_NONE, -- Course 0 [LEVEL_CASTLE_COURTYARD] = COURSE_NONE, -- Course 0 [LEVEL_BOB] = COURSE_BOB, -- Course 1 [LEVEL_WF] = COURSE_WF, -- Course 2 [LEVEL_JRB] = COURSE_JRB, -- Course 3 [LEVEL_CCM] = COURSE_CCM, -- Course 4 [LEVEL_BBH] = COURSE_BBH, -- Course 5 [LEVEL_HMC] = COURSE_HMC, -- Course 6 [LEVEL_LLL] = COURSE_LLL, -- Course 7 [LEVEL_SSL] = COURSE_SSL, -- Course 8 [LEVEL_DDD] = COURSE_DDD, -- Course 9 [LEVEL_SL] = COURSE_SL, -- Course 10 [LEVEL_WDW] = COURSE_WDW, -- Course 11 [LEVEL_TTM] = COURSE_TTM, -- Course 12 [LEVEL_THI] = COURSE_THI, -- Course 13 [LEVEL_TTC] = COURSE_TTC, -- Course 14 [LEVEL_RR] = COURSE_RR, -- Course 15 [LEVEL_BITDW] = COURSE_BITDW, -- Course 16 [LEVEL_BOWSER_1] = COURSE_BITDW, -- Course 16 [LEVEL_BITFS] = COURSE_BITFS, -- Course 17 [LEVEL_BOWSER_2] = COURSE_BITFS, -- Course 17 [LEVEL_BITS] = COURSE_BITS, -- Course 18 [LEVEL_BOWSER_3] = COURSE_BITS, -- Course 18 [LEVEL_PSS] = COURSE_PSS, -- Course 19 [LEVEL_COTMC] = COURSE_COTMC, -- Course 20 [LEVEL_TOTWC] = COURSE_TOTWC, -- Course 21 [LEVEL_VCUTM] = COURSE_VCUTM, -- Course 22 [LEVEL_WMOTR] = COURSE_WMOTR, -- Course 23 [LEVEL_SA] = COURSE_SA, -- Course 24 [LEVEL_ENDING] = COURSE_CAKE_END, -- Course 25 (will not appear in MiniHunt) } local IS_BOWSER_STAGE = { [LEVEL_BITDW] = 1, [LEVEL_BITFS] = 1, [LEVEL_BITS] = 1, [LEVEL_BOWSER_1] = 1, [LEVEL_BOWSER_2] = 1, [LEVEL_BOWSER_3] = 1, [LEVEL_ENDING] = 1, } local AREA_EXTRA = { [LEVEL_CCM] = { [2] = "Slide", }, [LEVEL_JRB] = { [2] = "Ship", }, [LEVEL_LLL] = { [2] = "Volcano", }, [LEVEL_SSL] = { [2] = "Pyramid", [3] = "Eyerok", }, [LEVEL_DDD] = { [2] = "Sub", }, [LEVEL_SL] = { [2] = "Igloo", }, [LEVEL_TTM] = { [2] = "Slide", [3] = "Slide", [4] = "Slide", }, [LEVEL_WDW] = { [2] = "Town", }, [LEVEL_THI] = { [1] = "Huge", [2] = "Tiny", [3] = "Cave", }, } local INVALID_WARP_ENTRY = { id_bhvDeathWarp, id_bhvLaunchDeathWarp, id_bhvAirborneDeathWarp, id_bhvPaintingDeathWarp, id_bhvExitPodiumWarp, id_bhvHardAirKnockBackWarp, id_bhvAirborneWarp, id_bhvSpinAirborneWarp, id_bhvLaunchDeathWarp, id_bhvLaunchStarCollectWarp, id_bhvAirborneStarCollectWarp, id_bhvStar, } local warpNodeRandomInverse = {} local pairedOutput = {} local doneWarpNames = {} local knownWarps = {} local doDiscover = "" local isRomHack = false local gameModeLoaded = false for i,mod in pairs(gActiveMods) do if mod.category then if mod.category == "romhack" then isRomHack = true elseif mod.category == "gamemode" then gameModeLoaded = true end elseif mod.incompatible then if mod.incompatible:find("romhack") then isRomHack = true elseif mod.incompatible:find("gamemode") then gameModeLoaded = true end end if isRomHack and gameModeLoaded then break end end function do_warp_node_randomization() warpNodeRandomInverse = {} pairedOutput = {} local outputList = {} local twoWayList = {} local alreadyDidOutput = {} for inputString,outputString in pairs(defaultWarpNodeMap) do local inputData = split(inputString, " ") local outputData = split(outputString, " ") local inputLevel = tonumber(inputData[1]) local inputArea = tonumber(inputData[2]) local inputNode = tonumber(inputData[3]) local outputLevel = tonumber(outputData[1]) local outputArea = tonumber(outputData[2]) local outputNode = tonumber(outputData[3]) local pairNode = warpPair[outputString] local valid = (alreadyDidOutput[outputString] == nil) if not valid then local otherInputString = alreadyDidOutput[outputString] -- make sure this node is always considered first if otherInputString > inputString then alreadyDidOutput[outputString] = inputString end end if valid and not ((gGlobalSyncTable.excludeDeathWarps and inputNode == 0xF1) or (gGlobalSyncTable.excludeStarWarps and inputNode == 0xF0) or (gGlobalSyncTable.excludeBowser and (IS_BOWSER_STAGE[inputLevel] or IS_BOWSER_STAGE[outputLevel])) or (gGlobalSyncTable.excludeExitCastle and outputLevel == gLevelValues.exitCastleLevel and outputArea == gLevelValues.exitCastleArea and outputNode == gLevelValues.exitCastleWarpNode)) then alreadyDidOutput[outputString] = inputString if pairNode then local pairOutputString = outputLevel .. " " .. outputArea .. " " .. pairNode if gGlobalSyncTable.connectDoubleDoors and not pairedOutput[outputString] then if pairNode < outputNode then pairedOutput[pairOutputString] = outputString else pairedOutput[outputString] = pairOutputString end end end if not pairedOutput[outputString] then local inverseOutput = defaultWarpNodeMap[outputString] --print(inputString, outputString, inverseOutput) if gGlobalSyncTable.twoWayWarp and inverseOutput and inverseOutput == inputString then table.insert(twoWayList, outputString) else table.insert(outputList, outputString) end end end end table.sort(twoWayList) table.sort(outputList) blorandomseed(gGlobalSyncTable.seed) while #twoWayList > 1 do local origOutput = twoWayList[1] local index = blorandom(2, #twoWayList) local newOutput = defaultWarpNodeMap[twoWayList[index]] warpNodeRandomInverse[origOutput] = newOutput warpNodeRandomInverse[twoWayList[index]] = defaultWarpNodeMap[origOutput] --print(origOutput.." -> "..newOutput) --print(twoWayList[index].." -> "..defaultWarpNodeMap[origOutput]) table.remove(twoWayList, index) table.remove(twoWayList, 1) end if #outputList ~= 0 then local shuffledOutput = {} for i,outputString in ipairs(outputList) do table.insert(shuffledOutput, outputString) end shuffle(shuffledOutput) local containedIn = {} for i,origOutput in ipairs(outputList) do local origInput = alreadyDidOutput[origOutput] local inData = split(origInput, " ") local inLevel = tonumber(inData[1]) local inArea = tonumber(inData[2]) local newOutput = shuffledOutput[i] local outData = split(newOutput, " ") local outLevel = tonumber(outData[1]) local outArea = tonumber(outData[2]) local outNode = tonumber(outData[3]) local doShuffle = (outLevel == inLevel and outArea == inArea and outNode == 0x0A) or (containedIn[inLevel * 10 + inArea] == outLevel * 10 + outArea and outNode == 0x0A) or (warpHasKey[origOutput] == 1 and outLevel == LEVEL_BOWSER_2) or (warpHasKey[origOutput] == 2 and outLevel == LEVEL_BOWSER_1) local swap = i while doShuffle do --print("swapped", shuffledOutput[i]) -- swap with next (or first) to prevent level containing itself swap = swap + 1 if swap > #outputList then swap = 1 local firstOutput = outputList[1] warpNodeRandomInverse[firstOutput] = shuffledOutput[i] end shuffledOutput[swap], shuffledOutput[i] = shuffledOutput[i], shuffledOutput[swap] newOutput = shuffledOutput[i] outData = split(newOutput, " ") outLevel = tonumber(outData[1]) outArea = tonumber(outData[2]) outNode = tonumber(outData[3]) doShuffle = (outLevel == inLevel and outArea == inArea and outNode == 0x0A) or (containedIn[inLevel * 10 + inArea] == outLevel * 10 + outArea and outNode == 0x0A) or (warpHasKey[origOutput] == 1 and outLevel == LEVEL_BOWSER_2) or (warpHasKey[origOutput] == 2 and outLevel == LEVEL_BOWSER_1) end if outNode == 0x0A then -- set that this level is "contained" in another level. Chains such that if TTC is in WDW and BOB is in TTC, BOB is said to be in WDW instead containedIn[outLevel * 10 + outArea] = containedIn[inLevel * 10 + inArea] or (inLevel * 10 + inArea) --print(outLevel * 10 + outArea,"is contained in",containedIn[outLevel * 10 + outArea]) end warpNodeRandomInverse[origOutput] = newOutput --print(origOutput.." -> "..shuffledOutput[i]) end end end -- https://gist.github.com/Uradamus/10323382 function shuffle(tbl) for i = #tbl, 2, -1 do local j = blorandom(i) tbl[i], tbl[j] = tbl[j], tbl[i] end return tbl end -- Converts string into a table using a determiner (but stop splitting after a certain amount) function split(s, delimiter, limit_) local limit = limit_ or 999 local result = {} local finalmatch = "" local i = 0 for match in (s):gmatch(string.format("[^%s]+", delimiter)) do --djui_chat_message_create(match) i = i + 1 if i >= limit then finalmatch = finalmatch .. match .. delimiter else table.insert(result, match) end end if i >= limit then finalmatch = string.sub(finalmatch, 1, string.len(finalmatch) - string.len(delimiter)) table.insert(result, finalmatch) end return result end -- blocky's randomization functions to ensure compatibility with all OSes local blorandom_state = 0 function blorandomseed(seed) blorandom_state = (seed or 0) & 0xFFFFFFFF end function blorandom(min, max) blorandom_state = (blorandom_state * 1664525 + 1013904223) & 0xFFFFFFFF local rand_value = ((blorandom_state >> 16) & 0x7FFF) / 0x7FFF if min and max then return math.floor(rand_value * (max - min + 1)) + min elseif min then return math.floor(rand_value * min) + 1 else return rand_value end end function get_warp_name(inLevel, inArea, inNode, outLevel, outArea, outNode, o) local inAbr = get_custom_abr(LEVEL_TO_COURSE[inLevel] or 0, inLevel, inArea) local outAbr = get_custom_abr(LEVEL_TO_COURSE[outLevel] or 0, outLevel, outArea) if outLevel == gLevelValues.exitCastleLevel and outArea == gLevelValues.exitCastleArea and outNode == gLevelValues.exitCastleWarpNode then return "Exit To Castle" elseif outNode == 0x0A and (isRomHack or outLevel ~= LEVEL_CASTLE_GROUNDS) then return outAbr .. " Entrance" elseif inNode == 0xF0 then if inLevel == LEVEL_BOWSER_1 or inLevel == LEVEL_BOWSER_2 or inLevel == LEVEL_BOWSER_3 then return inAbr .. " Key" else return inAbr .. " Star" end elseif inNode == 0xF1 then return inAbr .. " Death" elseif inNode == 0xF3 then return inAbr .. " Fall" elseif obj_has_behavior_id(o, id_bhvExitPodiumWarp) ~= 0 then return inAbr .. " Exit" else local objType = "" if obj_has_behavior_id(o, id_bhvFadingWarp) ~= 0 then objType = "Fading Warp" elseif obj_has_behavior_id(o, id_bhvWarpPipe) ~= 0 then objType = "Pipe" elseif obj_has_behavior_id(o, id_bhvDoorWarp) ~= 0 then objType = "Door" elseif inAbr == outAbr then objType = "Warp" else return inAbr .. " to " .. outAbr end if inAbr ~= outAbr then return inAbr .. " " .. objType .. " to " .. outAbr else return inAbr .. " " .. objType end end end function create_new_warp_name(inLevel, inArea, inNode, outLevel, outArea, outNode, o) local warpString = outLevel .. " " .. outArea .. " " .. outNode if not warpNames[warpString] then local name = get_warp_name(inLevel, inArea, inNode, outLevel, outArea, outNode, o) if doneWarpNames[name] then doneWarpNames[name] = doneWarpNames[name] + 1 warpNames[warpString] = name .. " " .. doneWarpNames[name] else warpNames[warpString] = name doneWarpNames[name] = 1 end end end function get_custom_abr(course, level, area) if level == LEVEL_BOWSER_1 then return "Bowser 1" elseif level == LEVEL_BOWSER_2 then return "Bowser 2" elseif level == LEVEL_BOWSER_3 then return "Bowser 3" elseif level == LEVEL_ENDING then return "Ending" else local name = get_level_name(course, level, area) or "???" if name:sub(1, 7) == "Castle " then return name:sub(8) end local abr = "" local words = split(name, " -") for i,word in ipairs(words) do local upperWord = word:upper() local letter = upperWord:sub(1, 1) if upperWord == "OMB" or upperWord == "IN" or upperWord == "THE" or upperWord == "OVER" or upperWord == "OF" or upperWord == "UNDER" then letter = letter:lower() if abr == "" then letter = "" end end abr = abr .. letter end if (not isRomHack) and AREA_EXTRA[level] and AREA_EXTRA[level][area] then abr = abr .. " " .. AREA_EXTRA[level][area] elseif area ~= 1 then abr = abr .. " Area " .. area end return abr end end local discoverWarpString = "" function before_warp(level, area, node, arg) local outputString = level .. " " .. area .. " " .. node if pairedOutput[outputString] then outputString = pairedOutput[outputString] end if warpNodeRandomInverse[outputString] then local newWarpAsString = warpNodeRandomInverse[outputString] local data = split(newWarpAsString, " ") discoverWarpString = outputString if tonumber(data[1]) == LEVEL_ENDING and level_is_vanilla_level(LEVEL_ENDING) then if not knownWarps[discoverWarpString] then network_send_include_self(true, { id = PACKET_WARP_DISCOVER, from = network_global_index_from_local(0), warpString = discoverWarpString, }) end discoverWarpString = "" end return {destLevel = tonumber(data[1]), destArea = tonumber(data[2]), destWarpNode = tonumber(data[3]), arg = arg} end end hook_event(HOOK_BEFORE_WARP, before_warp) local lastWarpString = "" function on_warp(type, level, area, node, arg) lastWarpString = level .. " " .. area .. " " .. node -- the reason this is done here instead of in before_warp is to create tension. -- for paintings, before_warp runs the second you enter, so doing it here delays it until the level appears. if discoverWarpString ~= "" then if not knownWarps[discoverWarpString] then network_send_include_self(true, { id = PACKET_WARP_DISCOVER, from = network_global_index_from_local(0), warpString = discoverWarpString, }) end discoverWarpString = "" end end hook_event(HOOK_ON_WARP, on_warp) local genLevel = -1 local genArea = 1 local currDoorArg = -1 local curWarpNode = 0 function generate_warp_node_list(msg) genLevel = 4 while isRomHack and level_is_vanilla_level(genLevel) and genLevel < 40 do genLevel = genLevel + 1 end if genLevel > 40 then genLevel = -1 return -- cancel the whole thing end defaultWarpNodeMap = {} warpNames = {} warpPair = {} doneWarpNames = {} if DO_MANUAL_DOORS or isRomHack then manualDoorArg = {} manualDoorActive = false end genArea = 1 curWarpNode = 0 currDoorArg = -1 return true end if DO_MANUAL_GEN then hook_chat_command("genwarps", "- Generate warp node list", generate_warp_node_list) end function check_command(msg) while msg and msg:sub(1, 1) == " " do msg = msg:sub(2) end if msg == nil or msg == "" then djui_chat_message_create("\\#ff5050\\Where are you checking?") return true end local searchName = msg:lower() -- some other common names --searchName = searchName:gsub("exit", "death") searchName = searchName:gsub("fade", "fading") local name local warpString for checkString,checkName in pairs(warpNames) do if checkName:lower() == searchName or checkName:lower() == searchName .. " entrance" then -- if perfect match, use that name = checkName warpString = checkString break elseif warpString == nil and checkName:lower():find(searchName) then -- otherwise, get match with find command name = checkName warpString = checkString end end if not warpString then djui_chat_message_create("\\#ff5050\\Couldn't find warp. Make sure you entered the name correctly.") else if pairedOutput[warpString] then warpString = pairedOutput[warpString] end if warpNodeRandomInverse[warpString] then if knownWarps[warpString] then local warpString2 = warpNodeRandomInverse[warpString] if defaultWarpNodeMap[warpString2] then warpString2 = defaultWarpNodeMap[warpString2] end local name2 = warpNames[warpString2] or "UNKNOWN" djui_chat_message_create("\\#ffff50\\"..name.." -> "..name2) else djui_chat_message_create("\\#ff5050\\"..name.." has not been checked yet.") if network_is_server() then doDiscover = warpString djui_chat_message_create("If you want to spoil this location, use /discover now.") end end else djui_chat_message_create("\\#ffff50\\"..name.." acts as normal.") end end return true end hook_chat_command("check","[NAME] - Find out where this warp leads.",check_command) function discover_command(msg) if msg and msg:lower() == "all" then for origOutput, newOutput in pairs(warpNodeRandomInverse) do if not knownWarps[origOutput] then knownWarps[origOutput] = 1 end end knownWarps.id = PACKET_WARP_LIST_REQUEST network_send(true, knownWarps) djui_chat_message_create("\\#ffff50\\All locations have been revealed!") return true end if doDiscover == nil or doDiscover == "" then djui_chat_message_create("\\#ff5050\\Use /check or /find first.") return true elseif not warpNodeRandomInverse[doDiscover] then djui_chat_message_create("\\#ff5050\\This warp acts as normal!") return true elseif knownWarps[doDiscover] then djui_chat_message_create("\\#ff5050\\Already discovered this warp!") return true end network_send_include_self(true, { id = PACKET_WARP_DISCOVER, from = 0, warpString = doDiscover, cheat = true, }) doDiscover = "" return true end if network_is_server() then hook_chat_command("discover","[ALL?] (Host only) - After using /check or /find, use this command to spoil the location.",discover_command) end ---@param msg string function find_command(msg) while msg and msg:sub(1, 1) == " " do msg = msg:sub(2) end if msg == nil or msg == "" then djui_chat_message_create("\\#ff5050\\What are you trying to find?") return true end local searchName = msg:lower() -- some other common names --searchName = searchName:gsub("exit", "death") searchName = searchName:gsub("fade", "fading") local name local warpString for checkString,checkName in pairs(warpNames) do if checkName:lower() == searchName or checkName:lower() == searchName .. " entrance" then name = checkName warpString = checkString break elseif warpString == nil and checkName:lower():find(searchName) then -- otherwise, get match with find command name = checkName warpString = checkString end end if not warpString then djui_chat_message_create("\\#ff5050\\Couldn't find warp. Make sure you entered the name correctly.") else if defaultWarpNodeMap[warpString] then warpString = defaultWarpNodeMap[warpString] end --if pairedOutput[warpString] then warpString = pairedOutput[warpString] end for orig,pair in pairs(pairedOutput) do if pair == warpString then warpString = orig print(orig,"is paired with",pair) break end end if not warpNodeRandomInverse[warpString] then djui_chat_message_create("\\#ffff50\\"..name.." acts as normal.") return true end local correctOutput for origOutput,newOutput in pairs(warpNodeRandomInverse) do --print(origOutput.."->"..newOutput) if newOutput == warpString then correctOutput = origOutput break end end if correctOutput then if knownWarps[correctOutput] then local name2 = warpNames[correctOutput] or "UNKNOWN" djui_chat_message_create("\\#ffff50\\"..name2.." -> "..name) else djui_chat_message_create("\\#ff5050\\"..name.." has not been found yet.") if network_is_server() then doDiscover = correctOutput djui_chat_message_create("If you want to spoil this location, use /discover now.") end end else djui_chat_message_create("\\#ff5050\\Couldn't find an entrance here!") end end return true end hook_chat_command("find","[NAME] - Find out how to get to this warp.",find_command) local showWarpProgress = false local warpY = 0 local warpProgressSwitchName = false function warp_progress_command(msg) showWarpProgress = not showWarpProgress return true end hook_chat_command("warps", "- View all known warp locations.", warp_progress_command) ---@param m MarioState function mario_update(m) -- prevent instantly using the warp again local np = gNetworkPlayers[m.playerIndex] if m.action == ACT_TELEPORT_FADE_IN then local o = obj_get_nearest_object_with_behavior_id(m.marioObj, id_bhvFadingWarp) if o and o.oInteractionSubtype == INT_SUBTYPE_FADING_WARP then interact_warp(m, INTERACT_WARP, o) end elseif m.action == ACT_WARP_DOOR_SPAWN and m.usedObj then if manualDoorActive then local usingArg = (m.usedObj.oFaceAngleYaw == m.spawnInfo.startAngle.y and 1) or 0 --print(lastWarpString, usingArg, manualDoorArg[lastWarpString]) if m.playerIndex == 0 and manualDoorArg[lastWarpString] and usingArg ~= manualDoorArg[lastWarpString] then if usingArg == 1 then m.spawnInfo.startAngle.y = m.usedObj.oFaceAngleYaw + 0x8000 else m.spawnInfo.startAngle.y = m.usedObj.oFaceAngleYaw end m.spawnInfo.startPos.x = m.usedObj.oPosX + 300 * sins(m.spawnInfo.startAngle.y) m.spawnInfo.startPos.y = m.usedObj.oPosY m.spawnInfo.startPos.z = m.usedObj.oPosZ + 300 * coss(m.spawnInfo.startAngle.y) m.pos.x, m.pos.y, m.pos.z = m.spawnInfo.startPos.x, m.spawnInfo.startPos.y, m.spawnInfo.startPos.z m.faceAngle.y = m.spawnInfo.startPos.y end else local checkX = m.pos.x - 1000 * sins(m.faceAngle.y) local checkY = m.pos.y + 500 local checkZ = m.pos.z - 1000 * coss(m.faceAngle.y) local floor = collision_find_floor(checkX, checkY, checkZ) if (m.floor == nil or m.floor.type == SURFACE_VERTICAL_WIND or m.floor.type == SURFACE_DEATH_PLANE) or (floor and floor.type ~= SURFACE_VERTICAL_WIND and floor.type ~= SURFACE_DEATH_PLANE) then m.faceAngle.y = m.faceAngle.y + 0x8000 m.pos.x = m.usedObj.oPosX + 300 * sins(m.faceAngle.y) m.pos.y = m.usedObj.oPosY m.pos.z = m.usedObj.oPosZ + 300 * coss(m.faceAngle.y) --set_mario_action(m, ACT_IDLE, 0) end end if m.playerIndex == 0 then local o = obj_get_first_with_behavior_id(id_bhvCcmTouchedStarSpawn) if o and dist_between_objects(o, m.marioObj) < 500 then local starPos = gLevelValues.starPositions.CcmSlideStarPos spawn_default_star(starPos.x, starPos.y, starPos.z) end end end if (not gameModeLoaded) then local discovered = gPlayerSyncTable[m.playerIndex].points or 0 network_player_set_description(np, "Found: "..discovered, 255, 255, 255, 255) end if m.playerIndex ~= 0 or genLevel == -1 then return end -- generation of warp node list while genLevel ~= -1 do local doNext = false if np.currLevelNum ~= genLevel or np.currAreaIndex ~= genArea then doNext = true if smlua_collision_util_get_level_collision(genLevel, genArea) then doNext = false if not (warp_to_level(genLevel, genArea, 6) or warp_to_warpnode(genLevel, genArea, 6, 0xF1)) then doNext = true else break end end else local doBreak = false local didInput = {} while curWarpNode <= 255 do local warpNode = area_get_warp_node(curWarpNode) if warpNode then local destLevel = warpNode.node.destLevel if destLevel > 100 then -- some warps do weird crap like this destLevel = genLevel end local inputString = genLevel .. " " .. genArea .. " " .. curWarpNode local outputString = destLevel .. " " .. warpNode.node.destArea .. " " .. warpNode.node.destNode if inputString ~= outputString and destLevel ~= 0 and didInput[inputString] == nil then local valid = true local o = warpNode.object if o and obj_has_behavior_id(o, id_bhvDoorWarp) ~= 0 then if DO_MANUAL_DOORS then if currDoorArg ~= -1 and m.controller.buttonPressed & L_TRIG ~= 0 then manualDoorArg[outputString] = currDoorArg currDoorArg = -1 elseif currDoorArg ~= -1 and m.controller.buttonPressed & R_TRIG ~= 0 then manualDoorArg[outputString] = 1 - currDoorArg currDoorArg = -1 else m.pos.x = o.oPosX + 300 * sins(o.oFaceAngleYaw) m.pos.y = o.oPosY m.pos.z = o.oPosZ + 300 * coss(o.oFaceAngleYaw) if currDoorArg == -1 then currDoorArg = 0 djui_chat_message_create("Press L if this direction is correct and R if not") end doBreak = true break end end if (not isRomHack) and inputString == "5 1 20" then -- exclude ccm door since you can't enter it valid = false else local warpDoorId = o.oBehParams >> 24 if warpDoorId ~= 0 then warpHasKey[outputString] = warpDoorId end local pair = nil local closestDist = 500 local checkDoor = obj_get_first_with_behavior_id(id_bhvDoorWarp) while checkDoor do if o ~= checkDoor and closestDist > dist_between_objects(o, checkDoor) then pair = checkDoor end checkDoor = obj_get_next_with_same_behavior_id(checkDoor) end if pair then local warpNode2 = area_get_warp_node_from_params(pair) local destLevel2 = warpNode2.node.destLevel if destLevel2 > 100 then -- some warps do weird crap like this destLevel2 = genLevel end if warpNode2 and destLevel2 == destLevel and warpNode2.node.destArea == warpNode.node.destArea then local inputString2 = genLevel .. " " .. genArea .. " " .. warpNode2.node.id local outputString2 = destLevel2 .. " " .. warpNode2.node.destArea .. " " .. warpNode2.node.destNode if manualDoorArg[outputString] then manualDoorArg[outputString2] = 1 - manualDoorArg[outputString] end local warpDoorId2 = pair.oBehParams >> 24 if warpDoorId2 ~= 0 then warpHasKey[outputString2] = warpDoorId2 end warpPair[outputString] = warpNode2.node.destNode warpPair[outputString2] = warpNode.node.destNode didInput[inputString2] = 1 defaultWarpNodeMap[inputString2] = outputString2 print(inputString2.." -> "..outputString2) print("Pair with:") create_new_warp_name(genLevel, genArea, warpNode2.node.id, destLevel2, warpNode2.node.destArea, warpNode2.node.destNode, pair) end end end elseif o and obj_has_behavior_id(o, id_bhvFadingWarp) ~= 0 then local floorHeight = find_floor_height(o.oPosX, o.oPosY, o.oPosZ) -- don't accept as input if the fading warp is not on the ground (except ssl pyramid because it's quirky like that) if o.oPosY - floorHeight > 161 and (isRomHack or outputString ~= "8 2 21") then valid = false end elseif o == nil and curWarpNode ~= 0xF0 and curWarpNode ~= 0xF1 and curWarpNode ~= 0xF3 and (destLevel ~= LEVEL_ENDING or not level_is_vanilla_level(LEVEL_ENDING)) then valid = false elseif o and isRomHack then if genLevel == destLevel and genArea == warpNode.node.destArea then local other = area_get_warp_node(warpNode.node.destNode) if other == nil or other.object == nil then valid = false end else for i,id in ipairs(INVALID_WARP_ENTRY) do if obj_has_behavior_id(o, id) ~= 0 then valid = false break end end end end if valid then didInput[inputString] = 1 print(inputString.." -> "..outputString, o and get_behavior_name_from_id(get_id_from_behavior(o.behavior))) defaultWarpNodeMap[inputString] = outputString create_new_warp_name(genLevel, genArea, curWarpNode, destLevel, warpNode.node.destArea, warpNode.node.destNode, o) end end end curWarpNode = curWarpNode + 1 end if doBreak then break end -- paintings local didOutput = {} if m.area.paintingWarpNodes and m.floor then local oldFloorType = m.floor.type local oldY = m.pos.y for newFloorType = PAINTING_WARP_INDEX_START, PAINTING_WARP_INDEX_END - 1 do m.floor.type = newFloorType + SURFACE_PAINTING_WARP_D3 m.pos.y = m.floorHeight local paintingWarpNode = get_painting_warp_node() if paintingWarpNode then local destLevel = paintingWarpNode.destLevel if destLevel > 100 then destLevel = genLevel end local inputString = genLevel .. " " .. genArea .. " " .. (newFloorType + 256) local outputString = destLevel .. " " .. paintingWarpNode.destArea .. " " .. paintingWarpNode.destNode if didOutput[outputString] == nil and destLevel ~= 0 and inputString ~= outputString then didOutput[outputString] = 1 print(inputString.." -> "..outputString) defaultWarpNodeMap[inputString] = outputString create_new_warp_name(genLevel, genArea, (newFloorType + 256), destLevel, paintingWarpNode.destArea, paintingWarpNode.destNode) end end end m.floor.type = oldFloorType m.pos.y = oldY end doNext = true end if doNext then curWarpNode = 0 genArea = genArea + 1 if genArea > 8 then genArea = 1 genLevel = genLevel + 1 while isRomHack and level_is_vanilla_level(genLevel) and genLevel < 40 do genLevel = genLevel + 1 end if genLevel >= 40 then genLevel = -1 end end end end if genLevel == -1 then -- exit to castle local inputString = gLevelValues.exitCastleLevel .. " " .. gLevelValues.exitCastleArea local outputString = inputString .. " " .. gLevelValues.exitCastleWarpNode inputString = inputString .. " 999" if inputString ~= outputString then print(inputString.." -> "..outputString) defaultWarpNodeMap[inputString] = outputString create_new_warp_name(gLevelValues.exitCastleLevel, gLevelValues.exitCastleArea, 999, gLevelValues.exitCastleLevel, gLevelValues.exitCastleArea, gLevelValues.exitCastleWarpNode) end warp_to_start_level() do_warp_node_randomization() if DO_MANUAL_GEN then local maxPerLine = 5 local inLine = 0 manualDoorActive = true print("local defaultWarpNodeMap = {") local text = "" for inputString,outputString in pairs(defaultWarpNodeMap) do text = text .. "[\""..inputString.."\"] = \"" .. outputString .."\", " inLine = inLine + 1 if inLine >= maxPerLine then print(" "..text) text = "" inLine = 0 end end if inLine ~= 0 then print(" "..text) end print("}") print("local warpPair = {") text = "" inLine = 0 for warpString,node in pairs(warpPair) do text = text .. "[\""..warpString.."\"] = " .. node ..", " inLine = inLine + 1 if inLine >= maxPerLine then print(" "..text) text = "" inLine = 0 end end if inLine ~= 0 then print(" "..text) end print("}") print("local warpHasKey = {") text = "" inLine = 0 for warpString,key in pairs(warpHasKey) do text = text .. "[\""..warpString.."\"] = " .. key ..", " inLine = inLine + 1 if inLine >= maxPerLine then print(" "..text) text = "" inLine = 0 end end if inLine ~= 0 then print(" "..text) end print("}") print("local warpNames = {") text = "" inLine = 0 for warpString,name in pairs(warpNames) do text = text .. "[\""..warpString.."\"] = \"" .. name .."\", " inLine = inLine + 1 if inLine >= maxPerLine then print(" "..text) text = "" inLine = 0 end end if inLine ~= 0 then print(" "..text) end print("}") if DO_MANUAL_DOORS then print("local manualDoorArg = {") text = "" inLine = 0 for outputString,arg in pairs(manualDoorArg) do text = text .. "[\""..outputString.."\"] = " .. arg ..", " inLine = inLine + 1 if inLine >= maxPerLine then print(" "..text) text = "" inLine = 0 end end if inLine ~= 0 then print(" "..text) end print("}") end end end end hook_event(HOOK_MARIO_UPDATE, mario_update) -- menu thing for checking all warps function on_hud_render() if genLevel ~= -1 then djui_hud_set_resolution(RESOLUTION_N64) djui_hud_set_color(0, 0, 0, 255) local screenWidth = djui_hud_get_screen_width() local screenHeight = djui_hud_get_screen_height() djui_hud_render_rect(0, 0, screenWidth + 10, screenHeight + 10) djui_hud_set_color(255, 255, 255, 255) local text = "Generating warp list..." local scale = 1 local width = djui_hud_measure_text(text) * scale local x = screenWidth / 2 local y = screenHeight / 2 djui_hud_print_text(text, x - width / 2, y - 16 * scale, scale) return end if not showWarpProgress then return end local m = gMarioStates[0] m.freeze = math.max(m.freeze, 1) local newWarpY = warpY if m.controller.buttonPressed & (START_BUTTON | A_BUTTON) ~= 0 then showWarpProgress = false elseif m.controller.buttonPressed & (B_BUTTON) ~= 0 then warpProgressSwitchName = not warpProgressSwitchName play_sound(SOUND_GENERAL2_PURPLE_SWITCH, gGlobalSoundSource) end if m.controller.buttonDown & D_JPAD ~= 0 or m.controller.stickY <= -32 then newWarpY = newWarpY + 4 end if m.controller.buttonDown & U_JPAD ~= 0 or m.controller.stickY >= 32 then newWarpY = newWarpY - 4 end if djui_hud_get_mouse_scroll_y() ~= 0 then newWarpY = newWarpY - djui_hud_get_mouse_scroll_y() * 4 end djui_hud_set_resolution(RESOLUTION_N64) djui_hud_set_color(0, 0, 0, 155) local screenWidth = djui_hud_get_screen_width() local screenHeight = djui_hud_get_screen_height() djui_hud_render_rect(0, 0, screenWidth + 10, screenHeight + 10) djui_hud_set_color(255, 255, 255, 255) local scale = 0.25 local y = 20 * scale local lines = {} local lineData = {} for origOutput, newOutput in pairs(warpNodeRandomInverse) do if defaultWarpNodeMap[newOutput] then newOutput = defaultWarpNodeMap[newOutput] end local name = warpNames[origOutput] or "UNKNOWN" local name2 = warpNames[newOutput] or "UNKNOWN" local arrow = " -> " if warpProgressSwitchName then name, name2 = name2, name arrow = " <- " end if not knownWarps[origOutput] then name = "\\#ff5050\\"..name table.insert(lines, name..arrow.."???") else if gGlobalSyncTable.twoWayWarp and defaultWarpNodeMap[defaultWarpNodeMap[origOutput]] == origOutput then arrow = " <-> " end name = "\\#50ff50\\"..name table.insert(lines, name..arrow..name2) end lineData[lines[#lines]] = name end table.sort(lines, function(a, b) local nameA = lineData[a] local nameB = lineData[b] return nameA < nameB end) table.insert(lines, 1, "") table.insert(lines, 1, "Press B to switch entrance and exit") local yOffset = warpY local maxWarpY = 0 for i,line in ipairs(lines) do local renderX = 40 * scale if i % 2 == 0 then renderX = screenWidth - djui_hud_measure_text(remove_color(line)) * scale - renderX * 2 end djui_hud_print_text_with_color(line, renderX, y - yOffset, scale) if i % 2 == 0 then y = y + 40 * scale end end maxWarpY = y + 40 * scale - screenHeight if maxWarpY < 0 then maxWarpY = 0 end if newWarpY < 0 then newWarpY = 0 elseif newWarpY > maxWarpY then newWarpY = maxWarpY end y = 20 * scale warpY = newWarpY -- scroll bar if maxWarpY ~= 0 then djui_hud_set_color(0, 0, 0, 155) djui_hud_render_rect(screenWidth - 12, 5, 8, screenHeight - 10) djui_hud_set_color(100, 100, 100, 255) local portion = screenHeight / (maxWarpY + screenHeight) local bottomScrollY = (screenHeight - 12) local barSize = portion * bottomScrollY local scrollBarY = (warpY / maxWarpY) * (bottomScrollY - barSize) + 6 djui_hud_render_rect(screenWidth - 11, scrollBarY, 6, barSize) end end hook_event(HOOK_ON_HUD_RENDER, on_hud_render) -- removes color string function remove_color(text, get_color) local start = text:find("\\") local next = 1 while (next) and (start) do start = text:find("\\") if start then next = text:find("\\", start + 1) if not next then next = text:len() + 1 end if get_color then local color = text:sub(start, next) local render = text:sub(1, start - 1) text = text:sub(next + 1) return text, color, render else text = text:sub(1, start - 1) .. text:sub(next + 1) end end end return text end -- stops color text at the limit selected function cap_color_text(text, limit) local slash = false local capped_text = "" local chars = 0 local luaPoint = 0 while luaPoint < text:len() do luaPoint = luaPoint + 1 local char = text:sub(luaPoint, luaPoint) -- special characters are treated as multiple by lua: not doing this WILL cause game crashes! if string.byte(char) >= 128 then local foundEndChar = true while string.byte(char, char:len()) >= 128 do if luaPoint >= text:len() or char:len() >= 3 then -- 3 is the max, because the japanese characters are 3 lua characters long foundEndChar = false break end luaPoint = luaPoint + 1 char = char .. text:sub(luaPoint, luaPoint) end if foundEndChar then luaPoint = luaPoint - 1 char = char:sub(1, -2) end end if char == "\\" then slash = not slash elseif not slash then chars = chars + 1 if chars > limit then break end end capped_text = capped_text .. char end return capped_text end -- converts hex string to RGB values function convert_color(text) if text:sub(2, 2) ~= "#" then return nil end text = text:sub(3, -2) local rstring, gstring, bstring = "", "", "" if text:len() ~= 3 and text:len() ~= 6 then return 255, 255, 255, 255 end if text:len() == 6 then rstring = text:sub(1, 2) or "ff" gstring = text:sub(3, 4) or "ff" bstring = text:sub(5, 6) or "ff" else rstring = text:sub(1, 1) .. text:sub(1, 1) gstring = text:sub(2, 2) .. text:sub(2, 2) bstring = text:sub(3, 3) .. text:sub(3, 3) end local r = tonumber("0x" .. rstring) or 255 local g = tonumber("0x" .. gstring) or 255 local b = tonumber("0x" .. bstring) or 255 return r, g, b, 255 -- alpha is no longer writeable end -- prints text on the screen... with color! function djui_hud_print_text_with_color(text, x, y, scale, alpha) djui_hud_set_color(255, 255, 255, alpha or 255) local space = 0 local color = "" local render = "" text, color, render = remove_color(text, true) while render do local r, g, b, a = convert_color(color) djui_hud_print_text(render, x + space, y, scale); if r then djui_hud_set_color(r, g, b, alpha or a) end space = space + djui_hud_measure_text(render) * scale text, color, render = remove_color(text, true) end djui_hud_print_text(text, x + space, y, scale); end -- handle warp discovery function on_joined_game() gPlayerSyncTable[0].points = 0 if network_is_server() then return end network_send_to(1, true, { id = PACKET_WARP_LIST_REQUEST, from = network_global_index_from_local(0), }) end hook_event(HOOK_JOINED_GAME, on_joined_game) function on_mods_loaded() if isRomHack then generate_warp_node_list() end end hook_event(HOOK_ON_MODS_LOADED, on_mods_loaded) function on_packet_warp_discover(data, self) local np = network_player_from_global_index(data.from) if not np then return end local playerColor = network_get_player_text_color_string(np.localIndex) local pName = playerColor if np.localIndex == 0 then pName = pName .. "You" else pName = pName .. np.name end local startText = pName .. "\\#ffff50\\ discovered:\n" local warpString = data.warpString knownWarps[warpString] = 1 if pairedOutput[warpString] then warpString = pairedOutput[warpString] knownWarps[warpString] = 1 end local name = warpNames[warpString] or "UNKNOWN" if warpNodeRandomInverse[warpString] then local warpString2 = warpNodeRandomInverse[warpString] local arrow = " -> " if defaultWarpNodeMap[warpString2] then warpString2 = defaultWarpNodeMap[warpString2] if gGlobalSyncTable.twoWayWarp and defaultWarpNodeMap[defaultWarpNodeMap[warpString]] == warpString then knownWarps[warpString2] = 1 arrow = " <-> " if pairedOutput[warpString2] then knownWarps[warpString2] = 1 end end end local name2 = warpNames[warpString2] or "UNKNOWN" djui_chat_message_create(startText..name..arrow..name2) else djui_chat_message_create(startText..name.." acts as normal.") end if np.localIndex == 0 and not data.cheat then local sMario = gPlayerSyncTable[0] if sMario.points == nil then sMario.points = 0 end sMario.points = sMario.points + 1 end end function on_packet_warp_list_erase() knownWarps = {} end function on_packet_warp_list_request(data, self) if self then return end if network_is_server() then local index = network_local_index_from_global(data.from) knownWarps.id = PACKET_WARP_LIST_REQUEST network_send_to(index, true, knownWarps) else knownWarps = data end end PACKET_WARP_DISCOVER = 0 PACKET_WARP_LIST_ERASE = 1 PACKET_WARP_LIST_REQUEST = 2 local sPacketTable = { [PACKET_WARP_DISCOVER] = on_packet_warp_discover, [PACKET_WARP_LIST_ERASE] = on_packet_warp_list_erase, [PACKET_WARP_LIST_REQUEST] = on_packet_warp_list_request, } function on_packet_receive(data) if sPacketTable[data.id] then return sPacketTable[data.id](data) end end hook_event(HOOK_ON_PACKET_RECEIVE, on_packet_receive) function network_send_include_self(reliable, data) network_send(reliable, data) if sPacketTable[data.id] then return sPacketTable[data.id](data, true) end end function on_sync_table_change(field, oldVal, newVal) if oldVal == newVal and (oldVal ~= nil or field ~= "seed") then return end do_warp_node_randomization() if network_is_server() and field == "seed" then knownWarps = {} network_send(true, { id = PACKET_WARP_LIST_ERASE, }) end end local syncFields = { "seed", "twoWayWarp", "excludeStarWarps", "excludeDeathWarps", "connectDoubleDoors", "excludeBowser", "excludeExitCastle", } for i,field in ipairs(syncFields) do hook_on_sync_table_change(gGlobalSyncTable, field, field, on_sync_table_change) end -- menu stuff function new_saveable_checkbox(name, fieldName) if mod_storage_exists(fieldName) then gGlobalSyncTable[fieldName] = mod_storage_load_bool(fieldName) end hook_mod_menu_checkbox(name, gGlobalSyncTable[fieldName], function(index, value) gGlobalSyncTable[fieldName] = value mod_storage_save_bool(fieldName, gGlobalSyncTable[fieldName]) end) end hook_mod_menu_button("List Seed And Settings", function(index) djui_chat_message_create("\\#ffff50\\Seed: "..string.format("%08x",gGlobalSyncTable.seed)) local toggleString = (gGlobalSyncTable.connectDoubleDoors and "\\#50ff50\\ON") or "\\#ff5050\\OFF" djui_chat_message_create("\\#ffff50\\Connect Double Doors: "..toggleString) toggleString = (gGlobalSyncTable.twoWayWarp and "\\#50ff50\\ON") or "\\#ff5050\\OFF" djui_chat_message_create("\\#ffff50\\Two Way Warps: "..toggleString) toggleString = (gGlobalSyncTable.excludeStarWarps and "\\#50ff50\\ON") or "\\#ff5050\\OFF" djui_chat_message_create("\\#ffff50\\Exclude Star Warps: "..toggleString) toggleString = (gGlobalSyncTable.excludeDeathWarps and "\\#50ff50\\ON") or "\\#ff5050\\OFF" djui_chat_message_create("\\#ffff50\\Exclude Death Warps: "..toggleString) toggleString = (gGlobalSyncTable.excludeBowser and "\\#50ff50\\ON") or "\\#ff5050\\OFF" djui_chat_message_create("\\#ffff50\\Exclude Bowser Stages: "..toggleString) toggleString = (gGlobalSyncTable.excludeExitCastle and "\\#50ff50\\ON") or "\\#ff5050\\OFF" djui_chat_message_create("\\#ffff50\\Exclude \"Exit To Castle\": "..toggleString) end) if network_is_server() then gGlobalSyncTable.seed = get_time() & 0xFFFFFFFF local newSeed = string.format("%08x",gGlobalSyncTable.seed) hook_mod_menu_inputbox("New Seed", newSeed, 9, function(index, value) newSeed = value end) hook_mod_menu_button("Generate New Seed", function(index) if not tonumber("0x"..newSeed) then djui_chat_message_create("\\#ff5050\\Invalid seed!") else local newNum = tonumber("0x"..newSeed) if gGlobalSyncTable.seed ~= newNum then gGlobalSyncTable.seed = newNum djui_chat_message_create("\\#ffff50\\New seed generated!") else djui_chat_message_create("\\#ff5050\\Seed already generated!") end end end) new_saveable_checkbox("Connect Double Doors", "connectDoubleDoors") new_saveable_checkbox("Two Way Warps", "twoWayWarp") new_saveable_checkbox("Exclude Star Warps", "excludeStarWarps") new_saveable_checkbox("Exclude Death Warps", "excludeDeathWarps") new_saveable_checkbox("Exclude Bowser Stages", "excludeBowser") new_saveable_checkbox("Exclude \"Exit To Castle\"", "excludeExitCastle") end