To clarify my situation:
- I am using Places inside one Experience, not different Experiences.
- I have a Lobby Place where players select their towers using a GUI (a
LoadoutFrame
where each tower is shown as a cloned template).
- When players step into a lift, they are teleported using a
TeleporterHandlerModule
to the Match Place (which is part of the same experience).
Now here’s my goal:
When the player gets teleported to the Match Place, I want their LoadoutFrame
to already contain the exact same towers they selected in the lobby — same order, same amount, depending on whether they own the gamepass (3 or 5 towers).
One more quick question:
In the Lobby, I have a DataStoreHandler
ModuleScript that handles saving the loadout (as a table) to the DataStore. Do I need to duplicate or copy this same DataStoreHandler into the Match Place too?
Or should I just create a separate script in the Match Place that loads the data again using the same keys?
Basically:
Does the DataStoreHandler
module from the Lobby need to exist in the Match Place too, or can I just write a new script in the Match Place that uses the same DataStore logic?
This is my Lobby DataStoreHandler:
local DataStoreService = game:GetService("DataStoreService")
local Players = game:GetService("Players")
local DataStore = DataStoreService:GetDataStore("TowerDefensePlayerData")
local DataStoreHandler = {}
DataStoreHandler.__index = DataStoreHandler
local DEFAULT_DATA = {
Cash = 0,
Crystals = 0,
DailyLoginDay = 0,
LastLoginDate = "1970-01-01",
TotalPlaytime = 0,
DailyPlaytime = 0,
LastPlayDate = "1970-01-01",
LastClaimTimestamp = 0,
OwnedTowers = {},
Loadout = {},
BuyButtonStates = {},
}
local playerDataCache = {}
local lastSaveTimes = {}
local SAVE_INTERVAL = 30
local MAX_SAVE_ATTEMPTS = 3
local SAVE_DELAY = 1
local function getDateString()
local now = os.time()
local utcDate = os.date("!*t", now)
return string.format("%04d-%02d-%02d", utcDate.year, utcDate.month, utcDate.day)
end
function DataStoreHandler.LoadPlayerData(userId)
if playerDataCache[userId] then
return playerDataCache[userId]
end
local success, data = pcall(function()
return DataStore:GetAsync("Player_"..userId)
end)
if success and data then
for k,v in pairs(DEFAULT_DATA) do
if data[k] == nil then
data[k] = v
end
end
playerDataCache[userId] = data
lastSaveTimes[userId] = os.time()
return data
else
local defaultData = table.clone(DEFAULT_DATA)
playerDataCache[userId] = defaultData
lastSaveTimes[userId] = os.time()
return defaultData
end
end
function DataStoreHandler.SavePlayerData(userId, data, forceImmediate)
if not data then return end
playerDataCache[userId] = data
local currentTime = os.time()
local lastSave = lastSaveTimes[userId] or 0
if not forceImmediate and (currentTime - lastSave) < SAVE_INTERVAL then
return
end
local saveData = {
Cash = math.max(0, math.floor(data.Cash or 0)),
Crystals = math.max(0, math.floor(data.Crystals or 0)),
DailyLoginDay = math.clamp(data.DailyLoginDay or 0, 0, 7),
LastLoginDate = tostring(data.LastLoginDate or "1970-01-01"),
TotalPlaytime = math.max(0, math.floor(data.TotalPlaytime or 0)),
DailyPlaytime = math.max(0, math.floor(data.DailyPlaytime or 0)),
LastPlayDate = tostring(data.LastPlayDate or "1970-01-01"),
LastClaimTimestamp = math.max(0, math.floor(data.LastClaimTimestamp or 0)),
OwnedTowers = data.OwnedTowers or {},
Loadout = data.Loadout or {},
BuyButtonStates = data.BuyButtonStates or {},
}
local attempts = 0
local success = false
while attempts < MAX_SAVE_ATTEMPTS and not success do
attempts = attempts + 1
if attempts > 1 then
wait(SAVE_DELAY * attempts)
end
success = pcall(function()
DataStore:SetAsync("Player_"..userId, saveData)
end)
if success then
lastSaveTimes[userId] = currentTime
elseif attempts == MAX_SAVE_ATTEMPTS then
warn("Failed to save data for userId "..userId.." after "..MAX_SAVE_ATTEMPTS.." attempts")
end
end
end
function DataStoreHandler.GetPlayerData(userId)
return playerDataCache[userId]
end
function DataStoreHandler.UpdatePlayerData(userId, updateFunction)
local data = playerDataCache[userId]
if data then
updateFunction(data)
DataStoreHandler.SavePlayerData(userId, data, false)
end
end
function DataStoreHandler.SaveAllPlayers()
for userId, data in pairs(playerDataCache) do
spawn(function()
DataStoreHandler.SavePlayerData(userId, data, true)
end)
end
end
function DataStoreHandler.PlayerLeaving(userId)
local data = playerDataCache[userId]
if data then
DataStoreHandler.SavePlayerData(userId, data, true)
spawn(function()
wait(2)
playerDataCache[userId] = nil
lastSaveTimes[userId] = nil
end)
end
end
spawn(function()
while true do
wait(SAVE_INTERVAL)
for userId, data in pairs(playerDataCache) do
local player = Players:GetPlayerByUserId(userId)
if player then
DataStoreHandler.SavePlayerData(userId, data, false)
wait(0.1)
end
end
end
end)
game:BindToClose(function()
print("Server shutting down, saving all player data...")
local saveCount = 0
for userId, data in pairs(playerDataCache) do
spawn(function()
DataStoreHandler.SavePlayerData(userId, data, true)
saveCount = saveCount + 1
end)
wait(0.1)
end
local startTime = tick()
while saveCount < #playerDataCache and (tick() - startTime) < 25 do
wait(0.1)
end
print("Saved data for "..saveCount.." players")
end)
return DataStoreHandler
And this is the LoadoutManagerModule:
local LoadoutManager = {}
LoadoutManager.PlayerLoadouts = {}
function LoadoutManager:GetSlotCount(hasGamepass)
if hasGamepass == nil then
warn("hasGamepass is nil in GetSlotCount")
return 3
end
if hasGamepass then
return 5
else
return 3
end
end
function LoadoutManager:IsTowerAlreadyPlaced(playerLoadout, towerName)
if not playerLoadout then
return false
end
for _, tower in pairs(playerLoadout) do
if tower == towerName then
return true
end
end
return false
end
function LoadoutManager:InitializeLoadout(playerUserId, slotCount)
local loadout = {}
for i = 1, slotCount do
loadout[i] = nil
end
LoadoutManager.PlayerLoadouts[playerUserId] = loadout
return loadout
end
function LoadoutManager:SetTowerOnSlot(playerUserId, slotNumber, towerName)
local loadout = LoadoutManager.PlayerLoadouts[playerUserId]
if not loadout then
warn("Loadout not initialized for player "..playerUserId)
return false
end
if self:IsTowerAlreadyPlaced(loadout, towerName) then
return false
end
loadout[slotNumber] = towerName
return true
end
function LoadoutManager:GetTowerOnSlot(playerUserId, slotNumber)
local loadout = LoadoutManager.PlayerLoadouts[playerUserId]
if loadout then
return loadout[slotNumber]
end
return nil
end
function LoadoutManager:PlaceTowerOnSlot(selectedTowersFolder, slotNumber, towerName, TowersFolder)
local template = selectedTowersFolder:FindFirstChild("Template")
if not template then
warn("Template not found in SelectedTowers folder")
return
end
local slotName = "Slot"..slotNumber
local existingSlot = selectedTowersFolder:FindFirstChild(slotName)
if existingSlot then
existingSlot:Destroy()
end
local clone = template:Clone()
clone.Name = slotName
clone.Visible = true
clone.Parent = selectedTowersFolder
local startX = 0.033
local startY = 0.07
local stepX = 0.205
clone.Position = UDim2.new(startX + (slotNumber - 1) * stepX, 0, startY, 0)
clone.Size = UDim2.new(0, 85, 0, 85)
local towerNameLabel = clone:FindFirstChild("TowerName")
if towerNameLabel then
towerNameLabel.Text = towerName
end
local viewportFrame = clone:FindFirstChild("ViewportFrame")
if viewportFrame then
local towerModel = TowersFolder:FindFirstChild(towerName)
if towerModel then
viewportFrame:ClearAllChildren()
local cloneModel = towerModel:Clone()
cloneModel.Parent = viewportFrame
local camera = Instance.new("Camera")
camera.Parent = viewportFrame
viewportFrame.CurrentCamera = camera
local modelCFrame, modelSize = cloneModel:GetBoundingBox()
local distance = math.max(modelSize.X, modelSize.Y, modelSize.Z) * 1.2
local cameraPosition = modelCFrame.Position + Vector3.new(0, distance * 0.5, distance * 0.8)
camera.CFrame = CFrame.new(cameraPosition, modelCFrame.Position)
end
end
end
return LoadoutManager