Seaside Open Source: My core systems and what I've learned

Five years ago, I created my first “popular” game. It was a mod of Andrew Bereza’s Miner’s Haven open source called “Miner’s Haven: Defiled”. This experience helped me to somewhat understand not only game ownership, but what it was like to develop on a larger scale project. It was my first foot in the door on real Roblox development.

A month ago I created Seaside, a cozy, retro RPG fishing game that touched the hearts of thousands. I curated a community of some of the most creative individuals I’ve ever met, and I learned what it was like to create something from scratch at that larger scale. I’d like to pay forward what Andrew has created and release an open source variant of some of Seaside’s core systems.

In this, you’ll find our ProfileStore data system, character setup, state machines, and some other systems that you can use for a good retro-style game. The animations will require spoofing, but everything else should work fine.

Similar to Miner’s Haven Open Source, this falls under the Apache Version 2 license, which means:

:white_check_mark: You may use materials from the source file, including source code, to create commercial projects with original intellectual property.

:white_check_mark: You may create and upload edits of this content if the brand property is not used commercially.

:x: You are prohibited from using the “Seaside” name and other brand property commercially without explicit written permission.

13 Likes

I have probably never seen such a well written, well organized and overall excellent place template as this. Even if it is just core’s this was very helpful (Especially the Profile Store).

I did have ONE tiny problem (I dont actually know what it is (I have never used data storing before, just learning about how to use it today) ).


This weird error (which, according to context). Shouldnt break anything. Keeps appearing no matter how many bug fixes I may make.

Im using your serializer, Datareplica and dataservice code. BUT, in a Knit Framework Format.

DataService
--//Services
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local ServerStorage = game:GetService("ServerStorage")
local InsertService = game:GetService("InsertService")
local MarketplaceService = game:GetService("MarketplaceService")
local Players = game:GetService("Players")
local RunService = game:GetService("RunService")

--//Modules
local ClientModules = ReplicatedStorage.Modules
local ServerModules = ServerStorage.Modules

local Serializer = require(ServerModules.Serializer)
local ProfileStore = require(ServerModules.ProfileStore)
local DataReplica = ServerModules.DataReplica

local Event = require(ClientModules.Utility.Event)
local TableUtil = require(ReplicatedStorage.Modules.Utility.TableUtil)

--//Template
local DataTemplate = {

	DataVersion = 1,
	
	Purchases = {
		GamePasses = {},
		Products = {}
	},
	
}

--//Variables
local ServerUpdateTime = tick()

--//Main 
local Knit = require(ReplicatedStorage.Packages.Knit)

--//Module
local DataService = Knit.CreateService  {
	Name = "DataService",
	
	NoWipe = {},
	ChangedSignals = {},
	DebugData = {},
	
	Profiles = {} :: {[Player] : typeof(DataStore:StartSessionAsync())},
	PlayerLoaded = Event.new() :: Event.Event<Player>,
	
	Config = {
		Tag = "PlayerData",
		DataVersion = 1,
		KickOnDataFailure = true,
		Mock = RunService:IsStudio() and (ReplicatedStorage:GetAttribute("DisableMock") ~= true),
	},
	
	Client = {
		SendDataSignal = Knit.CreateSignal(),
		GetDataSignal = Knit.CreateSignal(),
	},
	
}

--//Global Session
local DataStore = ProfileStore.new(DataService.Config.Tag, DataTemplate)

--//Start
function DataService:KnitStart()

	local success, gameVersion = pcall(function() 
		return InsertService:GetLatestAssetVersionAsync(game.PlaceId)
	end)

	if not success then
		gameVersion = 0
	end
	
	task.spawn(function()
		local GamePlaceInfo = MarketplaceService:GetProductInfo(game.PlaceId, Enum.InfoType.Asset)
		if not GamePlaceInfo then return end

		ServerUpdateTime = DateTime.fromIsoDate(GamePlaceInfo.Updated).UnixTimestamp
	end)
	
	if DataService.Config.Mock then
		DataStore = DataStore.Mock
	end
	
	Players.PlayerAdded:Connect(DataService.PlayerAdded)
	Players.PlayerRemoving:Connect(DataService.PlayerRemoving)

	for _, player in Players:GetPlayers() do
		DataService.PlayerAdded(player)
	end
	
end

--//Methods
function DataService.SanitizeData(Player: Player, Data: typeof(DataTemplate))

end

function DataService:GetDataChangedSignal(Player: Player)
	return DataService.ChangedSignals[Player]
end

