Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 60 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

23 changes: 23 additions & 0 deletions src/saveslot/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
## SaveSlot

<div align="center">
<a href="http://quenty.github.io/NevermoreEngine/">
<img src="https://github.com/Quenty/NevermoreEngine/actions/workflows/docs.yml/badge.svg" alt="Documentation status" />
</a>
<a href="https://discord.gg/mhtGUS8">
<img src="https://img.shields.io/discord/385151591524597761?color=5865F2&label=discord&logo=discord&logoColor=white" alt="Discord" />
</a>
<a href="https://github.com/Quenty/NevermoreEngine/actions">
<img src="https://github.com/Quenty/NevermoreEngine/actions/workflows/build.yml/badge.svg" alt="Build and release status" />
</a>
</div>

PlayerDataStoreService wrapper for save slots

<div align="center"><a href="https://quenty.github.io/NevermoreEngine/api/SaveSlotUtils">View docs →</a></div>

## Installation

```
npm install @quenty/saveslot --save
```
14 changes: 14 additions & 0 deletions src/saveslot/default.project.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"name": "saveslot",
"globIgnorePaths": [
"**/.package-lock.json",
"**/.pnpm",
"**/.pnpm-workspace-state-v1.json",
"**/.modules.yaml",
"**/.ignored",
"**/.ignored_*"
],
"tree": {
"$path": "src"
}
}
54 changes: 54 additions & 0 deletions src/saveslot/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
{
"name": "@quenty/saveslot",
"version": "1.0.0",
"description": "PlayerDataStoreService wrapper for save slots",
"keywords": [
"Roblox",
"Nevermore",
"Lua",
"saveslot"
],
"bugs": {
"url": "https://github.com/Quenty/NevermoreEngine/issues"
},
"repository": {
"type": "git",
"url": "https://github.com/Quenty/NevermoreEngine.git",
"directory": "src/saveslot/"
},
"funding": {
"type": "patreon",
"url": "https://www.patreon.com/quenty"
},
"license": "MIT",
"scripts": {
"preinstall": "npx only-allow pnpm"
},
"contributors": [
"Quenty"
],
"dependencies": {
"@quenty/adorneedata": "workspace:*",
"@quenty/baseobject": "workspace:*",
"@quenty/binder": "workspace:*",
"@quenty/brio": "workspace:*",
"@quenty/cmdrservice": "workspace:*",
"@quenty/datastore": "workspace:*",
"@quenty/instanceutils": "workspace:*",
"@quenty/loader": "workspace:*",
"@quenty/maid": "workspace:*",
"@quenty/playerbinder": "workspace:*",
"@quenty/promise": "workspace:*",
"@quenty/propertyvalue": "workspace:*",
"@quenty/remoting": "workspace:*",
"@quenty/rx": "workspace:*",
"@quenty/servicebag": "workspace:*",
"@quenty/signal": "workspace:*",
"@quenty/table": "workspace:*",
"@quenty/tie": "workspace:*",
"@quenty/valueobject": "workspace:*"
},
"publishConfig": {
"access": "public"
}
}
126 changes: 126 additions & 0 deletions src/saveslot/src/Client/Binders/HasSaveSlotsClient.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
--!strict
--[=[
@class HasSaveSlotsClient
]=]

local require = require(script.Parent.loader).load(script)

local Players = game:GetService("Players")

local Binder = require("Binder")
local HasSaveSlotsBase = require("HasSaveSlotsBase")
local HasSaveSlotsInterface = require("HasSaveSlotsInterface")
local Promise = require("Promise")
local Remoting = require("Remoting")
local SaveSlotData = require("SaveSlotData")
local ServiceBag = require("ServiceBag")

local HasSaveSlotsClient = setmetatable({}, HasSaveSlotsBase)
HasSaveSlotsClient.ClassName = "HasSaveSlotsClient"
HasSaveSlotsClient.__index = HasSaveSlotsClient

export type HasSaveSlotsClient =
typeof(setmetatable(
{} :: {
_obj: Player,
_serviceBag: ServiceBag.ServiceBag,
_remoting: any,
},
{} :: typeof({ __index = HasSaveSlotsClient })
))
& HasSaveSlotsBase.HasSaveSlotsBase

