@quenty/settings 4.12.0 → 4.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -3,6 +3,17 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ # [4.13.0](https://github.com/Quenty/NevermoreEngine/compare/@quenty/settings@4.12.0...@quenty/settings@4.13.0) (2023-01-17)
7
+
8
+
9
+ ### Bug Fixes
10
+
11
+ * Ensure that Settings can't contain invalid UTF8 characters and document settings package better ([cbf1406](https://github.com/Quenty/NevermoreEngine/commit/cbf140620795846daf35ce6945bcdf801e1156f9))
12
+
13
+
14
+
15
+
16
+
6
17
  # [4.12.0](https://github.com/Quenty/NevermoreEngine/compare/@quenty/settings@4.11.1...@quenty/settings@4.12.0) (2023-01-11)
7
18
 
8
19
  **Note:** Version bump only for package @quenty/settings
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quenty/settings",
3
- "version": "4.12.0",
3
+ "version": "4.13.0",
4
4
  "description": "Centralized player settings service",
5
5
  "keywords": [
6
6
  "Roblox",
@@ -25,10 +25,10 @@
25
25
  "Quenty"
26
26
  ],
27
27
  "dependencies": {
28
- "@quenty/attributeutils": "^8.4.0",
28
+ "@quenty/attributeutils": "^8.5.0",
29
29
  "@quenty/baseobject": "^6.0.1",
30
30
  "@quenty/binder": "^8.6.0",
31
- "@quenty/datastore": "^7.4.1",
31
+ "@quenty/datastore": "^7.5.0",
32
32
  "@quenty/enumutils": "^3.0.0",
33
33
  "@quenty/jsonutils": "^6.0.1",
34
34
  "@quenty/loader": "^6.0.1",
@@ -51,5 +51,5 @@
51
51
  "publishConfig": {
52
52
  "access": "public"
53
53
  },
54
- "gitHead": "db2db00ab4e24a3eab0449b77a00bee6d91f2755"
54
+ "gitHead": "a89a3f27cad25767cd645b91cdc2a2881afeecd5"
55
55
  }
@@ -1,4 +1,8 @@
1
1
  --[=[
2
+ A player's current settings. Handles replication back to the server
3
+ when a setting changes. See [PlayerSettingsBase].
4
+
5
+ @client
2
6
  @class PlayerSettingsClient
3
7
  ]=]
4
8
 
@@ -6,14 +10,15 @@ local require = require(script.Parent.loader).load(script)
6
10
 
7
11
  local Players = game:GetService("Players")
8
12
 
13
+ local DataStoreStringUtils = require("DataStoreStringUtils")
14
+ local Maid = require("Maid")
15
+ local Observable = require("Observable")
9
16
  local PlayerSettingsBase = require("PlayerSettingsBase")
10
17
  local PlayerSettingsConstants = require("PlayerSettingsConstants")
11
- local RemoteFunctionUtils = require("RemoteFunctionUtils")
12
18
  local PlayerSettingsUtils = require("PlayerSettingsUtils")
13
- local ThrottledFunction = require("ThrottledFunction")
14
- local Maid = require("Maid")
19
+ local RemoteFunctionUtils = require("RemoteFunctionUtils")
15
20
  local Symbol = require("Symbol")
16
- local Observable = require("Observable")
21
+ local ThrottledFunction = require("ThrottledFunction")
17
22
  local ValueObject = require("ValueObject")
18
23
 
19
24
  local UNSET_VALUE = Symbol.named("unsetValue")
@@ -24,8 +29,15 @@ PlayerSettingsClient.__index = PlayerSettingsClient
24
29
 
25
30
  require("PromiseRemoteFunctionMixin"):Add(PlayerSettingsClient, PlayerSettingsConstants.REMOTE_FUNCTION_NAME)
26
31
 
27
- function PlayerSettingsClient.new(obj, serviceBag)
28
- local self = setmetatable(PlayerSettingsBase.new(obj, serviceBag), PlayerSettingsClient)
32
+ --[=[
33
+ See [SettingsBindersClient] and [SettingsServiceClient] on how to properly use this class.
34
+
35
+ @param folder Folder
36
+ @param serviceBag ServiceBag
37
+ @return PlayerSettingsClient
38
+ ]=]
39
+ function PlayerSettingsClient.new(folder, serviceBag)
40
+ local self = setmetatable(PlayerSettingsBase.new(folder, serviceBag), PlayerSettingsClient)
29
41
 
30
42
  if self:GetPlayer() == Players.LocalPlayer then
31
43
  self._toReplicate = nil
@@ -48,6 +60,13 @@ function PlayerSettingsClient.new(obj, serviceBag)
48
60
  return self
49
61
  end
50
62
 
63
+ --[=[
64
+ Gets a settings value
65
+
66
+ @param settingName string
67
+ @param defaultValue T
68
+ @return T
69
+ ]=]
51
70
  function PlayerSettingsClient:GetValue(settingName, defaultValue)
52
71
  assert(type(settingName) == "string", "Bad settingName")
53
72
 
@@ -63,6 +82,13 @@ function PlayerSettingsClient:GetValue(settingName, defaultValue)
63
82
  return getmetatable(PlayerSettingsClient).GetValue(self, settingName, defaultValue)
64
83
  end
65
84
 
85
+ --[=[
86
+ Observes a settings value.
87
+
88
+ @param settingName string
89
+ @param defaultValue T
90
+ @return Observable<T>
91
+ ]=]
66
92
  function PlayerSettingsClient:ObserveValue(settingName, defaultValue)
