-
-
Notifications
You must be signed in to change notification settings - Fork 142
feat: Save slots #692
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
feat: Save slots #692
Changes from all commits
1359c8b
9e4e13f
e78f956
fe93d2e
3972e17
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| 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 | ||
| ``` |
| 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" | ||
| } | ||
| } |
| 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" | ||
| } | ||
| } |
| 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, | ||
| 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() | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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() | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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> | ||
| 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 |
| 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 |
There was a problem hiding this comment.
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.