Hello!
I have a problem where my RouteData in PlayerData (aka SlotData) is not getting saved on player leave. I also have another save script that saves the other data in SlotData.
Basically it keeps updated RouteData in a table called PlayerData but once the player leaves and it prints SlotData it is the stale data from when I joined meaning the data never saved. I can confirm this by joining again and seeing the stale data.
The main things you should be looking at is the PlayerRemoving function and where the PlayerData is being called.
RouteData is the data from a route in aviation. This is how it is structured:
["NTL-MQL"] = ▼ {
["ArrivalTime"] = 1780134921,
["CurrentDestination"] = "MQL",
["CurrentLeg"] = "Outbound",
["CurrentOrigin"] = "NTL",
["Demand"] = ▶ {...},
["DepartureTime"] = 1780129521,
["Destination"] = "MQL",
["Distance"] = 917,
["DistanceRemaining"] = 917,
["DistanceTravelled"] = 0,
["Expenses"] = 8,
["FlightTimeLeft"] = 2.471858113231571,
["IsActive"] = true,
["LastUpdateTime"] = 1782863886,
["Menu"] = ▶ {...},
["Menus"] = ▶ {...},
["Origin"] = "NTL",
["OriginGate"] = "Gate_001",
["PhaseDurations"] = ▶ {...},
["Plane"] = ▶ {...},
["Prices"] = ▶ {...},
["Progress"] = 0,
["Revenue"] = 184,
["Status"] = "Parked",
["TicketRevenue"] = ▶ {...},
["TotalFlightTime"] = 1.019262389949119,
["_cruiseSecondsLeft"] = 52.15574339694714,
["_lastAnimPhase"] = "Cruise (Outbound)"
},
SCRIPT:
-- RouteServerHandler.server.lua
local RouteSimulation = require(game.ReplicatedStorage.Modules.Route.Simulation.RouteSimulation)
local RouteSimManager = require(game.ReplicatedStorage.Modules.Route.Simulation.RouteSimulation.RouteSimManager)
local DefualtAirlineData = require(game.ReplicatedStorage.Modules.Data.DefualtAirlineData)
local FlightPhase = require(game.ReplicatedStorage.Modules.Route.Animation.FlightPhase)
local DataStoreService = game:GetService("DataStoreService")
local AirlineStore = DataStoreService:GetDataStore("AirlineDataStoreTest40")
local RouteAnimationRem = game.ReplicatedStorage.Events.Route.Animation.RouteAnimationRem
local GetRouteDisplay = game.ReplicatedStorage.Functions.Route.Simulation.GetRouteDisplay
local GetSlotRem = game.ReplicatedStorage.Events.Slot.GetSlot
-- [UserId] = { [Slot] = SlotData } — the single source of truth for route state
local PlayerData = {}
local PlayerRouteManagers = {} -- [UserId] = manager
local ActiveSlot = {} -- [UserId] = slotNumber
-- Expose for RouteSimAPI (GetRouteDisplay, etc.)
_G.RouteSim_ActiveSlot = ActiveSlot
_G.RouteSim_PlayerManagers = PlayerRouteManagers
local function deepCopy(tbl)
local copy = {}
for k, v in pairs(tbl) do
copy[k] = typeof(v) == "table" and deepCopy(v) or v
end
return copy
end
local function deepMerge(target, source)
for k, v in pairs(source) do
if typeof(v) == "table" and typeof(target[k]) == "table" then
deepMerge(target[k], v)
else
target[k] = v
end
end
end
local function loadSlot(player, slot)
local key = player.UserId .. "_Slot" .. slot
local data = AirlineStore:GetAsync(key)
if data then
return data
else
local newData = deepCopy(DefualtAirlineData)
AirlineStore:SetAsync(key, newData)
return newData
end
end
local function saveSlot(player, slot, data)
local key = player.UserId .. "_Slot" .. slot
AirlineStore:SetAsync(key, data)
end
GetRouteDisplay.OnServerInvoke = function(player, routeId)
-- not important
end
local function LoadSlotRoutes(manager, SlotData)
for routeId, routeData in pairs(SlotData.Routes or {}) do
if routeId == "RouteLines" then continue end
routeData.IsActive = true
manager:Register(routeId, routeData)
end
end
GetSlotRem.OnServerEvent:Connect(function(player, Slot)
print("[RouteSim] LOADED SLOT:", Slot, player.Name)
local UserId = player.UserId
PlayerData[UserId] = PlayerData[UserId] or {}
PlayerRouteManagers[UserId] = PlayerRouteManagers[UserId] or {}
ActiveSlot[UserId] = Slot
PlayerData[UserId][Slot] = loadSlot(player, Slot)
local SlotData = PlayerData[UserId][Slot]
local manager = RouteSimManager.new()
PlayerRouteManagers[UserId][Slot] = manager
LoadSlotRoutes(manager, SlotData)
local PausedRoutes = {}
local function NextPhaseAfterAnimation(phase, routeData)
-- not important
end
task.spawn(function() -- Might be important you can have a look if you need
while player.Parent do
for routeId, routeData in pairs(SlotData.Routes) do
if routeId == "RouteLines" then continue end
if PausedRoutes[routeId] then continue end
local ok, phaseName = pcall(function()
return routeData.PhaseOverride or FlightPhase.GetPhase(routeData)
end)
if not ok then
warn("GetPhase error for", routeId, ":", phaseName)
continue
end
print("SIMULATING ROUTE: ", phaseName, routeData.Progress)
print(PlayerData)
-- CRUISE: tick progress directly
if phaseName == "Cruise (Outbound)" or phaseName == "Cruise (Return)" then
local isOutbound = phaseName == "Cruise (Outbound)"
if routeData._lastAnimPhase ~= phaseName then
routeData.PhaseDurations = routeData.PhaseDurations or {}
routeData.PhaseDurations.EnRoute = routeData.PhaseDurations.EnRoute or (routeData.TotalFlightTime * 60)
local enRoute = routeData.PhaseDurations.EnRoute
local cStart = isOutbound and 0.025 or 0.525
local cEnd = isOutbound and 0.5 or 1.0
local localProg = math.clamp((routeData.Progress - cStart) / (cEnd - cStart), 0, 1)
routeData._cruiseSecondsLeft = enRoute * (1 - localProg)
routeData.LastUpdateTime = os.time()
routeData._lastAnimPhase = phaseName
RouteAnimationRem:FireClient(player, routeData, nil, phaseName)
end
routeData.PhaseOverride = nil
local now = os.time()
local delta = math.max(0, now - (routeData.LastUpdateTime or now))
routeData.LastUpdateTime = now
routeData._cruiseSecondsLeft = math.max(0, (routeData._cruiseSecondsLeft or 0) - delta)
local enRoute = routeData.PhaseDurations.EnRoute
local cStart = isOutbound and 0.025 or 0.525
local cEnd = isOutbound and 0.5 or 1.0
local localProg = enRoute > 0 and (1 - routeData._cruiseSecondsLeft / enRoute) or 1
routeData.Progress = cStart + math.clamp(localProg, 0, 1) * (cEnd - cStart)
routeData.Status = "En-Route"
if routeData._cruiseSecondsLeft <= 0 then
local nextPhase = isOutbound and "Landing (Destination)" or "Landing (Origin)"
routeData.Progress = cEnd
routeData.PhaseOverride = nextPhase
routeData._lastAnimPhase = nil
routeData._cruiseSecondsLeft = nil
end
continue
end
-- AT GATE: fire animation to client; client handles boarding via BoardPassengersFunc
if phaseName == "At Gate (Destination)" or phaseName == "At Gate (Origin)" then
PausedRoutes[routeId] = true
RouteAnimationRem:FireClient(player, routeData, nil, phaseName)
task.spawn(function()
task.wait(routeData.TurnaroundTime or 20)
if phaseName == "At Gate (Destination)" then
routeData.PhaseOverride = "Take Off (Destination)"
routeData.Progress = 0.5
else
routeData.PhaseOverride = "Take Off (Origin)"
routeData.Progress = 0
end
PausedRoutes[routeId] = nil
end)
continue
end
-- ANIMATED PHASES (Taxi, Take Off, Landing)
local PHASE_DURATIONS = {
["Taxi (Origin)"] = 30, ["Taxi (Destination)"] = 30,
["Push Back (Origin)"] = 8, ["Push Back (Destination)"] = 8,
["Take Off (Origin)"] = 60, ["Take Off (Destination)"] = 60,
["Landing (Destination)"] = 60,["Landing (Origin)"] = 60,
}
local TotalDuration = PHASE_DURATIONS[phaseName]
if TotalDuration then
PausedRoutes[routeId] = true
local taxidata = FlightPhase.CalcTaxiPoints(player, phaseName, routeData)
RouteAnimationRem:FireClient(player, routeData, nil, phaseName)
task.spawn(function()
task.wait(taxidata.TotalDuration or TotalDuration)
local nextPhase = NextPhaseAfterAnimation(phaseName, routeData)
if nextPhase ~= phaseName then
routeData.PhaseOverride = nextPhase
end
PausedRoutes[routeId] = nil
end)
continue
end
warn("[RouteSim] unhandled phase '", phaseName, "' for", routeId)
routeData.PhaseOverride = nil
routeData.Progress = 0
end
task.wait(1)
end
warn("[RouteSim] loop stopped for", player.Name)
end)
end)
game.Players.PlayerRemoving:Connect(function(player) -- LOOK HERE
local userId = player.UserId
local slots = PlayerData[userId]
print(PlayerData, slots)
if not slots then return end
for slot = 1, 5 do
local slotData = slots[slot]
if not slotData then continue end
print(slotData)
local ok, err = pcall(function()
saveSlot(player, slot, slotData)
end)
if ok then
print("[RouteSim] Saved slot", slot, "for", player.Name, slotData)
else
warn("[RouteSim] Failed to save slot", slot, ":", err)
end
end
-- Wait a bit for SetAsync to complete before clearing
task.defer(function()
PlayerData[userId] = nil
PlayerRouteManagers[userId] = nil
ActiveSlot[userId] = nil
end)
end)