67
93
  assert(type(settingName) == "string", "Bad settingName")
68
94
 
@@ -128,9 +154,24 @@ function PlayerSettingsClient:ObserveValue(settingName, defaultValue)
128
154
  end)
129
155
  end
130
156
 
157
+ --[=[
158
+ Sets a settings value and replicates the value eventually (in a de-duplicated manner).
159
+
160
+ @param settingName string
161
+ @param value T
162
+ ]=]
131
163
  function PlayerSettingsClient:SetValue(settingName, value)
132
164
  assert(type(settingName) == "string", "Bad settingName")
133
165
  assert(self:GetPlayer() == Players.LocalPlayer, "Cannot set settings of another player")
166
+ assert(DataStoreStringUtils.isValidUTF8(settingName), "Bad settingName")
167
+
168
+ if type(value) == "string" then
169
+ assert(DataStoreStringUtils.isValidUTF8(value), "Invalid string")
170
+
171
+ if (#value + #settingName) > PlayerSettingsConstants.MAX_SETTINGS_LENGTH then
172
+ error(string.format("[PlayerSettingsClient.SetValue] - Setting is too long for %q", settingName))
173
+ end
174
+ end
134
175
 
135
176
  local queueReplication = false
136
177
  if not self._toReplicate then
@@ -1,4 +1,8 @@
1
1
  --[=[
2
+ Provides access to settings on the client. See [SettingDefinition] which should
3
+ register settings on the server. See [SettingsService] for server component.
4
+
5
+ @client
2
6
  @class SettingsServiceClient
3
7
  ]=]
4
8
 
@@ -11,6 +15,11 @@ local Rx = require("Rx")
11
15
 
12
16
  local SettingsServiceClient = {}
13
17
 
18
+ --[=[
19
+ Initializes the setting service. Should be done via ServiceBag.
20
+
21
+ @param serviceBag ServiceBag
22
+ ]=]
14
23
  function SettingsServiceClient:Init(serviceBag)
15
24
  assert(not self._serviceBag, "Already initialized")
16
25
  self._serviceBag = assert(serviceBag, "No serviceBag")
@@ -20,40 +29,85 @@ function SettingsServiceClient:Init(serviceBag)
20
29
  self._binders = self._serviceBag:GetService(require("SettingsBindersClient"))
21
30
  end
22
31
 
32
+ --[=[
33
+ Gets the local player settings
34
+ @return PlayerSettingsClient | nil
35
+ ]=]
23
36
  function SettingsServiceClient:GetLocalPlayerSettings()
24
37
  return self:GetPlayerSettings(Players.LocalPlayer)
25
38
  end
26
39
 
40
+ --[=[
41
+ Observes the local player settings in a brio
42
+
43
+ @return Observable<Brio<PlayerSettingsClient>>
44
+ ]=]
27
45
  function SettingsServiceClient:ObserveLocalPlayerSettingsBrio()
28
46
  return self:ObservePlayerSettingsBrio(Players.LocalPlayer)
29
47
  end
30
48
 
49
+ --[=[
50
+ Observes the local player settings
51
+
52
+ @return Observable<PlayerSettingsClient | nil>
53
+ ]=]
31
54
  function SettingsServiceClient:ObserveLocalPlayerSettings()
32
55
  return self:ObservePlayerSettings(Players.LocalPlayer)
33
56
  end
34
57
 
58
+ --[=[
59
+ Promises the local player settings
60
+
61
+ @param cancelToken CancellationToken
62
+ @return Promise<PlayerSettingsClient>
63
+ ]=]
35
64
  function SettingsServiceClient:PromiseLocalPlayerSettings(cancelToken)
36
65
  return self:PromisePlayerSettings(Players.LocalPlayer, cancelToken)
37
66
  end
38
67
 
68
+ --[=[
69
+ Observes the player settings
70
+
71
+ @param player Player
72
+ @return Observable<PlayerSettingsClient | nil>
73
+ ]=]
39
74
  function SettingsServiceClient:ObservePlayerSettings(player)
40
75
  assert(typeof(player) == "Instance" and player:IsA("Player"), "Bad player")
41
76
 
42
77
  return PlayerSettingsUtils.observePlayerSettings(self._binders.PlayerSettings, player)
43
78
  end
44
79
 
80
+ --[=[
81
+ Observes the player settings in a brio
82
+
83
+ @param player Player
84
+ @return Observable<Brio<PlayerSettingsClient>>
85
+ ]=]
45
86
  function SettingsServiceClient:ObservePlayerSettingsBrio(player)
46
87
  assert(typeof(player) == "Instance" and player:IsA("Player"), "Bad player")
47
88
 
48
89
  return PlayerSettingsUtils.observePlayerSettingsBrio(self._binders.PlayerSettings, player)
49
90
  end
50
91
 
92
+ --[=[
93
+ Gets a player's settings
94
+
95
+ @param player Player
96
+ @return PlayerSettingsClient | nil
97
+ ]=]
51
98
  function SettingsServiceClient:GetPlayerSettings(player)
52
99
  assert(typeof(player) == "Instance" and player:IsA("Player"), "Bad player")
53
100
 
54
101
  return PlayerSettingsUtils.getPlayerSettings(self._binders.PlayerSettings, player)
55
102
  end
56
103
 
104
+ --[=[
105
+ Promises the player's settings
106
+
107
+ @param player Player
108
+ @param cancelToken CancellationToken
109
+ @return Promise<PlayerSettingsClient>
110
+ ]=]
57
111
  function SettingsServiceClient:PromisePlayerSettings(player, cancelToken)
58
112
  assert(typeof(player) == "Instance" and player:IsA("Player"), "Bad player")
59
113
 
@@ -8,6 +8,8 @@ local BaseObject = require("BaseObject")
8
8
  local PlayerSettingsUtils = require("PlayerSettingsUtils")
9
9
  local SettingsBindersServer = require("SettingsBindersServer")
10
10
  local PlayerDataStoreService = require("PlayerDataStoreService")
11
+ local DataStoreStringUtils = require("DataStoreStringUtils")
12
+ local PlayerSettingsConstants = require("PlayerSettingsConstants")
11
13
 
12
14
  local PlayerHasSettings = setmetatable({}, BaseObject)
13
15
  PlayerHasSettings.ClassName = "PlayerHasSettings"
@@ -64,7 +66,21 @@ function PlayerHasSettings:_handleAttributeChanged(subStore, attributeName)
64
66
 
65
67
  -- Write the new value
66
68
  local settingName = PlayerSettingsUtils.getSettingName(attributeName)
69
+ if not DataStoreStringUtils.isValidUTF8(settingName) then
70
+ warn(string.format("[PlayerHasSettings] - Bad settingName %q, cannot save", settingName))
71
+ return
72
+ end
73
+
67
74
  local newValue = PlayerSettingsUtils.decodeForAttribute(self._settings:GetAttribute(attributeName))
75
+
76
+ if type(newValue) == "string" then
77
+ if (#settingName + #newValue) > PlayerSettingsConstants.MAX_SETTINGS_LENGTH then
78
+ warn(string.format("[PlayerSettingsClient.SetValue] - Setting is too long for %q. Cannot save.", settingName))
79
+ return
80
+ end
81
+ -- TODO: JSON encode and check length for ther scenarios
82
+ end
83
+
68
84
  subStore:Store(settingName, newValue)
69
85
  end
70
86
 
@@ -8,6 +8,7 @@ local PlayerSettingsBase = require("PlayerSettingsBase")
8
8
  local PlayerSettingsConstants = require("PlayerSettingsConstants")
9
9
  local PlayerSettingsUtils = require("PlayerSettingsUtils")
10
10
  local SettingRegistryServiceShared = require("SettingRegistryServiceShared")
11
+ local DataStoreStringUtils = require("DataStoreStringUtils")
11
12
 
12
13
  local PlayerSettings = setmetatable({}, PlayerSettingsBase)
13
14
  PlayerSettings.ClassName = "PlayerSettings"
@@ -42,12 +43,25 @@ function PlayerSettings.new(obj, serviceBag)
42
43
  end
43
44
 
44
45
  function PlayerSettings:EnsureInitialized(settingName, defaultValue)
46
+ assert(DataStoreStringUtils.isValidUTF8(settingName), "Bad settingName")
45
47
  assert(defaultValue ~= nil, "defaultValue cannot be nil")
46
48
 
47
49
  local attributeName = PlayerSettingsUtils.getAttributeName(settingName)
48
50
 
51
+ -- Paranoid UTF8 check. Don't even initialize this setting.
52
+ if type(defaultValue) == "string" then
53
+ assert(DataStoreStringUtils.isValidUTF8(defaultValue), "Bad UTF8 defaultValue")
54
+ end
55
+
49
56
  if self._obj:GetAttribute(attributeName) == nil then
50
- self._obj:SetAttribute(attributeName, PlayerSettingsUtils.encodeForAttribute(defaultValue))
57
+ local encoded = PlayerSettingsUtils.encodeForAttribute(defaultValue)
58
+
59
+ -- Paranoid UTF8 check
60
+ if type(encoded) == "string" then
61
+ assert(DataStoreStringUtils.isValidUTF8(defaultValue), "Bad UTF8 defaultValue")
62
+ end
63
+
64
+ self._obj:SetAttribute(attributeName, encoded)
51
65
  end
52
66
  end
53
67
 
@@ -67,6 +81,12 @@ function PlayerSettings:_setSettings(settingsMap)
67
81
  for settingName, value in pairs(settingsMap) do
68
82
  assert(type(settingName) == "string", "Bad key")
69
83
 
84
+ -- Avoid even letting these be set.
85
+ if not DataStoreStringUtils.isValidUTF8(settingName) then
86
+ warn("[PlayerSettings] - Bad UTF8 settingName. Skipping setting.")
87
+ continue
88
+ end
89
+
70
90
  local attributeName = PlayerSettingsUtils.getAttributeName(settingName)
71
91
 
72
92
  if self._obj:GetAttribute(attributeName) == nil then
@@ -74,8 +94,32 @@ function PlayerSettings:_setSettings(settingsMap)
74
94
  continue
75
95
  end
76
96
 
97
+ -- Paranoid UTF8 check. Avoid letting this value be set.
98
+ if type(value) == "string" then
99
+ if not DataStoreStringUtils.isValidUTF8(value) then
100
+ warn(string.format("[PlayerSettings] - Bad UTF8 value setting value for %q. Skipping setting.", settingName))
101
+ continue
102
+ end
103
+ end
104
+
77
105
  local decoded = PlayerSettingsUtils.decodeForNetwork(value)
78
- self._obj:SetAttribute(attributeName, PlayerSettingsUtils.encodeForAttribute(decoded))
106
+ local encodedAttribute = PlayerSettingsUtils.encodeForAttribute(decoded)
107
+
108
+ if type(encodedAttribute) == "string" then
109
+ -- Paranoid UTF8 check. Avoid letting this value be set.
110
+ if not DataStoreStringUtils.isValidUTF8(encodedAttribute) then
111
+ warn(string.format("[PlayerSettings] - Bad UTF8 encodedAttribute value for %q. Skipping setting.", settingName))
112
+ continue
113
+ end
114
+
115
+ -- Paranoid length check. One setting could prevent all from saving if we overflow our save limit.
116
+ if (#encodedAttribute + #settingName) > PlayerSettingsConstants.MAX_SETTINGS_LENGTH then
117
+ warn(string.format("[PlayerSettings] - Setting %q is too long. Skipping setting.", settingName))
118
+ continue
119
+ end
120
+ end
121
+
122
+ self._obj:SetAttribute(attributeName, encodedAttribute)
79
123
  end
80
124
  end
81
125
 
@@ -1,4 +1,6 @@
1
1
  --[=[
2
+ Base class for player settings.
3
+
2
4
  @class PlayerSettingsBase
3
5
  ]=]
4
6
 
@@ -9,13 +11,21 @@ local PlayerSettingsUtils = require("PlayerSettingsUtils")
9
11
  local RxAttributeUtils = require("RxAttributeUtils")
10
12
  local SettingDefinition = require("SettingDefinition")
11
13
  local Rx = require("Rx")
14
+ local DataStoreStringUtils = require("DataStoreStringUtils")
12
15
 
13
16
  local PlayerSettingsBase = setmetatable({}, BaseObject)
14
17
  PlayerSettingsBase.ClassName = "PlayerSettingsBase"
15
18
  PlayerSettingsBase.__index = PlayerSettingsBase
16
19
 
17
- function PlayerSettingsBase.new(obj, serviceBag)
18
- local self = setmetatable(BaseObject.new(obj), PlayerSettingsBase)
20
+ --[=[
21
+ Base class for player settings
22
+
23
+ @param folder Folder
24
+ @param serviceBag ServiceBag
25
+ @return PlayerSettingsBase
26
+ ]=]
27
+ function PlayerSettingsBase.new(folder, serviceBag)
28
+ local self = setmetatable(BaseObject.new(folder), PlayerSettingsBase)
19
29
 
20
30
  self._serviceBag = assert(serviceBag, "No serviceBag")
21
31
 
@@ -65,6 +75,7 @@ end
65
75
  function PlayerSettingsBase:GetValue(settingName, defaultValue)
66
76
  assert(type(settingName) == "string", "Bad settingName")
67
77
  assert(defaultValue ~= nil, "defaultValue cannot be nil")
78
+ assert(DataStoreStringUtils.isValidUTF8(settingName), "Bad settingName")
68
79
 
69
80
  local attributeName = PlayerSettingsUtils.getAttributeName(settingName)
70
81
 
@@ -87,6 +98,7 @@ end
87
98
  ]=]
88
99
  function PlayerSettingsBase:SetValue(settingName, value)
89
100
  assert(type(settingName) == "string", "Bad settingName")
101
+ assert(DataStoreStringUtils.isValidUTF8(settingName), "Bad settingName")
90
102
 
91
103
  local attributeName = PlayerSettingsUtils.getAttributeName(settingName)
92
104
 
@@ -103,6 +115,7 @@ end
103
115
  function PlayerSettingsBase:ObserveValue(settingName, defaultValue)
104
116
  assert(type(settingName) == "string", "Bad settingName")
105
117
  assert(defaultValue ~= nil, "defaultValue cannot be nil")
118
+ assert(DataStoreStringUtils.isValidUTF8(settingName), "Bad settingName")
106
119
 
107
120
  local attributeName = PlayerSettingsUtils.getAttributeName(settingName)
108
121
 
@@ -114,7 +127,16 @@ function PlayerSettingsBase:ObserveValue(settingName, defaultValue)
114
127
  })
