I should've done this a long time ago

This commit is contained in:
Lilian Jónsdóttir 2020-08-02 02:24:12 -07:00
parent d82d595709
commit b168775ac4
6 changed files with 1068 additions and 0 deletions

View file

@ -0,0 +1,29 @@
local inspect = require("inspect")
local this = {}
this.modName = "NPCs Go Home (At Night)"
this.author = "OEA/Celediel"
this.version = "0.0.1"
this.modInfo = "Move NPCs to their homes, or public houses (or just disable them), lock doors, " ..
"and prevent interaction after hours, selectively disable NPCs in inclement weather"
this.configPath = "NPCSGOHOME"
this.logLevels = {none = 0, small = 1, medium = 2, large = 3}
this.split = function(input, sep)
if not input then return end
if not sep then sep = "%s" end
local output = {}
for str in string.gmatch(input, "([^" .. sep .. "]+)") do table.insert(output, str) end
return output
end
this.log = function(...) mwse.log("[%s] %s", this.modName, string.format(...)) end
this.inspect = function(thing)
this.log("Inspecting a %s", thing)
this.log(inspect(thing))
end
return this

View file

@ -0,0 +1,36 @@
local common = require("celediel.NPCsGoHome.common")
-- todo: clean this up
local defaultConfig = {
disableNPCs = true,
lockDoors = true,
disableInteraction = true,
timerInterval = 7,
ignored = {
["fargoth"] = true,
["Balmora, Caius Cosades' House"] = true,
["Publican"] = true,
["Healer Service"] = true,
},
worstWeather = tes3.weather.thunder,
keepBadWeatherNPCs = true,
closeTime = 21,
openTime = 7,
minimumOccupancy = 3,
waistWorks = true,
moveNPCs = false,
homelessWanderersToPublicHouses = false,
logLevel = common.logLevels.none,
factionIgnorePercentage = 0.67
}
local currentConfig
local this = {}
function this.getConfig()
currentConfig = currentConfig or mwse.loadConfig(common.configPath, defaultConfig)
return currentConfig
end
return this

View file

@ -0,0 +1,21 @@
local this = {}
local homedNPCs = {}
this.setHomedNPCTable = function(t) homedNPCs = t end
this.getHomedNPCTable = function() return homedNPCs end
local inns = {}
this.setInnTable = function(t) inns = t end
this.getInnTable = function() return inns end
local movedNPCs = {}
this.setMovedNPCsTable = function(t) movedNPCs = t end
this.getMovedNPCsTable = function() return movedNPCs end
return this

View file