function HasSaveSlotsClient.new(player: Player, serviceBag: ServiceBag.ServiceBag): HasSaveSlotsClient
if player ~= Players.LocalPlayer then
return nil :: any
end

local self: HasSaveSlotsClient = setmetatable(HasSaveSlotsBase.new(player, serviceBag) :: any, HasSaveSlotsClient)

self._serviceBag = assert(serviceBag, "No serviceBag")

self._remoting = self._maid:Add(Remoting.Client.new(self._obj, "HasSaveSlots"))

self._maid:GiveTask(HasSaveSlotsInterface.Client:Implement(self._obj, self))

return self
end

--[=[
Returns whether the slot with the given ID exists
]=]
function HasSaveSlotsClient.PromiseHasSlot(self: HasSaveSlotsClient, slotId: string?): Promise.Promise<boolean>
return self._remoting.PromiseHasSlot:PromiseInvokeServer(slotId)
end

--[=[
Selects the slot with the given ID
]=]
function HasSaveSlotsClient.PromiseSelectSlot(self: HasSaveSlotsClient, slotId: string): Promise.Promise<any>
return self._remoting.PromiseSelectSlot:PromiseInvokeServer(slotId)
end

--[=[
Creates a slot at the given index
]=]
function HasSaveSlotsClient.PromiseCreateSlot(
self: HasSaveSlotsClient,
slotIndex: number,
metadata: SaveSlotData.SaveSlotMetadata?
): Promise.Promise<any>
return self._remoting.PromiseCreateSlot:PromiseInvokeServer(slotIndex, metadata)
end

--[=[
Deletes the slot with the given ID
]=]
function HasSaveSlotsClient.PromiseDeleteSlot(self: HasSaveSlotsClient, slotId: string): Promise.Promise<any>
return self._remoting.PromiseDeleteSlot:PromiseInvokeServer(slotId)
end

--[=[
Sets the metadata for the slot with the given ID
]=]
function HasSaveSlotsClient.PromiseSetSlotMetadata(
self: HasSaveSlotsClient,
slotId: string,
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typically I'd type slotId to make future refactors safer but it's ok as-is too.

data: SaveSlotData.SaveSlotMetadata
): Promise.Promise<any>
return self._remoting.PromiseSetSlotMetadata:PromiseInvokeServer(slotId, data)
end

--[=[
Gets the metadata for the slot with the given ID
]=]
function HasSaveSlotsClient.PromiseGetSlotMetadata(
self: HasSaveSlotsClient,
slotId: string
): Promise.Promise<SaveSlotData.SaveSlotMetadata>
return self._remoting.PromiseGetSlotMetadata:PromiseInvokeServer(slotId)
end

--[=[
Gets the last active slot ID
]=]
function HasSaveSlotsClient.PromiseLastActiveSlotId(self: HasSaveSlotsClient): Promise.Promise<string?>
return self._remoting.PromiseLastActiveSlotId:PromiseInvokeServer()
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might be worth some fancier cache management, potentially internally a Boolean attribute for loaded and an attribute for the slot id, so that we're effectively cached on the client instead of requesting server a lot.

This will happen on initial game load, potentially many systems may promise the slot id.

end

--[=[
Returns the slot ID from the given index
]=]
function HasSaveSlotsClient.PromiseSlotIdFromIndex(
self: HasSaveSlotsClient,
slotIndex: number
): Promise.Promise<string?>
return self._remoting.PromiseSlotIdFromIndex:PromiseInvokeServer(slotIndex)
end

--[=[
Refreshes the active slot summary
]=]
function HasSaveSlotsClient.PromiseRefreshActiveSlotSummary(self: HasSaveSlotsClient): Promise.Promise<any>
return self._remoting.PromiseRefreshActiveSlotSummary:PromiseInvokeServer()
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Preferably we don't have to leak this state externally but it's ok as-is and I don't see a clean way right now to avoid this.

end