115
128
  end
116
129
 
130
+ --[=[
131
+ Restores the default value for the setting
132
+
133
+ @param settingName string
134
+ @param defaultValue T
135
+ ]=]
117
136
  function PlayerSettingsBase:RestoreDefault(settingName, defaultValue)
137
+ assert(type(settingName) == "string", "Bad settingName")
138
+ assert(DataStoreStringUtils.isValidUTF8(settingName), "Bad settingName")
139
+
118
140
  -- TODO: Maybe something more sophisticated?
119
141
  self:SetValue(settingName, defaultValue)
120
142
  end
@@ -128,6 +150,7 @@ end
128
150
  function PlayerSettingsBase:EnsureInitialized(settingName, defaultValue)
129
151
  assert(type(settingName) == "string", "Bad settingName")
130
152
  assert(defaultValue ~= nil, "defaultValue cannot be nil")
153
+ assert(DataStoreStringUtils.isValidUTF8(settingName), "Bad settingName")
131
154
 
132
155
  -- noop
133
156
  end
@@ -14,4 +14,5 @@ return Table.readonly({
14
14
  PLAYER_SETTINGS_NAME = "PlayerSettings";
15
15
  REMOTE_FUNCTION_NAME = "PlayerSettingsRemoteFunction";
16
16
  REQUEST_UPDATE_SETTINGS = "requestUpdateSettings";
17
+ MAX_SETTINGS_LENGTH = 2048;
17
18
  })