@ -0,0 +1,669 @@
-- {{{ other files
-- ? could probably split this file out to others as well
local config = require("celediel.NPCsGoHome.config").getConfig()
local common = require("celediel.NPCsGoHome.common")
local interop = require("celediel.NPCsGoHome.interop")
local positions = require("celediel.NPCsGoHome.positions")
-- }}}
-- {{{ variables and such
-- Waistworks string match
-- I'm probably trying too hard to avoid false positives
local waistworks = {"^[Vv]ivec,?.*[Ww]aist", "[Cc]analworks", "[Ww]aistworks"}
-- timers
local updateTimer
-- NPC homes
local homedNPCS = {}
local publicHouses = {}
-- city name if cell.name is nil
local wilderness = "Wilderness"
-- maybe this shouldn't be hardcoded
local publicHouseTypes = {inns = "Inns", guildhalls = "Guildhalls", temples = "Temples", houses = "Houses"}
-- local movedNPCs = {}
-- build a list of followers on cellChange
local followers = {}
local zeroVector = tes3vector3.new(0, 0, 0)
-- animated morrowind NPCs are contextual
local contextualNPCs = {"^AM_"}
-- }}}
-- {{{ helper functions
local function log(level, ...) if config.logLevel >= level then common.log(...) end end
-- {{{ npc evaluators
-- NPCs barter gold + value of all inventory items
local function calculateNPCWorth(npc)
local worth = npc.object.barterGold
if npc.object.inventory then
for _, item in pairs(npc.object.inventory) do worth = worth + (item.object.value or 0) end
end
return worth
end
-- }}}
-- {{{ housing
-- ? I honestly don't know if there are any wandering NPCs that "live" in close-by manors, but I wrote this anyway
local function checkIfManor(cellName, npcName)
if not cellName or (cellName and not string.find(cellName, "Manor")) then return end
local splitName = common.split(npcName)
local given = splitName[1]
local sur = splitName[2]
-- surnameless peasants don't live in manors
if not sur then return end
log(common.logLevels.large, "Checking if %s %s lives in %s", given, sur, cellName)
return string.match(cellName, sur)
end
local function pickPublicHouseType(cellName)
if cellName:match("Guild") then
return publicHouseTypes.guildhalls
elseif cellName:match("Temple") then
return publicHouseTypes.temples
elseif cellName:match("House") then
return publicHouseTypes.houses
else
return publicHouseTypes.inns
end
end
local function pickInnForNPC(npc, city)
-- todo: pick in Inn intelligently ?
-- high class inns for nobles and rich merchants and such
-- lower class inns for commoners and poor merchants
-- ? pick based on barterGold and value of equipment for merchants ?
-- ? for others, pick based on value of equipment
-- but for now pick one at random
if publicHouses[city] and publicHouses[city][publicHouseTypes.inns] then
local choice = table.choice(publicHouses[city][publicHouseTypes.inns])
if not choice then return end
log(common.logLevels.medium, "Picking inn %s, %s for %s", choice.city, choice.name, npc.object.name)
return choice.cell
end
end
local function pickPublicHouseForNPC(npc, city)
-- look for wandering guild members
if publicHouses[city] and publicHouses[city][publicHouseTypes.guildhalls] then
for _, data in pairs(publicHouses[city][publicHouseTypes.guildhalls]) do
-- if npc's faction and proprietor's faction match, pick that one
if npc.object.faction == data.proprietor.object.faction then
log(common.logLevels.medium, "Picking %s for %s based on faction", data.cell.id, npc.object.name)
return data.cell
end
end
end
if publicHouses[city] and publicHouses[city][publicHouseTypes.temples] then
for _, data in pairs(publicHouses[city][publicHouseTypes.temples]) do
if npc.object.faction == data.proprietor.object.faction then
log(common.logLevels.medium, "Picking temple %s for %s based on faction", data.cell.id, npc.object.name)
return data.cell
end
end
end
-- found nothing so pick an inn
return pickInnForNPC(npc, city)
end
local function createHomedNPCTableEntry(npc, home, startingPlace, isHome)
if npc.object and (npc.object.name == nil or npc.object.name == "") then return end
log(common.logLevels.medium, "Found home for %s: %s... adding it to in memory table...", npc.object.name, home.id)
local pickedPosition, pickedOrientation, p, o
if isHome and positions.npcs[npc.object.name] then
p = positions.npcs[npc.object.name].position
o = positions.npcs[npc.object.name].orientation
pickedPosition = positions.npcs[npc.object.name] and tes3vector3.new(p[1], p[2], p[3]) or zeroVector:copy()
pickedOrientation = positions.npcs[npc.object.name] and tes3vector3.new(o[1], o[2], o[3]) or zeroVector:copy()
elseif positions.cells[home.id] then
p = table.choice(positions.cells[home.id]).position
o = table.choice(positions.cells[home.id]).orientation
pickedPosition = positions.cells[home.id] and tes3vector3.new(p[1], p[2], p[3]) or zeroVector:copy()
pickedOrientation = positions.cells[home.id] and tes3vector3.new(o[1], o[2], o[3]) or zeroVector:copy()
else
pickedPosition = zeroVector:copy()
pickedOrientation = zeroVector:copy()
end
local this = {
name = npc.object.name,
npc = npc,
isHome = isHome,
home = home,
homeName = home.id,
ogPlace = startingPlace,
ogPlaceName = startingPlace.id,
ogPosition = npc.position and npc.position:copy() or zeroVector:copy(),
ogOrientation = npc.orientation and npc.orientation:copy() or zeroVector:copy(),
homePosition = pickedPosition,
homeOrientation = pickedOrientation,
worth = calculateNPCWorth(npc)
}
homedNPCS[home.id] = this
interop.setHomedNPCTable(homedNPCS)
return this
end
-- looks through doors to find a cell that matches a wandering NPCs name
local function pickHomeForNPC(cell, npc)
-- wilderness cells don't have name
if not cell.name then return end
-- don't move contextual, such as Animated Morrowind, NPCs at all
for _, str in pairs(contextualNPCs) do if npc.object.id:match(str) then return end end
local name = npc.object.name
local city = common.split(cell.name, ",")[1]
for door in cell:iterateReferences(tes3.objectType.door) do
if door.destination then
local dest = door.destination.cell
if dest.id:match(name) or checkIfManor(dest.name, name) then
return createHomedNPCTableEntry(npc, dest, cell, true)
end
end
end
-- haven't found a home, so put them in an inn or guildhall
if config.homelessWanderersToPublicHouses then
log(common.logLevels.medium, "Didn't find a home for %s, trying inns", npc.object.name)
local dest = pickPublicHouseForNPC(npc, city)
-- return createHomedNPCTableEntry(npc, dest, door)
if dest then return createHomedNPCTableEntry(npc, dest, cell, false) end
end
return nil
end
-- }}}
-- {{{ checks
local function isIgnoredNPC(npc)
local obj
if npc.object.baseObject then
obj = npc.object.baseObject
else
obj = npc.object
end
-- ignore dead or attack on sight NPCs
local isDead = false
local isHostile = false
if npc.mobile then
if npc.mobile.health.current <= 0 then isDead = true end
if npc.mobile.fight > 70 then isHostile = true end
end
-- todo: non mwscript version of these
local isVampire = mwscript.getSpellEffects({reference = npc, spell = "vampire sun damage"})
local isWerewolf = mwscript.getSpellEffects({reference = npc, spell = "werewolf vision"})
log(common.logLevels.large, ("Checking NPC:%s (%s or %s): id blocked:%s, mod blocked:%s " ..
"guard:%s dead:%s vampire:%s werewolf:%s dreamer:%s follower:%s hostile:%s"), obj.name, npc.object.id,
npc.object.baseObject and npc.object.baseObject.id or "nil", config.ignored[obj.id],
config.ignored[obj.sourceMod], npc.object.isGuard, isDead, isVampire, isWerewolf,
obj.class.id == "Dreamers", followers[obj.id], isHostile)
return config.ignored[obj.id] or --
config.ignored[obj.sourceMod] or --
npc.object.isGuard or --
isDead or -- don't move dead NPCS
isHostile or --
followers[obj.id] or -- ignore followers
isVampire or --
isWerewolf or --
obj.class.id == "Dreamers" or obj.class.id == "Witchhunter" --
end
-- checks NPC class and faction in cells for block list and adds to publicHouse list
local function isPublicHouse(cell)
local npcs = {factions = {}, total = 0}
for npc in cell:iterateReferences(tes3.objectType.npc) do
-- Check for NPCS of ignored classes first
if not isIgnoredNPC(npc) and (npc.object.class and config.ignored[npc.object.class.id]) then
log(common.logLevels.medium, "NPC:\'%s\' of class:\'%s\' made %s public", npc.object.name,
npc.object.class and npc.object.class.id or "none", cell.name)
local city, publicHouseName
if cell.name then
city = common.split(cell.name, ",")[1]
publicHouseName = common.split(cell.name, ",")[2]:gsub("^%s", "")
else
city = wilderness
publicHouseName = cell.id
end
local type = pickPublicHouseType(cell.name)
if not publicHouses[city] then publicHouses[city] = {} end
if not publicHouses[city][type] then publicHouses[city][type] = {} end
publicHouses[city][type][cell.name] = {
name = publicHouseName,
city = city,
cell = cell,
proprietor = npc,
worth = calculateNPCWorth(npc)
-- positions = positions.cells[cell.id] or {position = zeroVector, orientation = zeroVector}
}
interop.setInnTable(publicHouses)
return true
end
local faction = npc.object.faction and npc.object.faction.id
if faction then
if not npcs.factions[faction] then npcs.factions[faction] = {total = 0, percentage = 0} end
npcs.factions[faction].total = npcs.factions[faction].total + 1
end
npcs.total = npcs.total + 1
end
-- no NPCs of ignored classes, so let's check out factions
for faction, info in pairs(npcs.factions) do
info.percentage = info.total / npcs.total
log(common.logLevels.large,
"No NPCs of ignored class in %s, checking faction %s (ignored: %s) with %s (%s%%) vs total %s", cell.name,
faction, config.ignored[faction], info.total, info.percentage * 100, npcs.total)
-- less than 3 NPCs can't possibly be a public house unless it's a Blades house
if config.ignored[faction] and (npcs.total >= config.minimumOccupancy or faction == "Blades") and
info.percentage >= config.factionIgnorePercentage then
log(common.logLevels.medium, "%s is %s%% faction %s, marking public.", cell.name, info.percentage * 100,
faction)
return true
end
end
log(common.logLevels.large, "%s isn't public", cell.name)
return false
end
-- todo: check cell contents to decide if it should be locked
local function isIgnoredDoor(door, homeCellId)
-- don't lock non-cell change doors, and don't lock doors to outside
if not door.destination then
log(common.logLevels.large, "Non-Cell-change door %s, ignoring", door.id)
return true
end
-- Only doors in cities and towns (cells that share the same first characters) return true
local inCity = string.sub(homeCellId, 1, 4) == string.sub(door.destination.cell.id, 1, 4)
-- peek inside doors to look for guild halls, inns and clubs
local isPublic = isPublicHouse(door.destination.cell)
local hasOccupants = false
for npc in door.destination.cell:iterateReferences(tes3.objectType.npc) do
if not isIgnoredNPC(npc) then
hasOccupants = true
-- break
end
end
log(common.logLevels.large, "%s is %s, %s is %s (%sin a city, is %spublic)", door.destination.cell.id,
config.ignored[door.destination.cell.id] and "ignored" or "not ignored", door.destination.cell.sourceMod,
config.ignored[door.destination.cell.sourceMod] and "ignored" or "not ignored", inCity and "" or "not ",
isPublic and "" or "not ")
return config.ignored[door.destination.cell.id] or config.ignored[door.destination.cell.sourceMod] or not inCity or
isPublicHouse or not hasOccupants
end
local function isIgnoredCell(cell)
log(common.logLevels.large, "%s is %s, %s is %s", cell.id, config.ignored[cell.id] and "ignored" or "not ignored",
cell.sourceMod, config.ignored[cell.sourceMod] and "ignored" or "not ignored")
-- don't do things in the wilderness
-- local wilderness = false
-- if not cell.name then wilderness = true end
return config.ignored[cell.id] or config.ignored[cell.sourceMod] -- or wilderness
end
local function checkInteriorCell(cell)
if not cell then return end
log(common.logLevels.large, "Cell: interior: %s, behaves as exterior: %s therefore returning %s", cell.isInterior,
cell.behavesAsExterior, cell.isInterior and not cell.behavesAsExterior)
return cell.isInterior and not cell.behavesAsExterior
end
local function checkCantonCell(cellName)
for _, str in pairs(waistworks) do if cellName:match(str) then return true end end
return false
end
-- AT NIGHT
local function checkTime()
log(common.logLevels.large, "Current time is %s, things are closed between %s and %s",
tes3.worldController.hour.value, config.closeTime, config.openTime)
return tes3.worldController.hour.value >= config.closeTime or tes3.worldController.hour.value <= config.openTime
end
-- inclement weather
local function checkWeather(cell)
if not cell.region then return end
log(common.logLevels.large, "Weather: %s >= %s == %s", cell.region.weather.index, config.worstWeather,
cell.region.weather.index >= config.worstWeather)
return cell.region.weather.index >= config.worstWeather
end
-- travel agents, their steeds, and argonians stick around
local function badWeatherNPC(npc)
if not npc.object then return end
log(common.logLevels.large, "NPC Inclement Weather: %s is %s, %s", npc.object.name, npc.object.class.name,
npc.object.race.id)
return npc.object.class.name == "Caravaner" or npc.object.class.name == "Gondolier" or npc.object.race.id ==
"Argonian"
end
-- }}}
-- {{{ cell change checks
local function checkEnteredSpawnedNPCHome(cell)
local home = homedNPCS[cell.id]
if home then log(common.logLevels.medium, "Entering home of %s, %s", home.name, home.homeName) end
end
local function checkEnteredPublicHouse(cell, city)
local type = pickPublicHouseType(cell.name)
local publicHouse = publicHouses[city] and (publicHouses[city][type] and publicHouses[city][type][cell.name])
if publicHouse then
log(common.logLevels.medium, "Entering public space %s, %s, in the city of %s. Talk to %s, %s for services.",
publicHouse.name, publicHouse.type, publicHouse.city, publicHouse.proprietor.object.name,
publicHouse.proprietor.object.class)
end
end
-- }}}
-- }}}
-- {{{ real meat and potatoes functions
local function moveNPC(data)
-- movedNPCs[#movedNPCs + 1] = data
-- table.insert(movedNPCs, data)
-- interop.setMovedNPCsTable(movedNPCs)
table.insert(tes3.player.data.NPCsGoHome.movedNPCs, data)
interop.setMovedNPCsTable(tes3.player.data.NPCsGoHome.movedNPCs)
tes3.positionCell({
cell = data.home,
reference = data.npc,
position = data.homePosition,
orientation = data.homeOrientation
})
log(common.logLevels.medium, "Moving %s to home %s (%s, %s, %s)", data.npc.object.name, data.home.id,
data.homePosition.x, data.homePosition.y, data.homePosition.z)
end
local function putNPCsBack()
-- for i = #movedNPCs, 1, -1 do
for i = #tes3.player.data.NPCsGoHome.movedNPCs, 1, -1 do
-- local data = table.remove(movedNPCs, i)
local data = table.remove(tes3.player.data.NPCsGoHome.movedNPCs, i)
log(common.logLevels.medium, "Moving %s back outside to %s (%s, %s, %s)", data.npc.object.name, data.ogPlace.id,
data.ogPosition.x, data.ogPosition.y, data.ogPosition.z)
tes3.positionCell({
cell = data.ogPlace,
reference = data.npc,
position = data.ogPosition,
orientation = data.ogPlace
})
-- interop.setMovedNPCsTable(movedNPCs)
interop.setMovedNPCsTable(tes3.player.data.NPCsGoHome.movedNPCs)
end
end
-- todo: rename to toggleNPCs(cell, state = true|false)
-- todo: using tes3.setEnabled({ enabled = state })
local function disableNPCs(cell)
-- iterate NPCs in the cell, move them to their homes, and keep track of moved NPCs so we can move them back later
for npc in cell:iterateReferences(tes3.objectType.npc) do
-- for npc, _ in pairs(cellsInMemory[cell].npcs) do
if config.disableNPCs and not isIgnoredNPC(npc) then
log(common.logLevels.large, "People change")
-- find NPC homes
local npcHome = config.moveNPCs and pickHomeForNPC(cell, npc) or nil
local tmpLogLevelNPCHome = npcHome and common.logLevels.small or common.logLevels.medium
log(tmpLogLevelNPCHome, "%s %s %s%s", npc.object.name,
npcHome and (npcHome.isHome and "lives in" or "goes to") or "lives",
npcHome and npcHome.home or "nowhere", npcHome and (npcHome.isHome and "." or " at night."))
-- disable or move NPCs
if (checkTime() or
(checkWeather(cell) and
(not badWeatherNPC(npc) or (badWeatherNPC(npc) and not config.keepBadWeatherNPCs)))) then
if npcHome then
moveNPC(npcHome)
else
log(common.logLevels.medium, "Disabling homeless %s", npc.object.name)
-- npc:disable() -- ! this one sometimes causes crashes
mwscript.disable({reference = npc}) -- ! this one is deprecated
-- tes3.setEnabled({reference = npc, enabled = false})
end
else
if not npcHome then
log(common.logLevels.medium, "Enabling homeless %s", npc.object.name)
-- npc:enable()
mwscript.enable({reference = npc})
-- tes3.setEnabled({reference = npc, enabled = true})
end
end
end
end
-- now put NPCs back
-- if not (checkTime() or checkWeather(cell)) and #movedNPCs > 0 then putNPCsBack() end
if not (checkTime() or checkWeather(cell)) and #tes3.player.data.NPCsGoHome.movedNPCs > 0 then putNPCsBack() end
end
local function disableSiltStriders(cell)
log(common.logLevels.large, "Looking for silt striders")
for activator in cell:iterateReferences(tes3.objectType.activator) do
log(common.logLevels.large, "Is %s a silt strider??", activator.object.id)
if activator.object.id:match("siltstrider") then
if checkTime() or (checkWeather(cell) and not config.keepBadWeatherNPCs) then
log(common.logLevels.medium, "Disabling silt strider %s!", activator.object.name)
mwscript.disable({reference = activator})
-- activator:disable()
-- tes3.setEnabled({reference = activator, enabled = false})
else
log(common.logLevels.medium, "Enabling silt strider %s!", activator.object.name)
mwscript.enable({reference = activator})
-- activator:enable()
-- tes3.setEnabled({reference = activator, enabled = true})
end
end
end
log(common.logLevels.large, "Done with silt striders")
end
local function processDoors(cell)
log(common.logLevels.large, "Checking out doors")
for door in cell:iterateReferences(tes3.objectType.door) do
if not door.data.NPCsGoHome then door.data.NPCsGoHome = {} end
log(common.logLevels.large, "Door has destination? %s", door.destination ~= nil)
if config.lockDoors and not isIgnoredDoor(door, cell.id) then
log(common.logLevels.large, "It knows there's a door")
local alreadyLocked = tes3.getLocked({reference = door})
door.data.NPCsGoHome.alreadyLocked = alreadyLocked
log(common.logLevels.large, "Locked Status: %s", alreadyLocked)
if checkTime() then
if not door.data.NPCsGoHome.alreadyLocked then
log(common.logLevels.large, "It should lock now")
log(common.logLevels.large, "What door is this anyway: %s to %s", door.object.name,
door.destination.cell.id)
local lockLevel = math.random(25, 100)
tes3.lock({reference = door, level = lockLevel})
door.data.NPCsGoHome.modified = true
end
else
if door.data.NPCsGoHome and door.data.NPCsGoHome.modified then
door.data.NPCsGoHome.modified = false
tes3.setLockLevel({reference = door, level = 0})
tes3.unlock({reference = door})
log(common.logLevels.large, "It should unlock now")
log(common.logLevels.large, "What unlocked door is this anyway: %s to %s", door.object.name,
door.destination.cell.id)
end
end
log(common.logLevels.large, "Now Locked Status: %s", tes3.getLocked({reference = door}))
end
end
log(common.logLevels.large, "Done with doors")
end
local function applyChanges(cell)
if not cell then cell = tes3.getPlayerCell() end
-- build our followers list
for friend in tes3.iterate(tes3.mobilePlayer.friendlyActors) do
local obj
if friend.object.baseObject then
obj = friend.object.baseObject
else
obj = friend.object
end
followers[obj.id] = true
log(common.logLevels.large, "%s is follower", obj.id)
end
if isIgnoredCell(cell) then return end
-- Interior cell, except Waistworks, don't do anything
if checkInteriorCell(cell) and not (config.waistWorks and checkCantonCell(cell.name)) then return end
log(common.logLevels.medium, "The hour is: %s", tes3.worldController.hour.value)
-- Disable NPCs in cell
disableNPCs(cell)
disableSiltStriders(cell)
-- check doors in cell, locking those that aren't inns/clubs
processDoors(cell)
end
local function updateCells()
log(common.logLevels.medium, "Updating active cells!")
for _, cell in pairs(tes3.getActiveCells()) do
log(common.logLevels.large, "Applying changes to cell %s", cell.id)
applyChanges(cell)
end
end
local function updatePlayerTrespass(cell)
cell = cell or tes3.getPlayerCell()
if checkInteriorCell(cell) and not isIgnoredCell(cell) and not isPublicHouse(cell) then
if config.disableInteraction and checkTime() then
tes3.player.data.NPCsGoHome.intruding = true
else
tes3.player.data.NPCsGoHome.intruding = false
end
else
tes3.player.data.NPCsGoHome.intruding = false
end
log(common.logLevels.small, "Updating player trespass status to %s", tes3.player.data.NPCsGoHome.intruding)
end
-- }}}
-- {{{ event functions
local function onActivated(e)
if e.activator ~= tes3.player and e.target.object.objectType ~= tes3.objectType.npc then return end
if tes3.player.data.NPCsGoHome.intruding and not isIgnoredNPC(e.target) then
tes3.messageBox(string.format("%s: Get out before I call the guards!", e.target.object.name))
return false
end
end
local function onLoaded()
if not tes3.player.data.NPCsGoHome then tes3.player.data.NPCsGoHome = {} end
if not tes3.player.data.NPCsGoHome.movedNPCs then tes3.player.data.NPCsGoHome.movedNPCs = {} end
-- movedNPCs = {}
if not updateTimer or (updateTimer and updateTimer.state ~= timer.active) then
updateTimer = timer.start({
type = timer.simulate,
duration = config.timerInterval,
iterations = -1,
callback = updateCells
})
end
end
local function onCellChanged(e)
updateCells()
updatePlayerTrespass(e.cell)
checkEnteredSpawnedNPCHome(e.cell)
if e.cell.name then -- exterior wilderness cells don't have name
checkEnteredPublicHouse(e.cell, common.split(e.cell.name, ",")[1])
end
--[[
-- ! delete this
if config.logLevel == common.logLevels.none then
if (e.previousCell and e.previousCell.name and e.previousCell.name ~= e.cell.name) then
mwse.log("}\n[\"%s\"] = {", e.cell.id)
elseif not e.previousCell then
mwse.log("[\"%s\"] = {", e.cell.id)
end
end
-- ! ]]
end
-- }}}
-- {{{ event registering
event.register("loaded", onLoaded)
event.register("cellChanged", onCellChanged)
event.register("activate", onActivated)
event.register("modConfigReady", function() mwse.mcm.register(require("celediel.NPCsGoHome.mcm")) end)
-- }}}
-- vim:fdm=marker

View file

@ -0,0 +1,188 @@
local config = require("celediel.NPCsGoHome.config").getConfig()
local common = require("celediel.NPCsGoHome.common")
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)
})
-- todo: categorize the options
local category = page:createCategory(common.modName)
category:createYesNoButton({
label = "Lock doors and containers at night?",
variable = createTableVar("lockDoors")
})
category:createYesNoButton({
label = "Disable non-Guard NPCs at night?",
variable = createTableVar("disableNPCs")
})
category:createYesNoButton({
label = "Move NPCs with homes instead of disabling them?",
variable = createTableVar("moveNPCs")
})
category:createYesNoButton({
label = "Move \"homeless\" NPCs to Inns at night and in bad weather instead of disabling them?",
variable = createTableVar("homelessWanderersToPublicHouses")
})
category:createYesNoButton({
label = "Prevent dialogue in interiors at night?",
variable = createTableVar("disableInteraction")
})
category:createYesNoButton({
label = "Treat Canton waistworks and canalworks as exteriors (lock doors and disable NPCs)",
variable = createTableVar("waistWorks")
})
category:createYesNoButton({
label = "Keep Caravaners, their Silt Striders, and Argonians enabled in inclement weather?",
variable = createTableVar("keepBadWeatherNPCs")
})
category:createDropdown({
label = "NPC Inclement Weather Cutoff Point",
description = "NPCs \"go home\" in this weather or worse",
options = {
{label = "None", value = 10},
{label = "Clear", value = tes3.weather.clear},
{label = "Cloudy", value = tes3.weather.cloudy},
{label = "Foggy", value = tes3.weather.foggy},
{label = "Overcast", value = tes3.weather.overcast},
{label = "Rain", value = tes3.weather.rain},
{label = "Thunderstorm", value = tes3.weather.thunder},
{label = "Ashstorm", value = tes3.weather.ash},
{label = "Blight", value = tes3.weather.blight},
{label = "Snow", value = tes3.weather.snow},
{label = "Blizzard", value = tes3.weather.blizzard}
},
defaultSetting = tes3.weather.thunder,
variable = createTableVar("worstWeather")
})
category:createSlider({
label = "Close Time",
description = "Time when people \"go home\" and doors lock",
min = 0,
max = 24,
step = 1,
jump = 2,
variable = createTableVar("closeTime")
})
category:createSlider({
label = "Open Time",
description = "Time when people \"wake up\" and doors unlock",
min = 0,
max = 24,
step = 1,
jump = 2,
variable = createTableVar("openTime")
})
category:createSlider({
label = "Minimum number of occupants for public house",
description = "Cells with less than this number of occupants won't even be considered for \"public house\" status.\n\n" ..
"Blades (if on the ignore list) are an exception to this rule, because Blades trainers don't mind if you come in.",
min = 1,
max = 20,
step = 1,
jump = 4,
variable = createTableVar("minimumOccupancy")
})
category:createSlider({
label = "Faction Ignore Percentage",
description = "Cells whose occupants are this % or more of one faction will be marked public if that faction is on the ignored list.",
min = 0,
max = 100,
step = 5,
jump = 10,
variable = createTableVar("factionIgnorePercentage")
})
category:createSlider({
label = "Update Timer",
description = [[How often the update timer fires, in seconds. Updates are also triggered on cell change.]],
min = 1,
max = 60,
step = 1,
jump = 2,
-- todo: button or something to reset the timer to the new interval
restartRequired = true,
variable = createTableVar("timerInterval")
})
category:createDropdown({
label = "Debug log level",
description = [[Enable this if you want to flood mwse.log with nonsense. Even small is huge.]],
options = {
{label = "None", value = common.logLevels.none},
{label = "Small", value = common.logLevels.small},
{label = "Medium", value = common.logLevels.medium},
{label = "Large", value = common.logLevels.large}
},
variable = createTableVar("logLevel")
})
template:createExclusionsPage({
label = "Ignored things",
description = ("NPCs on the Ignored list will not disappear at night, and will be available to talk to if indoors. " ..
"Interior Cells on the Ignored list will not have the doors to them locked. Exterior cells will have neither doors nor NPCs in them affected. " ..
"Many exterior cells have the same name, and so you will need to use trial and error to disable the correct ones. " ..
"For Plugins, all the above applies to all applicable data from the mod. " ..
"For classes any cell that contains at least one NPC of said class will be considered \"public\", " ..
"and will not be locked or have its NPCS disabled at night. Exterior NPCs of this class or faction will not be disabled. " ..
"For factions, at least 75% of the cell's occupants need to be a part of said faction." ..
"Best used with Guilds and Publican classes."),
showAllBlocked = false,
variable = createTableVar("ignored"),
filters = {
{label = "Plugins", type = "Plugin"},
{label = "NPCs", type = "Object", objectType = tes3.objectType.npc},
{
label = "Cells",
callback = (function()
local CellNames = {}
for cell, _ in pairs(tes3.dataHandler.nonDynamicData.cells) do
table.insert(CellNames, cell)
end
return CellNames
end)
},
{
label = "Factions",
callback = function()
local factions = {}
for _, faction in pairs(tes3.dataHandler.nonDynamicData.factions) do
table.insert(factions, faction.id)
end
return factions
end
},
{
label = "Classes",
callback = function()
local classes = {}
for _, class in pairs(tes3.dataHandler.nonDynamicData.classes) do
table.insert(classes, class.id)
end
return classes
end
}
}
})
return template

