diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e5e3127 --- /dev/null +++ b/.gitignore @@ -0,0 +1,86 @@ + +# Created by https://www.toptal.com/developers/gitignore/api/lua,visualstudiocode,windows +# Edit at https://www.toptal.com/developers/gitignore?templates=lua,visualstudiocode,windows + +### Lua ### +# Compiled Lua sources +luac.out + +# luarocks build files +*.src.rock +*.zip +*.tar.gz + +# Object files +*.o +*.os +*.ko +*.obj +*.elf + +# Precompiled Headers +*.gch +*.pch + +# Libraries +*.lib +*.a +*.la +*.lo +*.def +*.exp + +# Shared objects (inc. Windows DLLs) +*.dll +*.so +*.so.* +*.dylib + +# Executables +*.exe +*.out +*.app +*.i*86 +*.x86_64 +*.hex + + +### VisualStudioCode ### +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +*.code-workspace + +### VisualStudioCode Patch ### +# Ignore all local history of files +.history + +### Windows ### +# Windows thumbnail cache files +Thumbs.db +Thumbs.db:encryptable +ehthumbs.db +ehthumbs_vista.db + +# Dump file +*.stackdump + +# Folder config file +[Dd]esktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msix +*.msm +*.msp + +# Windows shortcuts +*.lnk + +# End of https://www.toptal.com/developers/gitignore/api/lua,visualstudiocode,windows diff --git a/MWSE/mods/celediel/DoorRandomizer/common.lua b/MWSE/mods/celediel/DoorRandomizer/common.lua new file mode 100644 index 0000000..6dd2e52 --- /dev/null +++ b/MWSE/mods/celediel/DoorRandomizer/common.lua @@ -0,0 +1,15 @@ +local this = {} + +this.modName = "Door Randomizer" -- or something +this.author = "Celediel" +this.version = "0.0.1" +this.modInfo = "When a cell-change door is activated, its destination is randomized, based on various configuration options" +this.configPath = string.gsub(this.modName, "%s+", "") +this.cellTypes = { + interior = 0, + exterior = 1, + both = 2, + match = 3 +} + +return this diff --git a/MWSE/mods/celediel/DoorRandomizer/config.lua b/MWSE/mods/celediel/DoorRandomizer/config.lua new file mode 100644 index 0000000..e486c7d --- /dev/null +++ b/MWSE/mods/celediel/DoorRandomizer/config.lua @@ -0,0 +1,21 @@ +local common = require("celediel.DoorRandomizer.common") + +local defaultConfig = { + randomizeChance = 50, + interiorExterior = common.cellTypes.both, + wildernessCells = true, + needDoor = true, + debug = false, + ignoredCells = {}, +} + +local currentConfig + +local this = {} + +function this.getConfig() + currentConfig = currentConfig or mwse.loadConfig(common.configPath, defaultConfig) + return currentConfig +end + +return this diff --git a/MWSE/mods/celediel/DoorRandomizer/main.lua b/MWSE/mods/celediel/DoorRandomizer/main.lua new file mode 100644 index 0000000..e618c06 --- /dev/null +++ b/MWSE/mods/celediel/DoorRandomizer/main.lua @@ -0,0 +1,144 @@ +-- pick a destination cell at random when activating a door +local common = require("celediel.DoorRandomizer.common") +local config = require("celediel.DoorRandomizer.config").getConfig() + +local cells = {} + +-- {{{ helper functions +local function log(...) if config.debug then mwse.log("[%s] %s", common.modName, string.format(...)) end end + +local function isZero(spot) + return spot.position.x == 0 and + spot.position.y == 0 and + spot.position.z == 0 and + spot.orientation.x == 0 and + spot.orientation.y == 0 and + spot.orientation.z == 0 +end + +local function isTypeMatch(ogCell, chosenCell) + return ((ogCell.isInterior and ogCell.behavesAsExterior) or not ogCell.isInterior) == + ((chosenCell.isInterior and chosenCell.behavesAsExterior) or not chosenCell.isInterior) +end +-- }}} + +-- {{{ cell and position/orientation picking +local function pickSpot(cell) + -- this will be the position/orientation destination of every door that puts the player into the above chosen cell + local spots = {} + local spot + + -- peek through doors in that cell to pick a position/orientation for the player + for cellDoor in cell:iterateReferences(tes3.objectType.door) do + if cellDoor.destination then -- only cell change doors + -- loop through doors in THAT cell to find the door that led us there in the first place + log("Looking through door %s in %s leading to %s", cellDoor.name or cellDoor.id, cell.id, cellDoor.destination.cell.id) + for innerDoor in cellDoor.destination.cell:iterateReferences(tes3.objectType.door) do + if innerDoor.destination and innerDoor.destination.cell.id == cell.id then + -- found the door, now add where that door puts a player to our table + log("Found a door in %s leading to %s with starting position:(%s) and orientation:(%s)", + cellDoor.destination.cell.id, innerDoor.destination.cell.id, + innerDoor.destination.marker.position, innerDoor.destination.marker.orientation) + -- comment this out and set config.needDoor = true to cause infinite recursion + table.insert(spots, { + position = innerDoor.destination.marker.position, + orientation = innerDoor.destination.marker.orientation + }) + end + end + end + end + + if #spots > 0 then + log("There %s %s spot%s in %s", #spots > 1 and "were" or "was", #spots, #spots > 1 and "s" or "", cell.id) + spot = table.choice(spots) + else + -- if we don't find any then use 0,0,0, which can be really bad in some cells + spot = {position = tes3vector3.new(0, 0, 0), orientation = tes3vector3.new(0, 0, 0)} + end + + return spot +end + +local function pickCell(ogDest) + local picked = table.choice(cells) + local cell = tes3.getCell({id = picked}) + + if not config.wildernessCells and not cell.name then + log("%s is wilderness, trying again...", cell.id) + cell = pickCell(ogDest) + end + + if config.ignoredCells[cell.id] then + log("%s is ignored cell, trying again...", cell.id) + cell = pickCell(ogDest) + end + + if config.interiorExterior == common.cellTypes.exterior and cell.isInterior and not cell.behavesAsExterior then + log("%s is interior, trying again...", cell.id) + cell = pickCell(ogDest) + elseif config.interiorExterior == common.cellTypes.interior and (not cell.isInterior or cell.behavesAsExterior) then + log("%s is exterior, trying again...", cell.id) + cell = pickCell(ogDest) + elseif config.interiorExterior == common.cellTypes.match and not isTypeMatch(ogDest, cell) then + log("%s and %s aren't same cell type, trying again...", ogDest.id, cell.id) + cell = pickCell(ogDest) + end + + return cell +end + +local function pickCellAndSpot(ogDest) + local cell = pickCell(ogDest) + log("finally settled on %s, now picking position/orientation", cell.id) + + local spot = pickSpot(cell) + + if config.needDoor and isZero(spot) then + log("No good spots in %s, starting over!", cell.id) + cell, spot = pickCellAndSpot(ogDest) + end + + return cell, spot +end +-- }}} + +-- {{{ event functions +local function onActivate(e) + -- only do stuff on cell change doors + if e.activator ~= tes3.player and e.target.objectType ~= tes3.objectType.door then return end + + local door = e.target + + -- don't randomize locked doors, or: only randomize when a cell change event happens + if door.destination and not tes3.getLocked({reference = door}) then + local roll = math.random(0, 100) + local rollStr = "Randomize Roll: %s " + if config.randomizeChance < roll then + log(rollStr .. "> %s, not randomizing", roll, config.randomizeChance) + return + end + + log(rollStr .. "< %s, randomizing!", roll, config.randomizeChance) + + log("Picking initial cell...") + local cell, spot = pickCellAndSpot(door.destination.cell) + + log("Picked %s at (%s) facing (%s)", cell.id, spot.position, spot.orientation) + + -- set the door's destination to the picked cell and position/orientation + tes3.setDestination({reference = door, cell = cell, position = spot.position, orientation = spot.orientation}) + end +end + +local function onInitialized(e) + for cell, _ in pairs(tes3.dataHandler.nonDynamicData.cells) do table.insert(cells, cell) end + + log("found %s cells", #cells) + + event.register("activate", onActivate) +end +-- }}} + +event.register("initialized", onInitialized) +event.register("modConfigReady", function() mwse.mcm.register(require("celediel.DoorRandomizer.mcm")) end) diff --git a/MWSE/mods/celediel/DoorRandomizer/mcm.lua b/MWSE/mods/celediel/DoorRandomizer/mcm.lua new file mode 100644 index 0000000..cc0ce92 --- /dev/null +++ b/MWSE/mods/celediel/DoorRandomizer/mcm.lua @@ -0,0 +1,74 @@ +local common = require("celediel.DoorRandomizer.common") +local config = require("celediel.DoorRandomizer.config").getConfig() + +local function createTableVar(id) + return mwse.mcm.createTableVariable({id = id, table = config}) +end + +local template = mwse.mcm.createTemplate({name = common.modName}) +template:saveOnClose(common.configPath, config) + +local page = template:createSideBarPage({ + label = "Main Options", + description = string.format("%s v%s by %s\n\n%s\n\n", common.modName, common.version, common.author, common.modInfo) +}) + +local category = page:createCategory(common.modName) + +category:createYesNoButton({ + label = "Pick wilderness cells?", + variable = createTableVar("wildernessCells") +}) + +category:createYesNoButton({ + label = "Pick only cells that place the player at doors?", + variable = createTableVar("needDoor") +}) + +category:createDropdown({ + label = "Pick interior or exterior cells only", + options = { + {label = "Both", value = common.cellTypes.both}, + {label = "Interior Only", value = common.cellTypes.interior}, + {label = "Exterior Only", value = common.cellTypes.exterior}, + {label = "Match Original Destination", value = common.cellTypes.match} + }, + defaultSetting = common.cellTypes.both, + variable = createTableVar("interiorExterior") +}) + +category:createSlider({ + label = "Randomize Chance", + min = 0, + max = 100, + step = 1, + jump = 2, + variable = createTableVar("randomizeChance") +}) + +category:createYesNoButton({ + label = "Debug logging", + variable = createTableVar("debug") +}) + +template:createExclusionsPage({ + label = "Ignored cells", + description = "These cells will not even be considered when randomizing.", + showAllBlocked = false, + variable = createTableVar("ignoredCells"), + filters = { + { + label = "Cells", + callback = function() + local cells = {} + for cell, _ in pairs(tes3.dataHandler.nonDynamicData.cells) do + table.insert(cells, cell) + end + return cells + end + } + } +}) + +return template +