@@ -1,4 +1,6 @@
1
1
  --[=[
2
+ Utility helpers to work with settings.
3
+
2
4
  @class PlayerSettingsUtils
3
5
  ]=]
4
6
 
@@ -11,9 +13,16 @@ local BinderUtils = require("BinderUtils")
11
13
  local Binder = require("Binder")
12
14
  local RxStateStackUtils = require("RxStateStackUtils")
13
15
  local EnumUtils = require("EnumUtils")
16
+ local DataStoreStringUtils = require("DataStoreStringUtils")
14
17
 
15
18
  local PlayerSettingsUtils = {}
16
19
 
20
+ --[=[
21
+ Creates a new player settings
22
+
23
+ @param binder Binder<PlayerSettings>
24
+ @return Folder
25
+ ]=]
17
26
  function PlayerSettingsUtils.create(binder)
18
27
  assert(Binder.isBinder(binder), "No binder")
19
28
 
@@ -25,6 +34,13 @@ function PlayerSettingsUtils.create(binder)
25
34
  return playerSettings
26
35
  end
27
36
 
37
+ --[=[
38
+ Observe a player settings for a player.
39
+
40
+ @param binder Binder<PlayerSettings>
41
+ @param player Player
42
+ @return Observable<PlayerSettings>
43
+ ]=]
28
44
  function PlayerSettingsUtils.observePlayerSettingsBrio(binder, player)