View file

@ -0,0 +1,125 @@
-- for spawning NPCs into cells or their homes
return {
-- home positions for NPCS
npcs = {
-- Balmora, vanilla
["Rarayn Radarys"] = {position = {136.23, 132.69, 7.00}, orientation = {0.00, 0.00, -3.14}},
["Dralosa Athren"] = {position = {190.74, 91.01, 7.00}, orientation = {0.00, 0.00, -1.55}},
["Balyn Omarel"] = {position = {0, 0, 0}, orientation = {0, 0, 0}},
["Dralcea Arethi"] = {position = {0, 0, 0}, orientation = {0, 0, 0}}
},
-- positions picked from a list for public houses
cells = {
["Balmora, Lucky Lockup"] = {
{position = {222.87, 1290.63, -505.00}, orientation = {0.00, 0.00, -2.54}},
{position = {-10.30, 757.07, -505.00}, orientation = {0.00, 0.00, -0.04}},
{position = {5.55, 996.71, -504.11}, orientation = {0.00, 0.00, -0.01}},
{position = {221.29, 1358.15, -505.00}, orientation = {0.00, 0.00, -3.10}},
{position = {334.71, 913.76, -505.00}, orientation = {0.00, 0.00, -3.10}}
},
["Balmora, Council Club"] = {
{position = {-62.33, -31.99, 7.00}, orientation = {0.00, 0.00, 0.82}},
{position = {579.51, -1.70, -249.00}, orientation = {0.00, 0.00, -0.64}},
{position = {406.88, 590.81, -249.00}, orientation = {0.00, 0.00, 3.09}},
{position = {-36.75, 269.06, -249.00}, orientation = {0.00, 0.00, 1.05}},
{position = {-716.66, 719.97, -505.00}, orientation = {0.00, 0.00, 1.42}},
{position = {-1.61, -200.41, -249.00}, orientation = {0.00, 0.00, -0.02}},
{position = {-272.50, 71.34, -249.00}, orientation = {0.00, 0.00, 1.28}}
},
["Balmora, Eight Plates"] = {
{position = {463.41, -333.82, -249.00}, orientation = {0.00, 0.00, -0.50}},
{position = {426.63, -320.01, -249.00}, orientation = {0.00, 0.00, -0.92}},
{position = {-215.54, 486.13, -249.00}, orientation = {0.00, 0.00, 0.96}},
{position = {-236.28, 820.92, -249.00}, orientation = {0.00, 0.00, -0.03}},
{position = {-109.68, 823.78, -249.00}, orientation = {0.00, 0.00, -0.07}},
{position = {-28.39, 822.93, -249.00}, orientation = {0.00, 0.00, 0.02}},
{position = {59.99, 823.30, -249.00}, orientation = {0.00, 0.00, -0.02}},
{position = {187.22, 919.89, -249.00}, orientation = {0.00, 0.00, -1.50}},
{position = {185.86, 984.72, -249.00}, orientation = {0.00, 0.00, -1.55}},
{position = {187.40, 1075.68, -249.00}, orientation = {0.00, 0.00, -1.65}},
{position = {190.35, 1164.61, -249.00}, orientation = {0.00, 0.00, -1.53}},
{position = {404.83, 1090.28, -249.00}, orientation = {0.00, 0.00, -0.61}},
{position = {587.57, 1348.27, -249.00}, orientation = {0.00, 0.00, -2.48}},
{position = {118.16, -274.01, 7.00}, orientation = {0.00, 0.00, -1.56}},
{position = {61.89, -207.22, 7.00}, orientation = {0.00, 0.00, 1.49}}
},
["Balmora, South Wall Cornerclub"] = {
{position = {32.41, 586.87, 7.00}, orientation = {0.00, 0.00, 2.96}},
{position = {-278.73, -77.59, -249.00}, orientation = {0.00, 0.00, 0.44}},
{position = {301.04, -21.29, -249.00}, orientation = {0.00, 0.00, -0.69}},
{position = {382.68, 300.45, -249.00}, orientation = {0.00, 0.00, -0.12}},
{position = {468.77, 465.61, -249.00}, orientation = {0.00, 0.00, -1.59}},
{position = {467.71, 588.84, -249.00}, orientation = {0.00, 0.00, -1.59}},
{position = {446.45, 716.87, -249.00}, orientation = {0.00, 0.00, -2.51}},
{position = {820.65, 692.28, -249.00}, orientation = {0.00, 0.00, -1.65}},
{position = {806.36, 296.22, -217.31}, orientation = {0.00, 0.00, -1.11}},
{position = {337.37, 737.83, -249.00}, orientation = {0.00, 0.00, 3.09}}
},
["Balmora, South Wall Cornerclub (mod)"] = {
{position = {239.72, 589.39, -249.00}, orientation = {0.00, 0.00, -2.86}},
{position = {241.20, 588.73, -249.00}, orientation = {0.00, 0.00, -1.62}},
{position = {245.71, 471.54, -249.00}, orientation = {0.00, 0.00, -1.52}},
{position = {158.42, 334.12, -249.00}, orientation = {0.00, 0.00, -0.14}},
{position = {544.37, 441.53, -249.00}, orientation = {0.00, 0.00, 0.65}},
{position = {640.98, 786.91, -248.08}, orientation = {0.00, 0.00, 1.56}},
{position = {581.22, 644.53, -248.08}, orientation = {0.00, 0.00, 2.71}}
},
["Gnisis, Madach Tradehouse"] = {
{position = {-625.02, 305.07, -894.00}, orientation = {0.00, 0.00, 2.46}},
{position = {100.10, -49.81, -894.00}, orientation = {0.00, 0.00, -2.84}},
{position = {-420.28, -723.51, -894.00}, orientation = {0.00, 0.00, 0.29}},
{position = {575.21, -719.01, -894.00}, orientation = {0.00, 0.00, -0.74}},
{position = {873.63, 173.11, -894.00}, orientation = {0.00, 0.00, 2.76}},
{position = {659.33, 225.69, -894.00}, orientation = {0.00, 0.00, -2.37}},
{position = {-237.97, 153.85, -126.00}, orientation = {0.00, 0.00, -1.77}},
{position = {-238.99, 253.70, -126.00}, orientation = {0.00, 0.00, -1.66}},
{position = {-242.53, 359.42, -126.00}, orientation = {0.00, 0.00, -2.30}},
{position = {25.28, 568.43, -126.00}, orientation = {0.00, 0.00, -3.09}},
{position = {231.21, 241.74, -126.00}, orientation = {0.00, 0.00, -1.47}},
{position = {-23.55, 250.11, -382.00}, orientation = {0.00, 0.00, -1.47}}
},
["Gnisis, Fort Darius"] = {
{position = {-53.93, 519.00, -126.00}, orientation = {0.00, 0.00, 3.09}},
{position = {14.06, 231.89, -382.00}, orientation = {0.00, 0.00, 1.37}},
{position = {276.92, 314.82, -371.95}, orientation = {0.00, 0.00, 2.76}},
{position = {185.69, 240.55, -371.95}, orientation = {0.00, 0.00, 2.08}},
{position = {269.97, 531.29, -382.00}, orientation = {0.00, 0.00, -2.14}},
{position = {-265.57, 886.74, -382.00}, orientation = {0.00, 0.00, 1.37}},
{position = {-6.38, 773.91, -382.00}, orientation = {0.00, 0.00, -1.36}},
{position = {693.12, 279.94, 130.00}, orientation = {0.00, 0.00, -1.12}},
{position = {-1166.08, 319.61, -254.00}, orientation = {0.00, 0.00, 3.12}},
{position = {-1368.90, 232.95, -254.00}, orientation = {0.00, 0.00, -0.39}},
{position = {-1728.43, 172.37, -254.00}, orientation = {0.00, 0.00, 0.91}}
},
["Ald-ruhn, The Rat In The Pot"] = {
{position = {154.18, 425.84, 2.00}, orientation = {0.00, 0.00, 0.64}},
{position = {19.49, 429.50, 2.00}, orientation = {0.00, 0.00, -1.60}},
{position = {16.32, 517.69, 2.00}, orientation = {0.00, 0.00, -1.49}},
{position = {18.28, 643.34, 2.00}, orientation = {0.00, 0.00, -1.52}},
{position = {19.79, 429.49, 2.00}, orientation = {0.00, 0.00, -1.60}},
{position = {-3.88, 281.16, 2.00}, orientation = {0.00, 0.00, -0.44}},
{position = {-117.24, 278.73, 2.00}, orientation = {0.00, 0.00, 0.02}},
{position = {-304.90, 274.83, 2.00}, orientation = {0.00, 0.00, -0.02}},
{position = {-394.37, 279.30, 2.00}, orientation = {0.00, 0.00, -0.05}},
{position = {-110.95, 308.02, -254.00}, orientation = {0.00, 0.00, -0.09}},
{position = {-347.46, 652.76, -254.00}, orientation = {0.00, 0.00, 1.93}},
{position = {-112.87, 694.26, -254.00}, orientation = {0.00, 0.00, -1.68}},
{position = {-99.06, 802.53, -254.00}, orientation = {0.00, 0.00, 1.56}},
{position = {139.54, 812.52, -254.00}, orientation = {0.00, 0.00, -1.63}},
{position = {111.24, 694.92, -254.00}, orientation = {0.00, 0.00, -1.63}},
{position = {701.44, -91.77, -510.00}, orientation = {0.00, 0.00, -1.82}},
{position = {402.54, -694.76, -510.00}, orientation = {0.00, 0.00, -0.21}},
{position = {-594.88, -618.74, -510.00}, orientation = {0.00, 0.00, 0.34}},
{position = {-703.68, 16.55, -510.00}, orientation = {0.00, 0.00, 1.66}},
{position = {-347.25, 403.67, -510.00}, orientation = {0.00, 0.00, 2.75}},
{position = {485.94, 416.56, -510.00}, orientation = {0.00, 0.00, -2.32}},
{position = {625.61, 414.57, -510.00}, orientation = {0.00, 0.00, 2.88}},
{position = {-35.49, 731.88, -510.00}, orientation = {0.00, 0.00, -1.53}},
{position = {-211.77, 641.18, -510.00}, orientation = {0.00, 0.00, 1.48}},
{position = {-680.81, 211.37, -482.86}, orientation = {0.00, 0.00, 1.72}},
{position = {-682.71, 335.97, -482.86}, orientation = {0.00, 0.00, 2.12}},
{position = {-595.91, 435.92, -482.86}, orientation = {0.00, 0.00, 2.63}},
{position = {-482.90, 419.23, -482.86}, orientation = {0.00, 0.00, -3.03}}
}
}
}