function DataService.PlayerAdded(Player: Player)
	DataService.ChangedSignals[Player] = Event.new()
	local profile = DataStore:StartSessionAsync(`{Player.UserId}`, {
		Cancel = function()
			return Player.Parent ~= Players
		end
	})

	if not profile then
		Player:Kick("Failed to load data. Please relog.")
		return
	end

	profile:AddUserId(Player.UserId)
	profile:Reconcile()

	profile.OnSessionEnd:Connect(function()
		DataService.Profiles[Player] = nil
	end)

	if Player.Parent ~= Players then 
		profile:EndSession()
		return 
	end

	DataService.Profiles[Player] = profile


	local dataReplica = DataReplica:Clone()
	dataReplica.Parent = Player

	local replica = require(dataReplica)
	replica.Player = Player

	local Data = DataService.Profiles[Player].Data

	if Data.DataVersion ~= DataService.Config.DataVersion then
		for key, value in TableUtil.DeepCopy(DataTemplate) do
			if table.find(DataService.NoWipe, key) then
				continue
			end

			Data[key] = value
		end
	end

	DataService.SanitizeData(Player,Data)

	if (RunService:IsStudio() or game.PlaceId == 92564039248116) and DataService.Config.Mock then
		for key, value in DataService.DebugData do
			Data[key] = value
		end
	end

	task.defer(function()
		for key, value in Data do
			replica[key] = value
		end
	end)



	Player:SetAttribute("DataLoaded", true)
	DataService.PlayerLoaded:Fire(Player) 

	local leaderstats = Instance.new("Folder")
	leaderstats.Name = "leaderstats"
	leaderstats.Parent = Player
end

function DataService.PlayerRemoving(Player: Player)
	local profile = DataService.Profiles[Player]
	if not profile then return end

	DataService.ChangedSignals[Player] = nil

	local data = profile.Data

	profile:EndSession()
end

function DataService:GetData(Player: Player) : (typeof(DataTemplate))?
	while not Player:GetAttribute("DataLoaded") do
		if not Player:IsDescendantOf(Players) then return end
		task.wait()
	end

	local profile = DataService.Profiles[Player]
	if not profile then return end

	local data = profile.Data
	while not data do
		if not Player:IsDescendantOf(Players) then return end

		task.wait()
		data = profile.Data
	end

	return setmetatable({} :: {[any]: any}, {
		__index = function(_, key)
			return data[key]
		end,

		__newindex = function(_, key, value)
			error(`Cannot set data from GetData. Please use UpdateData instead.`)
		end
	}) :: never
end

function DataService:UpdateData(Player: Player, Key: string, Callback: (Data: {[number | string]: any}) -> ())
	local Profile = DataService.Profiles[Player]
	if not Profile then return end

	local Data = Profile.Data
	local success, result = pcall(Callback, Data[Key])

	if not success then
		warn('UpdateData: ' .. result)
		return
	end

	local newData = result

	if newData == nil then
		warn(`UpdateData: {Key} did not return a value. Was this intentional?`)
	end

	Data[Key] = newData

	DataService.ChangedSignals[Player]:Fire()

	local DataReplica = Player:FindFirstChild("DataReplica")
	if not DataReplica then return end

	local replica = require(DataReplica)
	replica[Key] = Data[Key]
end

return DataService
DataReplica
local DataReplica = {}

--//Services
local RunService = game:GetService("RunService")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local CollectionService = game:GetService("CollectionService")
local Players = game:GetService("Players")

--//Events
local Events = ReplicatedStorage:WaitForChild("Events")
local DataEvents = Events.Data
local ReplicateData = DataEvents:WaitForChild("ReplicateData")
local GetDataReplica = DataEvents:WaitForChild("GetDataReplica")

local Proxy = setmetatable({}, {	
	__newindex = function(tbl, index, value)
		if RunService:IsClient() then return end
		
		DataReplica[index] = value

		if index == "Player" then 
			DataReplica.Player = value
			return 
		end
		
		ReplicateData:FireClient(tbl.Player, index, value)
	end,
	
	__tostring = function(tbl)
		return tostring(DataReplica)
	end,

	__index = function(tbl, index)
		return DataReplica[index]
	end
})

if RunService:IsClient() then
	DataReplica = GetDataReplica:InvokeServer()
	ReplicateData.OnClientEvent:Connect(function(index, value)
		DataReplica[index] = value
	
		local player = Players.LocalPlayer
		local PlayerScripts = player:WaitForChild("PlayerScripts")
	end)
else
	GetDataReplica.OnServerInvoke = function(Player)
		return DataReplica
	end
end

return Proxy

(Serializer was left as is.)
(“Events”) folder
image

Any idea on how to fix it?
Thank you for this resource and for the help!

I’m not sure this is a fixable warning, I believe it stems from ProfileStore.

Probably a misuse?.

It doesnt happen in the “Seaside template” rbxl. Im not sure whats going on.