29
45
  assert(Binder.isBinder(binder), "No binder")
30
46
  assert(typeof(player) == "Instance" and player:IsA("Player"), "Bad player")
@@ -32,12 +48,26 @@ function PlayerSettingsUtils.observePlayerSettingsBrio(binder, player)
32
48
  return RxBinderUtils.observeBoundChildClassBrio(binder, player)
33
49
  end
34
50
 
51
+ --[=[
52
+ Observe a player's latest settings
53
+
54
+ @param binder Binder<PlayerSettings>
55
+ @param player Player
56
+ @return Observable<PlayerSettings>
57
+ ]=]
35
58
  function PlayerSettingsUtils.observePlayerSettings(binder, player)
36
59
  return RxBinderUtils.observeBoundChildClassBrio(binder, player):Pipe({
37
60
  RxStateStackUtils.topOfStack()
38
61
  })
39
62
  end
40
63
 
64
+ --[=[
65
+ Gets a player's latest settings
66
+
67
+ @param binder Binder<PlayerSettings>
68
+ @param player Player
69
+ @return PlayerSettings
70
+ ]=]
41
71
  function PlayerSettingsUtils.getPlayerSettings(binder, player)
42
72
  assert(Binder.isBinder(binder), "No binder")
43
73
  assert(typeof(player) == "Instance" and player:IsA("Player"), "Bad player")
@@ -45,25 +75,50 @@ function PlayerSettingsUtils.getPlayerSettings(binder, player)
45
75
  return BinderUtils.findFirstChild(binder, player)
46
76
  end
47
77
 
78
+ --[=[
79
+ Gets the attribute name for a setting
80
+
81
+ @param settingName string
82
+ @return string
83
+ ]=]
48
84
  function PlayerSettingsUtils.getAttributeName(settingName)
49
85
  assert(type(settingName) == "string", "Bad settingName")
86
+ assert(DataStoreStringUtils.isValidUTF8(settingName), "Bad settingName")
50
87
 
51
88
  return PlayerSettingsConstants.SETTING_ATTRIBUTE_PREFIX .. settingName
52
89
  end
53
90
 
91
+ --[=[
92
+ Gets the settings name from an attribute. May return a string
93
+ that cannot be loaded into datastore.
94
+
95
+ @param attributeName string
96
+ @return string
97
+ ]=]
54
98
  function PlayerSettingsUtils.getSettingName(attributeName)
55
99
  assert(type(attributeName) == "string", "Bad attributeName")
56
100
 
57
- attributeName = String.removePrefix(attributeName, PlayerSettingsConstants.SETTING_ATTRIBUTE_PREFIX)
58
- return attributeName
101
+ return String.removePrefix(attributeName, PlayerSettingsConstants.SETTING_ATTRIBUTE_PREFIX)
59
102
  end
60
103
 
104
+ --[=[
105
+ Returns true if the attribute name is a settings attribute
106
+
107
+ @param attributeName string
108
+ @return string
109
+ ]=]
61
110
  function PlayerSettingsUtils.isSettingAttribute(attributeName)
62
111
  assert(type(attributeName) == "string", "Bad attributeName")
63
112
 
64
113
  return String.startsWith(attributeName, PlayerSettingsConstants.SETTING_ATTRIBUTE_PREFIX)
65
114
  end
66
115
 
116
+ --[=[
117
+ Encodes a given value for network transfer
118
+
119
+ @param settingValue any
120
+ @return any
121
+ ]=]
67
122
  function PlayerSettingsUtils.encodeForNetwork(settingValue)
68
123
  assert(settingValue ~= "<NIL_SETTING_VALUE>", "Cannot have setting as <NIL_SETTING_VALUE>")
69
124
 
@@ -76,6 +131,12 @@ function PlayerSettingsUtils.encodeForNetwork(settingValue)
76
131
  end
77
132
  end
78
133
 
134
+ --[=[
135
+ Decodes a given value from network transfer
136
+
137
+ @param settingValue any
138
+ @return any
139
+ ]=]
79
140
  function PlayerSettingsUtils.decodeForNetwork(settingValue)