return Binder.new("HasSaveSlots", HasSaveSlotsClient :: any) :: Binder.Binder<HasSaveSlotsClient>
49 changes: 49 additions & 0 deletions src/saveslot/src/Client/Cmdr/SaveSlotCmdrServiceClient.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
--!strict
--[=[
@class SaveSlotCmdrServiceClient
]=]

local require = require(script.Parent.loader).load(script)

local CmdrServiceClient = require("CmdrServiceClient")
local Maid = require("Maid")
local SaveSlotCmdrUtils = require("SaveSlotCmdrUtils")
local SaveSlotDataService = require("SaveSlotDataService")
local ServiceBag = require("ServiceBag")

local SaveSlotCmdrServiceClient = {}
SaveSlotCmdrServiceClient.ServiceName = "SaveSlotCmdrServiceClient"

export type SaveSlotCmdrServiceClient = typeof(setmetatable(
{} :: {
_serviceBag: ServiceBag.ServiceBag,
_maid: Maid.Maid,
_cmdrServiceClient: any,
_saveSlotDataService: any,
},
{} :: typeof({ __index = SaveSlotCmdrServiceClient })
))

function SaveSlotCmdrServiceClient.Init(self: SaveSlotCmdrServiceClient, serviceBag: ServiceBag.ServiceBag)
assert(not (self :: any)._serviceBag, "Already initialized")
self._serviceBag = assert(serviceBag, "No serviceBag")
self._maid = Maid.new()

-- External
self._cmdrServiceClient = self._serviceBag:GetService(CmdrServiceClient)

-- Internal
self._saveSlotDataService = self._serviceBag:GetService(SaveSlotDataService)
end

function SaveSlotCmdrServiceClient.Start(self: SaveSlotCmdrServiceClient)
self._maid:GivePromise(self._cmdrServiceClient:PromiseCmdr()):Then(function(cmdr)
SaveSlotCmdrUtils.registerSlotIndexType(cmdr, self._saveSlotDataService)
end)
end

function SaveSlotCmdrServiceClient.Destroy(self: SaveSlotCmdrServiceClient): ()
self._maid:Destroy()
end

return SaveSlotCmdrServiceClient
55 changes: 55 additions & 0 deletions src/saveslot/src/Client/SaveSlotServiceClient.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
--!strict
--[=[
@class SaveSlotServiceClient
]=]

local require = require(script.Parent.loader).load(script)

local ReplicatedStorage = game:GetService("ReplicatedStorage")

local Maid = require("Maid")
local Remoting = require("Remoting")
local ServiceBag = require("ServiceBag")

local SaveSlotServiceClient = {}
SaveSlotServiceClient.ServiceName = "SaveSlotServiceClient"

export type SaveSlotServiceClient = typeof(setmetatable(
{} :: {
_serviceBag: ServiceBag.ServiceBag,
_maid: Maid.Maid,
_remoting: any,
},
{} :: typeof({ __index = SaveSlotServiceClient })
))

function SaveSlotServiceClient.Init(self: SaveSlotServiceClient, serviceBag: ServiceBag.ServiceBag)
assert(not (self :: any)._serviceBag, "Already initialized")
self._serviceBag = assert(serviceBag, "No serviceBag")
self._maid = Maid.new()

-- Internal
self._serviceBag:GetService(require("SaveSlotCmdrServiceClient"))
self._serviceBag:GetService(require("SaveSlotDataService"))

-- Binders
self._serviceBag:GetService(require("HasSaveSlotsClient"))

self._remoting = self._maid:Add(Remoting.Client.new(ReplicatedStorage, "SaveSlotService"))
end

--[=[
Returns whether explicit slot selection is required
]=]
function SaveSlotServiceClient.GetExplicitSelectionRequiredAsync(self: SaveSlotServiceClient): boolean
return self._remoting.GetExplicitSelectionRequired:InvokeServer()
end

--[=[
Destroys the service
]=]
function SaveSlotServiceClient.Destroy(self: SaveSlotServiceClient): ()
self._maid:Destroy()
end

return SaveSlotServiceClient
Loading
Loading