diff --git a/playerMap.lua b/playerMap.lua new file mode 100644 index 0000000..e7918fb --- /dev/null +++ b/playerMap.lua @@ -0,0 +1,214 @@ +-- This generates a json file that contains the position of +-- all the players on your server. It was inspired by the livemap +-- module by "Michael Fitzmayer" . +-- I rewrote this entire thing from scratch to work with 0.7 and hook +-- into the event handling system in a proper way. +-- My future plans for this will turn it into a nice per-player stream +-- of data, with separate update schedules per player and better +-- on-change handling of information deltas. That will work into +-- a few other ideas that I have with eater. + +jsonInterface = require("jsonInterface") +customEventHooks = require("customEventHooks") + +local playerMap = {} +playerMap.map = {} + +playerMap.updateInterval = 1000 + +-- currently unused, it'd be nice to have separate update and flush +-- intervals. That way other events updating things will propagate faster. +playerMap.flushInterval = 1000 +playerMap.fileName = "PlayerMap.json" +playerMap.timer = nil + +function playerMap.updateAllPlayers() + for pid, player in pairs(Players) do + playerMap.updatePlayer(pid) + end +end + +function playerMap.save() + jsonInterface.save(playerMap.fileName, playerMap.map) +end + +function playerMap.updateTimer() + playerMap.updateAllPlayers() + playerMap.save() + tes3mp.StartTimer(playerMap.timer) +end + +function OnPlayerMapUpdateTick() + playerMap.updateTimer() +end + +function playerMap.updatePlayer(pid) + if not playerMap.canUpdatePlayer(pid) then + return + end + + local player = Players[pid] + + -- Apparently this player did not login or register yet, so we won't + -- plot them. + if playerMap.map[player.accoundName] == nil then + return + end + + playerMap.updatePlayerLocation(pid) + playerMap.updatePlayerRotation(pid) + + playerMap.map[player.accountName].isOutside = tes3mp.IsInExterior(pid) + playerMap.map[player.accountName].cell = tes3mp.GetCell(pid) +end + +function playerMap.canUpdatePlayer(pid) + return Players[pid] ~= nil and Players[pid]:IsLoggedIn() +end + +function playerMap.updatePlayerLocation(pid) + playerMap.updatePlayerOutsideLocation(pid) + playerMap.updatePlayerInsideLocation(pid) + playerMap.updatePlayerTransitionLocation(pid) +end + +function playerMap.updatePlayerRotation(pid) + playerMap.setPlayerRotation(pid, + tes3mp.GetRotX(pid), + tes3mp.GetRotZ(pid)) +end + +-- We are outside, this is the happy path +function playerMap.updatePlayerOutsideLocation(pid) + if not tes3mp.IsInExterior(pid) then + return + end + + playerMap.setPlayerPosition(pid, + tes3mp.GetPosX(pid), + tes3mp.GetPosY(pid), + tes3mp.GetPosZ(pid)) +end + +-- We are inside and have just logged in, this is the worst path +function playerMap.updatePlayerInsideLocation(pid) + if tes3mp.IsInExterior(pid) then + return + end + + -- Player has already logged in and had a placeholder set + if playerMap.map[Players[pid].accoundName].x ~= nil then + return + end + + playerMap.setPlayerPosition(pid, + tes3mp.GetExteriorX(pid), + tes3mp.GetExteriorY(pid), + 0) -- cell exteriors have no z position +end + +-- We are inside but we were outside recently, so we can set out location to wherever +-- the door was. This is an okay path. +function playerMap.updatePlayerTransitionLocation(pid) + -- Player is outside so we don't need to be here + if tes3mp.IsInExterior(pid) then + return + end + + -- Last location was not outside so we can't save whatever overworld position we have anymore + if not playerMap.isOutside(pid) then + return + end + + playerMap.setPlayerPosition(pid, + tes3mp.GetPreviousCellPosX(pid), + tes3mp.GetPreviousCellPosY(pid), + tes3mp.GetPreviousCellPosZ(pid)) +end + +function playerMap.isOutside(pid) + return playerMap.map[Players[pid]].isOutside +end + +function playerMap.setPlayerPosition(pid, x, y, z) + local playerName = Players[pid].accoundName + + playerMap.map[playerName].x = math.floor( 0.5 + x ) + playerMap.map[playerName].y = math.floor( 0.5 + y ) + playerMap.map[playerName].z = math.floor( 0.5 + z ) +end + +function playerMap.setPlayerRotation(pid, rotx, rotz) + local playerName = Players[pid].accoundName + playerMap.map[playerName].rotx = rotx + playerMap.map[playerName].rotz = rotz + + local anglenorth = math.deg( rotz ) + + if anglenorth < 0 then + anglenorth = 360 + anglenorth + end + + playerMap.map[playerName].rot = math.floor( 0.5 + anglenorth ) +end + +-- OnGUIAction is called when you, login or register (and for a bunch of other things besides) +customEventHooks.registerHandler("OnGUIAction", function(eventStatus, pid, idGui, data) + if (idGui ~= guiHelper.ID.LOGIN and idGui ~= guiHelper.ID.REGISTER) then + return nil -- We aren't interested in anything but these two events + end + + if data == nil then + return nil -- data is nil when register or login fails + end + + local player = Players[pid] + + -- I don't really see how this could happen, but best to be safe. + if player == nil then + return + end + + playerMap.map[player.accountName] = {} +end) + +-- We want to use a validator here so that the player info still exists when this function is called. +-- If we registered a handler, it would be thrown away and all we would know is the pid, which +-- means we would not be able to throw away the player's account name key. +customEventHooks.registerValidator("OnPlayerDisconnect", function(eventStatus, pid) + if Players[pid] == nil or not Players[pid]:IsLoggedIn() then + return nil + end + + playerMap.map[Players[pid].accountName] = nil + return nil -- original eventstatus will be used in this case, we're not doing anything amazing +end) + +-- When the server starts, we want to start monitoring the players for +customEventHooks.registerHandler("OnServerPostInit", function(eventStatus) + playerMap.timer = tes3mp.CreateTimer("OnPlayerMapUpdateTick", updateInterval) + tes3mp.StartTimer(playerMap.timer) + + return nil +end) + +-- When the server exists we clear the json file of any entries so the map doesn't +-- "freeze" in a state of having some people on it. +customEventHooks.registerHandler("OnServerExit", function(eventStatus) + playerMap.map = {} + playerMap.save() + + -- TODO: Stop the update timer. I suppose it will also get destroyed when + -- we exit so maybe this is fine too. + + return nil +end) + +-- When the player changes from an exterior to an interior cell, we want to make sure that the last +-- saved coordinates are the ones the player had right before changing cells. This is the door. +customEventHooks.registerHandler("OnPlayerCellChange", function(eventStatus, pid) + playerMap.updatePlayer(pid) + return nil +end) + +return playerMap