80
141
  if settingValue == "<NIL_SETTING_VALUE>" then
81
142
  return nil
@@ -86,6 +147,12 @@ function PlayerSettingsUtils.decodeForNetwork(settingValue)
86
147
  end
87
148
  end
88
149
 
150
+ --[=[
151
+ Decodes a given value for attribute storage
152
+
153
+ @param settingValue any
154
+ @return any
155
+ ]=]
89
156
  function PlayerSettingsUtils.decodeForAttribute(settingValue)
90
157
  if EnumUtils.isEncodedEnum(settingValue) then
91
158
  return EnumUtils.decodeFromString(settingValue)
@@ -94,6 +161,12 @@ function PlayerSettingsUtils.decodeForAttribute(settingValue)
94
161
  end
95
162
  end
96
163
 
164
+ --[=[
165
+ Encodes a given value for attribute storage
166
+
167
+ @param settingValue any
168
+ @return any
169
+ ]=]
97
170
  function PlayerSettingsUtils.encodeForAttribute(settingValue)
98
171
  if typeof(settingValue) == "EnumItem" then
99
172
  return EnumUtils.encodeAsString(settingValue)
@@ -102,5 +175,4 @@ function PlayerSettingsUtils.encodeForAttribute(settingValue)
102
175
  end
103
176
  end
104
177
 
105
-
106
178
  return PlayerSettingsUtils
@@ -1,4 +1,20 @@
1
1
  --[=[
2
+ These settings definitions are used to define a setting and register them on both the client and server. See
3
+ [SettingDefinitionProvider] for more details on grouping these.
4
+
5
+ Notably a setting is basically anything on the client that can be stored on the server by the client, and that
6
+ relatively minimal validation is required upon. This can be both user-set settings, as well as very temporary
7
+ data.
8
+
9
+ ```lua
10
+ local SettingDefinition = require("SettingDefinition")
11
+
12
+ return require("SettingDefinitionProvider").new({
13
+ SettingDefinition.new("LastTimeUpdateSeen", 0);
14
+ SettingDefinition.new("LastTimeShopSeen", 0);
15
+ })
16
+ ```
17
+
2
18
  @class SettingDefinition
3
19
  ]=]
4
20
 
@@ -1,4 +1,26 @@
1
1
  --[=[
2
+ Provides settings in bulk, and can be initialized by a [ServiceBag]. See [SettingDefinition] for
3
+ more details on how to use this.
4
+
5
+ :::tip
6
+ These settings providers should be used on both the client and the server. On the client, these
7
+ are registered with the [SettingRegistryServiceShared] so that they can be shown in UI automatically
8
+ if desired.
9
+
10
+ On the server, these are registered with [SettingRegistryServiceShared] and then are checked before
11
+ arbitrary data can e sent.
12
+ :::
13
+
14
+ ```lua
15
+ local SettingDefinition = require("SettingDefinition")
16
+
17
+ return require("SettingDefinitionProvider").new({
18
+ SettingDefinition.new("KeyBinding", Enum.KeyCode.X);
19
+ SettingDefinition.new("CameraShake", true);
20
+ SettingDefinition.new("CameraSensitivity", 1);
21
+ })
22
+ ```
23
+
2
24
  @class SettingDefinitionProvider
3
25
  ]=]
4
26
 
@@ -12,6 +34,22 @@ SettingDefinitionProvider.ClassName = "SettingDefinitionProvider"
12
34
  SettingDefinitionProvider.ServiceName = "SettingDefinitionProvider"
13
35
  SettingDefinitionProvider.__index = SettingDefinitionProvider
14
36
 
37
+ --[=[
38
+ Constructs a new provider with a list of [SettingDefinition]'s.
39
+
40
+ ```lua
41
+ local SettingDefinition = require("SettingDefinition")
42
+
43
+ return require("SettingDefinitionProvider").new({
44
+ SettingDefinition.new("KeyBinding", Enum.KeyCode.X);
45
+ SettingDefinition.new("CameraShake", true);
46
+ SettingDefinition.new("CameraSensitivity", 1);
47
+ })
48
+ ```
49
+
50
+ @param settingDefinitions { SettingDefinition }
51
+ @return SettingDefinitionProvider
52
+ ]=]
15
53
  function SettingDefinitionProvider.new(settingDefinitions)
16
54
  local self = setmetatable({}, SettingDefinitionProvider)
17
55
 
@@ -26,6 +64,11 @@ function SettingDefinitionProvider.new(settingDefinitions)
26
64
  return self
27
65
  end
28
66
 
67
+ --[=[
68
+ Initializes the provider, storing the data in [SettingRegistryServiceShared]
69
+
70
+ @param serviceBag ServiceBag
71
+ ]=]
29
72
  function SettingDefinitionProvider:Init(serviceBag)
30
73
  assert(serviceBag, "No serviceBag")
31
74
  assert(not self._maid, "Already initialized")
@@ -38,16 +81,47 @@ function SettingDefinitionProvider:Init(serviceBag)
38
81
  end
39
82
  end
40
83
 
84
+ --[=[
85
+ Starts the provider. Empty.
86
+ ]=]
41
87
  function SettingDefinitionProvider:Start()
42
88
  -- Empty, to prevent us from erroring on service bag init
43
89
  end
44
90
 
91
+ --[=[
92
+ Returns the setting definition
93
+
94
+ @return { SettingDefinition }
95
+ ]=]
45
96
  function SettingDefinitionProvider:GetSettingDefinitions()
46
97
  return self._settingDefinitions
47
98
  end
48
99
 
100
+ --[=[
101
+ You can index the provider to get a setting. For example
102
+
103
+ ```lua
104
+ local SettingDefinition = require("SettingDefinition")
105
+
106
+ local provider = require("SettingDefinitionProvider").new({
107
+ SettingDefinition.new("KeyBinding", Enum.KeyCode.X);
108
+ SettingDefinition.new("CameraShake", true);
109
+ SettingDefinition.new("CameraSensitivity", 1);
110
+ })
111
+
112
+ local service = serviceBag:GetService(provider)
113
+
114
+ -- Write a setting
115
+ service.CamaraShake:GetLocalPlayerSettingProperty(serviceBag).Value = false
116
+ ```
117
+
118
+ @param index string
119
+ @return SettingDefinition
120
+ ]=]
49
121
  function SettingDefinitionProvider:__index(index)
50
- if SettingDefinitionProvider[index] then
122
+ if index == nil then
123
+ error("[SettingDefinitionProvider] - Cannot index provider with nil value")
124
+ elseif SettingDefinitionProvider[index] then
51
125
  return SettingDefinitionProvider[index]
52
126
  elseif index == "_lookup" or index == "_settingDefinitions" or index == "_maid" then
53
127
  return rawget(self, index)
@@ -64,10 +138,21 @@ function SettingDefinitionProvider:__index(index)
64
138
  end
65
139
  end
66
140
 
141
+ --[=[
142
+ Gets a new setting definition if it exists
143
+
144
+ @param settingName string
145
+ @return SettingDefinition
146
+ ]=]
67
147
  function SettingDefinitionProvider:Get(settingName)
148
+ assert(type(settingName) == "string", "Bad settingName")
149
+
68
150
  return self._lookup[settingName]
69
151
  end
70
152
 
153
+ --[=[
154
+ Cleans up the setting registration
155
+ ]=]
71
156
  function SettingDefinitionProvider:Destroy()
72
157
  self._maid:DoCleaning()
73
158
  end
@@ -11,6 +11,14 @@ local SettingProperty = {}
11
11
  SettingProperty.ClassName = "SettingProperty"
12
12
  SettingProperty.__index = SettingProperty
13
13
 
14
+ --[=[
15
+ Constructs a new SettingProperty.
16
+
17
+ @param serviceBag ServiceBag
18
+ @param player Player
19
+ @param definition SettingDefinition
20
+ @return SettingProperty<T>
21
+ ]=]
14
22
  function SettingProperty.new(serviceBag, player, definition)
15
23
  local self = setmetatable({}, SettingProperty)
16
24
 
@@ -27,6 +35,10 @@ function SettingProperty.new(serviceBag, player, definition)
27
35
  return self
28
36
  end
29
37
 
38
+ --[=[
39
+ Observes the value of the setting property
40
+ @return Observable<T>
41
+ ]=]
30
42
  function SettingProperty:Observe()
31
43
  return self:_observePlayerSettings():Pipe({
32
44
  Rx.where(function(settings)
@@ -85,15 +97,29 @@ function SettingProperty:__newindex(index, value)
85
97
  end
86
98
  end
87
99
 
100
+ --[=[
101
+ Sets the value of the setting property. Will warn if it cannot do so.
102
+
103
+ :::tip
104
+ Use [PromiseSetValue] to ensure value is set.
105
+ :::
106
+
107
+ @param value T
108
+ ]=]
88
109
  function SettingProperty:SetValue(value)
89
110
  local settings = self:_getPlayerSettings()
90
111
  if settings then
91
112
  settings:SetValue(self._definition:GetSettingName(), value)
92
113
  else
93
- warn("Cannot set setting value. Use :PromiseSetValue() to ensure value is set after load.")
114
+ warn("[SettingProperty.SetValue] - Cannot set setting value. Use :PromiseSetValue() to ensure value is set after load.")
94
115
  end
95
116
  end
96
117
 
118
+ --[=[
119
+ Promises the value of the setting once it's loaded.
120
+
121
+ @return Promise<T>
122
+ ]=]
97
123
  function SettingProperty:PromiseValue()
98
124
  return self:_promisePlayerSettings()
99
125
  :Then(function(playerSettings)
@@ -101,6 +127,12 @@ function SettingProperty:PromiseValue()
101
127
  end)
102
128
  end
103
129
 
130
+ --[=[
131
+ Promises to set the value
132
+
133
+ @param value T
134
+ @return Promise
135
+ ]=]
104
136
  function SettingProperty:PromiseSetValue(value)
105
137
  return self:_promisePlayerSettings()
106
138
  :Then(function(playerSettings)
@@ -108,15 +140,33 @@ function SettingProperty:PromiseSetValue(value)
108
140
  end)
109
141
  end
110
142
 
143
+ --[=[
144
+ Restores the setting to the default value
145
+ ]=]
111
146
  function SettingProperty:RestoreDefault()
112
147
  local settings = self:_getPlayerSettings()
113
148
  if settings then
114
149
  settings:RestoreDefault(self._definition:GetSettingName(), self._definition:GetDefaultValue())
115
150
  else
116
- warn("Cannot set setting value. Use :PromiseSetValue() to ensure value is set after load.")
151
+ warn("[SettingProperty.RestoreDefault] - Cannot set setting value. Use :PromiseRestoreDefault() to ensure value is set after load.")
117
152
  end
118
153
  end
119
154
 
155
+ --[=[
156
+ Restores the setting to the default value. This is different than setting to the default value
157
+ because it means there is no "user-set" value which could lead to values changing if
158
+ defaults change.
159
+
160
+ @return Promise
161
+ ]=]
162
+ function SettingProperty:PromiseRestoreDefault()
163
+ return self:_promisePlayerSettings()
164
+ :Then(function(playerSettings)
165
+ playerSettings:RestoreDefault(self._definition:GetSettingName(), self._definition:GetDefaultValue())
166
+ end)
167
+ end
168
+
169
+
120
170
  function SettingProperty:_observePlayerSettings()
121
171
  return self._bridge:ObservePlayerSettings(self._player)
122
172
  end
@@ -129,6 +179,4 @@ function SettingProperty:_promisePlayerSettings()
129
179
  return self._bridge:PromisePlayerSettings(self._player)
130
180
  end
131
181
 
132
-
133
-
134
182
  return SettingProperty
@@ -9,39 +9,81 @@ local require = require(script.Parent.loader).load(script)
9
9
  local ValueObject = require("ValueObject")
10
10
  local Rx = require("Rx")
11
11
  local ObservableSet = require("ObservableSet")
12
+ local Maid = require("Maid")
12
13
 
13
14
  local SettingRegistryServiceShared = {}
14
15
 
16
+ --[=[
17
+ Initializes the shared registry service. Should be done via [ServiceBag].
18
+
19
+ @param serviceBag ServiceBag
20
+ ]=]
15
21
  function SettingRegistryServiceShared:Init(serviceBag)
16
22
  assert(not self._serviceBag, "Already initialized")
17
23
  self._serviceBag = assert(serviceBag, "No serviceBag")
24
+ self._maid = Maid.new()
18
25
 
19
26
  self._settingService = ValueObject.new()
27
+ self._maid:GiveTask(self._settingService)
28
+
20
29
  self._settingDefinitions = ObservableSet.new()
30
+ self._maid:GiveTask(self._settingDefinitions)
21
31
  end
22
32
 
33
+ --[=[
34
+ Registers the shared setting service for this bridge
35
+
36
+ @param settingService SettingService
37
+ ]=]
23
38
  function SettingRegistryServiceShared:RegisterSettingService(settingService)
24
39
  self._settingService.Value = settingService
25
40
  end
26
41
 
42
+ --[=[
43
+ Registers settings definitions
44
+
45
+ @param definition SettingDefinition
46
+ @return callback -- Cleanup callback
47
+ ]=]
27
48
  function SettingRegistryServiceShared:RegisterSettingDefinition(definition)
28
49
  assert(definition, "No definition")
29
50
 
30
51
  return self._settingDefinitions:Add(definition)
31
52
  end
32
53
 
54
+ --[=[
55
+ Observes the registered definitions
56
+
57
+ @return Observable<Brio<SettingDefinition>>
58
+ ]=]
33
59
  function SettingRegistryServiceShared:ObserveRegisteredDefinitionsBrio()
34
60
  return self._settingDefinitions:ObserveItemsBrio()
35
61
  end
36
62
 
63
+ --[=[
64
+ Gets the current settings service
65
+
66
+ @return SettingService
67
+ ]=]
37
68
  function SettingRegistryServiceShared:GetSettingsService()
38
69
  return self._settingService.Value
39
70
  end
40
71
 
72
+ --[=[
73
+ Observes the current settings service
74
+
75
+ @return Observable<SettingService>
76
+ ]=]
41
77
  function SettingRegistryServiceShared:ObserveSettingsService()
42
78
  return self._settingService:Observe()
43
79
  end
44
80
 
81
+ --[=[
82
+ Observes the player's settings
83
+
84
+ @param player Player
85
+ @return Observable<PlayerSettingsBase>
86
+ ]=]
45
87
  function SettingRegistryServiceShared:ObservePlayerSettings(player)
46
88
  assert(typeof(player) == "Instance" and player:IsA("Player"), "Bad player")
47
89
 
@@ -56,6 +98,12 @@ function SettingRegistryServiceShared:ObservePlayerSettings(player)
56
98
  })
57
99
  end
58
100
 
101
+ --[=[
102
+ Promises the player's settings
103
+
104
+ @param player Player
105
+ @return Promise<PlayerSettingsBase>
106
+ ]=]
59
107
  function SettingRegistryServiceShared:PromisePlayerSettings(player)
60
108
  assert(typeof(player) == "Instance" and player:IsA("Player"), "Bad player")
61
109
 
@@ -69,6 +117,12 @@ function SettingRegistryServiceShared:PromisePlayerSettings(player)
69
117
  end)
70
118
  end
71
119
 
120
+ --[=[
121
+ Gets the player's settings
122
+
123
+ @param player Player
124
+ @return Promise<PlayerSettingsBase>
125
+ ]=]
72
126
  function SettingRegistryServiceShared:GetPlayerSettings(player)
73
127
  assert(typeof(player) == "Instance" and player:IsA("Player"), "Bad player")
74
128
 
@@ -80,4 +134,11 @@ function SettingRegistryServiceShared:GetPlayerSettings(player)
80
134
  end
81
135
  end
82
136
 
137
+ --[=[
138
+ Cleans up the shared registry service
139
+ ]=]
140
+ function SettingRegistryServiceShared:Destroy()
141
+ self._maid:DoCleaning()
142
+ end
143
+
83
144
  return SettingRegistryServiceShared