roblox-opencode 1.0.0 → 1.0.2

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.
Files changed (246) hide show
  1. package/README.md +112 -122
  2. package/commands/setup-game.md +108 -108
  3. package/commands/sync-check.md +53 -53
  4. package/core/roblox-core.md +93 -93
  5. package/dist/server.js +189 -167
  6. package/package.json +35 -35
  7. package/skills/roblox-analytics/SKILL.md +277 -277
  8. package/skills/roblox-analytics/references/event-batcher.luau +75 -75
  9. package/skills/roblox-animation-vfx/SKILL.md +1325 -1325
  10. package/skills/roblox-architecture/SKILL.md +877 -863
  11. package/skills/roblox-architecture/references/combat-systems.md +1381 -1381
  12. package/skills/roblox-code-review/SKILL.md +686 -686
  13. package/skills/roblox-data/SKILL.md +889 -889
  14. package/skills/roblox-data/references/inventory-systems.md +1729 -1729
  15. package/skills/roblox-debug/SKILL.md +98 -98
  16. package/skills/roblox-gui/SKILL.md +1103 -1103
  17. package/skills/roblox-gui-fusion/SKILL.md +150 -150
  18. package/skills/roblox-gui-fusion/references/inventory.luau +427 -427
  19. package/skills/roblox-gui-fusion/references/settings-menu.luau +579 -579
  20. package/skills/roblox-gui-fusion/references/shop.luau +411 -411
  21. package/skills/roblox-luau-mastery/SKILL.md +1618 -1519
  22. package/skills/roblox-monetization/SKILL.md +1084 -1084
  23. package/skills/roblox-monetization/references/process-receipt.luau +131 -131
  24. package/skills/roblox-networking/SKILL.md +669 -669
  25. package/skills/roblox-networking/references/remote-validator.luau +193 -193
  26. package/skills/roblox-publish-checklist/SKILL.md +127 -127
  27. package/skills/roblox-runtime/SKILL.md +753 -753
  28. package/skills/roblox-sharp-edges/SKILL.md +294 -294
  29. package/skills/roblox-sync/SKILL.md +126 -126
  30. package/skills/roblox-testing/SKILL.md +943 -943
  31. package/skills/roblox-tooling/SKILL.md +149 -149
  32. package/vendor/LICENSES/ProfileStore-LICENSE +201 -201
  33. package/vendor/LICENSES/RbxUtil-LICENSE +7 -7
  34. package/vendor/LICENSES/promise-LICENSE +20 -20
  35. package/vendor/LICENSES/t-LICENSE +21 -21
  36. package/vendor/LICENSES/testez-LICENSE +200 -200
  37. package/vendor/README.md +83 -83
  38. package/vendor/fusion/Animation/ExternalTime.luau +83 -83
  39. package/vendor/fusion/Animation/Spring.luau +321 -321
  40. package/vendor/fusion/Animation/Stopwatch.luau +127 -127
  41. package/vendor/fusion/Animation/Tween.luau +187 -187
  42. package/vendor/fusion/Animation/getTweenDuration.luau +27 -27
  43. package/vendor/fusion/Animation/getTweenRatio.luau +47 -47
  44. package/vendor/fusion/Animation/lerpType.luau +163 -163
  45. package/vendor/fusion/Animation/packType.luau +99 -99
  46. package/vendor/fusion/Animation/springCoefficients.luau +80 -80
  47. package/vendor/fusion/Animation/unpackType.luau +102 -102
  48. package/vendor/fusion/Colour/Oklab.luau +70 -70
  49. package/vendor/fusion/Colour/sRGB.luau +54 -54
  50. package/vendor/fusion/External.luau +167 -167
  51. package/vendor/fusion/ExternalDebug.luau +69 -69
  52. package/vendor/fusion/Graph/Observer.luau +113 -113
  53. package/vendor/fusion/Graph/castToGraph.luau +28 -28
  54. package/vendor/fusion/Graph/change.luau +80 -80
  55. package/vendor/fusion/Graph/depend.luau +32 -32
  56. package/vendor/fusion/Graph/evaluate.luau +55 -55
  57. package/vendor/fusion/Instances/Attribute.luau +57 -57
  58. package/vendor/fusion/Instances/AttributeChange.luau +46 -46
  59. package/vendor/fusion/Instances/AttributeOut.luau +63 -63
  60. package/vendor/fusion/Instances/Child.luau +21 -21
  61. package/vendor/fusion/Instances/Children.luau +147 -147
  62. package/vendor/fusion/Instances/Hydrate.luau +32 -32
  63. package/vendor/fusion/Instances/New.luau +52 -52
  64. package/vendor/fusion/Instances/OnChange.luau +49 -49
  65. package/vendor/fusion/Instances/OnEvent.luau +53 -53
  66. package/vendor/fusion/Instances/Out.luau +69 -69
  67. package/vendor/fusion/Instances/applyInstanceProps.luau +148 -148
  68. package/vendor/fusion/Instances/defaultProps.luau +194 -194
  69. package/vendor/fusion/LICENSE +21 -21
  70. package/vendor/fusion/Logging/formatError.luau +48 -48
  71. package/vendor/fusion/Logging/messages.luau +51 -51
  72. package/vendor/fusion/Logging/parseError.luau +24 -24
  73. package/vendor/fusion/Memory/checkLifetime.luau +133 -133
  74. package/vendor/fusion/Memory/deriveScope.luau +23 -23
  75. package/vendor/fusion/Memory/deriveScopeImpl.luau +44 -44
  76. package/vendor/fusion/Memory/doCleanup.luau +78 -78
  77. package/vendor/fusion/Memory/innerScope.luau +33 -33
  78. package/vendor/fusion/Memory/legacyCleanup.luau +17 -17
  79. package/vendor/fusion/Memory/needsDestruction.luau +16 -16
  80. package/vendor/fusion/Memory/poisonScope.luau +33 -33
  81. package/vendor/fusion/Memory/scopePool.luau +54 -54
  82. package/vendor/fusion/Memory/scoped.luau +26 -26
  83. package/vendor/fusion/Memory/whichLivesLonger.luau +74 -74
  84. package/vendor/fusion/RobloxExternal.luau +97 -97
  85. package/vendor/fusion/State/Computed.luau +138 -138
  86. package/vendor/fusion/State/For/Disassembly.luau +210 -210
  87. package/vendor/fusion/State/For/ForTypes.luau +30 -30
  88. package/vendor/fusion/State/For/init.luau +109 -109
  89. package/vendor/fusion/State/ForKeys.luau +93 -93
  90. package/vendor/fusion/State/ForPairs.luau +96 -96
  91. package/vendor/fusion/State/ForValues.luau +93 -93
  92. package/vendor/fusion/State/Value.luau +87 -87
  93. package/vendor/fusion/State/castToState.luau +25 -25
  94. package/vendor/fusion/State/peek.luau +30 -30
  95. package/vendor/fusion/Types.luau +314 -314
  96. package/vendor/fusion/Utility/Contextual.luau +90 -90
  97. package/vendor/fusion/Utility/Safe.luau +22 -22
  98. package/vendor/fusion/Utility/isSimilar.luau +29 -29
  99. package/vendor/fusion/Utility/merge.luau +35 -35
  100. package/vendor/fusion/Utility/nameOf.luau +34 -34
  101. package/vendor/fusion/Utility/never.luau +13 -13
  102. package/vendor/fusion/Utility/nicknames.luau +10 -10
  103. package/vendor/fusion/Utility/xtypeof.luau +26 -26
  104. package/vendor/fusion/init.luau +82 -82
  105. package/vendor/profilestore/init.luau +2242 -2242
  106. package/vendor/promise/init.luau +1982 -1982
  107. package/vendor/rbxutil/buffer-util/Buffer.test.luau +25 -25
  108. package/vendor/rbxutil/buffer-util/BufferReader.luau +228 -228
  109. package/vendor/rbxutil/buffer-util/BufferWriter.luau +269 -269
  110. package/vendor/rbxutil/buffer-util/DataTypeBuffer.luau +223 -223
  111. package/vendor/rbxutil/buffer-util/Types.luau +60 -60
  112. package/vendor/rbxutil/buffer-util/index.d.ts +153 -153
  113. package/vendor/rbxutil/buffer-util/init.luau +41 -41
  114. package/vendor/rbxutil/buffer-util/package.json +16 -16
  115. package/vendor/rbxutil/buffer-util/wally.toml +9 -9
  116. package/vendor/rbxutil/comm/Client/ClientComm.luau +232 -232
  117. package/vendor/rbxutil/comm/Client/ClientRemoteProperty.luau +156 -156
  118. package/vendor/rbxutil/comm/Client/ClientRemoteSignal.luau +109 -109
  119. package/vendor/rbxutil/comm/Client/init.luau +135 -135
  120. package/vendor/rbxutil/comm/Server/RemoteProperty.luau +295 -295
  121. package/vendor/rbxutil/comm/Server/RemoteSignal.luau +211 -211
  122. package/vendor/rbxutil/comm/Server/ServerComm.luau +211 -211
  123. package/vendor/rbxutil/comm/Server/init.luau +140 -140
  124. package/vendor/rbxutil/comm/Types.luau +18 -18
  125. package/vendor/rbxutil/comm/Util.luau +27 -27
  126. package/vendor/rbxutil/comm/init.luau +35 -35
  127. package/vendor/rbxutil/comm/wally.toml +13 -13
  128. package/vendor/rbxutil/component/init.luau +759 -759
  129. package/vendor/rbxutil/component/init.test.luau +311 -311
  130. package/vendor/rbxutil/component/wally.toml +14 -14
  131. package/vendor/rbxutil/concur/init.luau +542 -542
  132. package/vendor/rbxutil/concur/init.test.luau +364 -364
  133. package/vendor/rbxutil/concur/wally.toml +8 -8
  134. package/vendor/rbxutil/enum-list/init.luau +101 -101
  135. package/vendor/rbxutil/enum-list/init.test.luau +91 -91
  136. package/vendor/rbxutil/enum-list/wally.toml +8 -8
  137. package/vendor/rbxutil/find/index.d.ts +20 -20
  138. package/vendor/rbxutil/find/init.luau +44 -44
  139. package/vendor/rbxutil/find/package.json +17 -17
  140. package/vendor/rbxutil/find/wally.toml +8 -8
  141. package/vendor/rbxutil/input/Gamepad.luau +559 -559
  142. package/vendor/rbxutil/input/Keyboard.luau +124 -124
  143. package/vendor/rbxutil/input/Mouse.luau +278 -278
  144. package/vendor/rbxutil/input/PreferredInput.luau +91 -91
  145. package/vendor/rbxutil/input/Touch.luau +120 -120
  146. package/vendor/rbxutil/input/init.luau +33 -33
  147. package/vendor/rbxutil/input/wally.toml +12 -12
  148. package/vendor/rbxutil/loader/index.d.ts +15 -15
  149. package/vendor/rbxutil/loader/init.luau +137 -137
  150. package/vendor/rbxutil/loader/wally.toml +8 -8
  151. package/vendor/rbxutil/log/index.d.ts +38 -38
  152. package/vendor/rbxutil/log/init.luau +746 -746
  153. package/vendor/rbxutil/log/wally.toml +8 -8
  154. package/vendor/rbxutil/net/init.luau +190 -190
  155. package/vendor/rbxutil/net/wally.toml +8 -8
  156. package/vendor/rbxutil/option/index.d.ts +44 -44
  157. package/vendor/rbxutil/option/init.luau +489 -489
  158. package/vendor/rbxutil/option/init.test.luau +342 -342
  159. package/vendor/rbxutil/option/wally.toml +8 -8
  160. package/vendor/rbxutil/pid/index.d.ts +53 -53
  161. package/vendor/rbxutil/pid/init.luau +195 -195
  162. package/vendor/rbxutil/pid/package.json +16 -16
  163. package/vendor/rbxutil/pid/wally.toml +9 -9
  164. package/vendor/rbxutil/quaternion/index.d.ts +117 -117
  165. package/vendor/rbxutil/quaternion/init.luau +570 -570
  166. package/vendor/rbxutil/quaternion/package.json +16 -16
  167. package/vendor/rbxutil/quaternion/wally.toml +9 -9
  168. package/vendor/rbxutil/query/index.d.ts +43 -43
  169. package/vendor/rbxutil/query/init.luau +117 -117
  170. package/vendor/rbxutil/query/package.json +18 -18
  171. package/vendor/rbxutil/query/wally.toml +9 -9
  172. package/vendor/rbxutil/sequent/index.d.ts +28 -28
  173. package/vendor/rbxutil/sequent/init.luau +340 -340
  174. package/vendor/rbxutil/sequent/package.json +16 -16
  175. package/vendor/rbxutil/sequent/wally.toml +9 -9
  176. package/vendor/rbxutil/ser/init.luau +175 -175
  177. package/vendor/rbxutil/ser/init.test.luau +50 -50
  178. package/vendor/rbxutil/ser/wally.toml +11 -11
  179. package/vendor/rbxutil/shake/index.d.ts +36 -36
  180. package/vendor/rbxutil/shake/init.luau +532 -532
  181. package/vendor/rbxutil/shake/init.test.luau +267 -267
  182. package/vendor/rbxutil/shake/package.json +16 -16
  183. package/vendor/rbxutil/shake/wally.toml +9 -9
  184. package/vendor/rbxutil/signal/index.d.ts +100 -100
  185. package/vendor/rbxutil/signal/init.luau +432 -432
  186. package/vendor/rbxutil/signal/init.test.luau +190 -190
  187. package/vendor/rbxutil/signal/package.json +17 -17
  188. package/vendor/rbxutil/signal/wally.toml +9 -9
  189. package/vendor/rbxutil/silo/TableWatcher.luau +65 -65
  190. package/vendor/rbxutil/silo/Util.luau +55 -55
  191. package/vendor/rbxutil/silo/init.luau +338 -338
  192. package/vendor/rbxutil/silo/init.test.luau +215 -215
  193. package/vendor/rbxutil/silo/wally.toml +8 -8
  194. package/vendor/rbxutil/spring/index.d.ts +40 -40
  195. package/vendor/rbxutil/spring/init.luau +97 -97
  196. package/vendor/rbxutil/spring/package.json +17 -17
  197. package/vendor/rbxutil/spring/wally.toml +8 -8
  198. package/vendor/rbxutil/stream/index.d.ts +88 -88
  199. package/vendor/rbxutil/stream/init.luau +597 -597
  200. package/vendor/rbxutil/stream/package.json +18 -18
  201. package/vendor/rbxutil/stream/wally.toml +9 -9
  202. package/vendor/rbxutil/streamable/Streamable.luau +202 -202
  203. package/vendor/rbxutil/streamable/StreamableUtil.luau +80 -80
  204. package/vendor/rbxutil/streamable/init.luau +8 -8
  205. package/vendor/rbxutil/streamable/wally.toml +12 -12
  206. package/vendor/rbxutil/symbol/init.luau +56 -56
  207. package/vendor/rbxutil/symbol/init.test.luau +37 -37
  208. package/vendor/rbxutil/symbol/wally.toml +8 -8
  209. package/vendor/rbxutil/table-util/init.luau +938 -938
  210. package/vendor/rbxutil/table-util/init.test.luau +439 -439
  211. package/vendor/rbxutil/task-queue/index.d.ts +27 -27
  212. package/vendor/rbxutil/task-queue/init.luau +97 -97
  213. package/vendor/rbxutil/task-queue/wally.toml +8 -8
  214. package/vendor/rbxutil/timer/index.d.ts +81 -81
  215. package/vendor/rbxutil/timer/init.luau +249 -249
  216. package/vendor/rbxutil/timer/init.test.luau +73 -73
  217. package/vendor/rbxutil/timer/wally.toml +11 -11
  218. package/vendor/rbxutil/tree/index.d.ts +15 -15
  219. package/vendor/rbxutil/tree/init.luau +137 -137
  220. package/vendor/rbxutil/tree/wally.toml +8 -8
  221. package/vendor/rbxutil/trove/index.d.ts +46 -46
  222. package/vendor/rbxutil/trove/init.luau +787 -787
  223. package/vendor/rbxutil/trove/init.test.luau +203 -203
  224. package/vendor/rbxutil/trove/wally.toml +8 -8
  225. package/vendor/rbxutil/typed-remote/init.luau +196 -196
  226. package/vendor/rbxutil/typed-remote/wally.toml +8 -8
  227. package/vendor/rbxutil/wait-for/index.d.ts +17 -17
  228. package/vendor/rbxutil/wait-for/init.luau +257 -257
  229. package/vendor/rbxutil/wait-for/init.test.luau +182 -182
  230. package/vendor/rbxutil/wait-for/wally.toml +11 -11
  231. package/vendor/t/t.lua +1350 -1350
  232. package/vendor/testez/Context.lua +26 -26
  233. package/vendor/testez/Expectation.lua +311 -311
  234. package/vendor/testez/ExpectationContext.lua +38 -38
  235. package/vendor/testez/LifecycleHooks.lua +89 -89
  236. package/vendor/testez/Reporters/TeamCityReporter.lua +101 -101
  237. package/vendor/testez/Reporters/TextReporter.lua +105 -105
  238. package/vendor/testez/Reporters/TextReporterQuiet.lua +96 -96
  239. package/vendor/testez/TestBootstrap.lua +146 -146
  240. package/vendor/testez/TestEnum.lua +27 -27
  241. package/vendor/testez/TestPlan.lua +304 -304
  242. package/vendor/testez/TestPlanner.lua +39 -39
  243. package/vendor/testez/TestResults.lua +111 -111
  244. package/vendor/testez/TestRunner.lua +188 -188
  245. package/vendor/testez/TestSession.lua +243 -243
  246. package/vendor/testez/init.lua +39 -39
@@ -1,746 +1,746 @@
1
- -- Log
2
- -- Stephen Leitnick
3
- -- April 20, 2021
4
-
5
- --[[
6
-
7
- IMPORTANT: Only make one logger per script/module
8
-
9
-
10
- Log.Level { Trace, Debug, Info, Warning, Error, Fatal }
11
- Log.TimeUnit { Milliseconds, Seconds, Minutes, Hours, Days, Weeks, Months, Years }
12
-
13
- Constructor:
14
-
15
- logger = Log.new()
16
-
17
-
18
- Log:
19
-
20
- Basic logging at levels:
21
-
22
- logger:AtTrace():Log("Hello from trace")
23
- logger:AtDebug():Log("Hello from debug")
24
- logger:AtInfo():Log("Hello from info")
25
- logger:AtWarning():Log("Hello from warn")
26
- logger:AtError():Log("Hello from error")
27
- logger:AtFatal():Log("Hello from fatal")
28
- logger:At(Log.Level.Warning):Log("Warning!")
29
-
30
-
31
- Log every 10 logs:
32
-
33
- logger:AtInfo():Every(10):Log("Log this only every 10 times")
34
-
35
-
36
- Log at most every 3 seconds:
37
-
38
- logger:AtInfo():AtMostEvery(3, Log.TimeUnit.Seconds):Log("Hello there, but not too often!")
39
-
40
-
41
- Wrap the Log in a function:
42
-
43
- local log = logger:AtDebug():Wrap()
44
- log("Hello")
45
-
46
-
47
- --------------------------------------------------------------------------------------------------------------
48
-
49
- LogConfig: Create a LogConfig ModuleScript anywhere in ReplicatedStorage. The configuration lets developers
50
- tune the lowest logging level based on various environment conditions. The LogConfig will be automatically
51
- required and used to set the log level.
52
-
53
- To set the default configuration for all environments, simply return the log level from the LogConfig:
54
-
55
- return "Info"
56
-
57
- To set a configuration that is different while in Studio:
58
-
59
- return {
60
- Studio = "Debug";
61
- Other = "Warning"; -- "Other" can be anything other than Studio (e.g. could be named "Default")
62
- }
63
-
64
- Fine-tune between server and client:
65
-
66
- return {
67
- Studio = {
68
- Server = "Info";
69
- Client = "Debug";
70
- };
71
- Other = "Warning";
72
- }
73
-
74
- Fine-tune based on PlaceIds:
75
-
76
- return {
77
- Studio = {
78
- Server = "Info";
79
- Client = "Debug";
80
- };
81
- Other = {
82
- PlaceIds = {123456, 234567}
83
- Server = "Severe";
84
- Client = "Warning";
85
- };
86
- }
87
-
88
- Fine-tune based on GameIds:
89
-
90
- return {
91
- Studio = {
92
- Server = "Info";
93
- Client = "Debug";
94
- };
95
- Other = {
96
- GameIds = {123456, 234567}
97
- Server = "Severe";
98
- Client = "Warning";
99
- };
100
- }
101
-
102
- Example of full-scale config with multiple environments:
103
-
104
- return {
105
- Studio = {
106
- Server = "Debug";
107
- Client = "Debug";
108
- };
109
- Dev = {
110
- PlaceIds = {1234567};
111
- Server = "Info";
112
- Client = "Info";
113
- };
114
- Prod = {
115
- PlaceIds = {2345678};
116
- Server = "Severe";
117
- Client = "Warning";
118
- };
119
- Default = "Info";
120
- }
121
-
122
- --]]
123
-
124
- local IS_STUDIO = game:GetService("RunService"):IsStudio()
125
- local IS_SERVER = game:GetService("RunService"):IsServer()
126
-
127
- local AnalyticsService = game:GetService("AnalyticsService")
128
- local HttpService = game:GetService("HttpService")
129
- local Players = game:GetService("Players")
130
- local AnalyticsLogLevel = Enum.AnalyticsLogLevel
131
-
132
- local player = (not IS_SERVER and Players.LocalPlayer or nil)
133
-
134
- local configModule = game:GetService("ReplicatedStorage"):FindFirstChild("LogConfig", true)
135
- local config = (configModule and require(configModule) or "Debug")
136
-
137
- local logLevel = nil
138
- local timeFunc = os.clock
139
-
140
- local logLevels = {
141
- Trace = AnalyticsLogLevel.Trace.Value,
142
- Debug = AnalyticsLogLevel.Debug.Value,
143
- Info = AnalyticsLogLevel.Information.Value,
144
- Warning = AnalyticsLogLevel.Warning.Value,
145
- Error = AnalyticsLogLevel.Error.Value,
146
- Fatal = AnalyticsLogLevel.Fatal.Value,
147
- }
148
-
149
- local timeUnits = {
150
- Milliseconds = 0,
151
- Seconds = 1,
152
- Minutes = 2,
153
- Hours = 3,
154
- Days = 4,
155
- Weeks = 5,
156
- Months = 6,
157
- Years = 7,
158
- }
159
-
160
- local function ToSeconds(n, timeUnit)
161
- if timeUnit == timeUnits.Milliseconds then
162
- return n / 1000
163
- elseif timeUnit == timeUnits.Seconds then
164
- return n
165
- elseif timeUnit == timeUnits.Minutes then
166
- return n * 60
167
- elseif timeUnit == timeUnits.Hours then
168
- return n * 3600
169
- elseif timeUnit == timeUnits.Days then
170
- return n * 86400
171
- elseif timeUnit == timeUnits.Weeks then
172
- return n * 604800
173
- elseif timeUnit == timeUnits.Months then
174
- return n * 2592000
175
- elseif timeUnit == timeUnits.Years then
176
- return n * 31536000
177
- else
178
- error("Unknown time unit", 2)
179
- end
180
- end
181
-
182
- local function GetPlayerFromCustomData(customData)
183
- if type(customData) == "table" then
184
- local id = (customData.Player or customData.PlayerId)
185
- if id then
186
- return Players:GetPlayerByUserId(id)
187
- end
188
- end
189
- return nil
190
- end
191
-
192
- local FireAnalyticsLogEvent
193
- if IS_STUDIO then
194
- FireAnalyticsLogEvent = function(_level, _message, _traceback, _customData) end
195
- else
196
- FireAnalyticsLogEvent = function(level, message, traceback, customData)
197
- local success, err = pcall(function()
198
- local plr = (player or GetPlayerFromCustomData(customData))
199
- AnalyticsService:FireLogEvent(plr, level, message, { stackTrace = traceback }, customData)
200
- end)
201
- if not success then
202
- warn(err)
203
- end
204
- end
205
- end
206
-
207
- local LogItem = {}
208
- LogItem.__index = LogItem
209
-
210
- function LogItem.new(log, levelName, traceback, key)
211
- local self = setmetatable({
212
- _log = log,
213
- _traceback = traceback,
214
- _levelName = levelName,
215
- _modifiers = {
216
- Throw = false,
217
- },
218
- _key = key,
219
- }, LogItem)
220
- return self
221
- end
222
-
223
- function LogItem:_shouldLog(stats)
224
- if self._modifiers.Every and not stats:_checkAndIncrementCount(self._modifiers.Every) then
225
- return false
226
- end
227
- if self._modifiers.AtMostEvery and not stats:_checkLastTimestamp(timeFunc(), self._modifiers.AtMostEvery) then
228
- return false
229
- end
230
- return true
231
- end
232
-
233
- function LogItem:Every(n)
234
- self._modifiers.Every = n
235
- return self
236
- end
237
-
238
- function LogItem:AtMostEvery(n, timeUnit)
239
- self._modifiers.AtMostEvery = ToSeconds(n, timeUnit)
240
- return self
241
- end
242
-
243
- function LogItem:Throw()
244
- self._modifiers.Throw = true
245
- return self
246
- end
247
-
248
- function LogItem:Log(message, customData)
249
- local stats = self._log:_getLogStats(self._key)
250
- if not self:_shouldLog(stats) then
251
- return
252
- end
253
- if type(message) == "function" then
254
- local msg, data = message()
255
- message = msg
256
- if data ~= nil then
257
- customData = data
258
- end
259
- elseif type(message) == "table" then
260
- message = HttpService:JSONEncode(message)
261
- end
262
- stats:_setTimestamp(timeFunc())
263
- local logMessage = ("%s: [%s] %s"):format(self._log._name, self._levelName, message)
264
- local logLevelNum = logLevels[self._levelName]
265
- FireAnalyticsLogEvent(logLevelNum, ("%s: %s"):format(self._log._name, message), self._traceback, customData)
266
- if self._modifiers.Throw then
267
- error(logMessage .. (customData and (" " .. HttpService:JSONEncode(customData)) or ""), 4)
268
- elseif logLevelNum < logLevels.Warning then
269
- print(logMessage, customData or "")
270
- else
271
- warn(logMessage, customData or "")
272
- end
273
- end
274
-
275
- function LogItem:Wrap()
276
- return function(...)
277
- self:Log(...)
278
- end
279
- end
280
-
281
- function LogItem:Assert(condition, ...)
282
- if condition then
283
- self:Throw():Log(...)
284
- end
285
- end
286
-
287
- local LogItemBlank = {}
288
- LogItemBlank.__index = LogItemBlank
289
- setmetatable(LogItemBlank, LogItem)
290
-
291
- function LogItemBlank.new(...)
292
- local self = setmetatable(LogItem.new(...), LogItemBlank)
293
- return self
294
- end
295
-
296
- function LogItemBlank:Log()
297
- -- Do nothing
298
- end
299
-
300
- local LogStats = {}
301
- LogStats.__index = LogStats
302
-
303
- function LogStats.new()
304
- local self = setmetatable({}, LogStats)
305
- self._invocationCount = 0
306
- self._lastTimestamp = 0
307
- return self
308
- end
309
-
310
- function LogStats:_checkAndIncrementCount(rateLimit)
311
- local check = ((self._invocationCount % rateLimit) == 0)
312
- self._invocationCount += 1
313
- return check
314
- end
315
-
316
- function LogStats:_checkLastTimestamp(now, intervalSeconds)
317
- return ((now - self._lastTimestamp) >= intervalSeconds)
318
- end
319
-
320
- function LogStats:_setTimestamp(now)
321
- self._lastTimestamp = now
322
- end
323
-
324
- --[=[
325
- @class Log
326
- @server
327
- Log class for logging to the AnalyticsService (e.g. PlayFab). The API
328
- is based off of Google's [Flogger](https://google.github.io/flogger/)
329
- fluent logging API.
330
-
331
- ```lua
332
- local Log = require(somewhere.Log)
333
- local logger = Log.new()
334
-
335
- -- Log a simple message:
336
- logger:AtInfo():Log("Hello world!")
337
-
338
- -- Log only every 3 messages:
339
- for i = 1,20 do
340
- logger:AtInfo():Every(3):Log("Hi there!")
341
- end
342
-
343
- -- Log only every 1 second:
344
- for i = 1,100 do
345
- logger:AtInfo():AtMostEvery(3, Log.TimeUnit.Seconds):Log("Hello!")
346
- task.wait(0.1)
347
- end
348
-
349
- -- Wrap the above example into a function:
350
- local log = logger:AtInfo():AtMostEvery(3, Log.TimeUnit.Seconds):Wrap()
351
- for i = 1,100 do
352
- log("Hello!")
353
- task.wait(0.1)
354
- end
355
-
356
- -- Assertion:
357
- logger:Assert(typeof(32) == "number", "Somehow 32 is no longer a number")
358
- ```
359
-
360
- ------------
361
-
362
- ### LogConfig
363
-
364
- A LogConfig ModuleScript is expected to exist somewhere within ReplicatedStorage
365
- as well. This ModuleScript defines the behavior for the logger. If not found,
366
- the logger will default to the Debug log level for all operations.
367
-
368
- For instance, this could be a script located at `ReplicatedStorage.MyGameConfig.LogConfig`. There
369
- just needs to be some `LogConfig`-named ModuleScript within ReplicatedStorage.
370
-
371
- Below are a few examples of possible LogConfig ModuleScripts:
372
-
373
- ```lua
374
- -- Set "Info" as default log level for all environments:
375
- return "Info"
376
- ```
377
-
378
- ```lua
379
- -- To set a configuration that is different while in Studio:
380
- return {
381
- Studio = "Debug";
382
- Other = "Warning"; -- "Other" can be anything other than Studio (e.g. could be named "Default")
383
- }
384
- ```
385
-
386
- ```lua
387
- -- Fine-tune between server and client:
388
- return {
389
- Studio = {
390
- Server = "Info";
391
- Client = "Debug";
392
- };
393
- Other = "Warning";
394
- }
395
- ```
396
-
397
- ```lua
398
- -- Fine-tune based on PlaceIds:
399
- return {
400
- Studio = {
401
- Server = "Info";
402
- Client = "Debug";
403
- };
404
- Other = {
405
- PlaceIds = {123456, 234567}
406
- Server = "Severe";
407
- Client = "Warning";
408
- };
409
- }
410
- ```
411
-
412
- ```lua
413
- -- Fine-tune based on GameIds:
414
- return {
415
- Studio = {
416
- Server = "Info";
417
- Client = "Debug";
418
- };
419
- Other = {
420
- GameIds = {123456, 234567}
421
- Server = "Severe";
422
- Client = "Warning";
423
- };
424
- }
425
- ```
426
-
427
- ```lua
428
- -- Example of full-scale config with multiple environments:
429
- return {
430
- Studio = {
431
- Server = "Debug";
432
- Client = "Debug";
433
- };
434
- Dev = {
435
- PlaceIds = {1234567};
436
- Server = "Info";
437
- Client = "Info";
438
- };
439
- Prod = {
440
- PlaceIds = {2345678};
441
- Server = "Severe";
442
- Client = "Warning";
443
- };
444
- Default = "Info";
445
- }
446
- ```
447
- ]=]
448
- local Log = {}
449
- Log.__index = Log
450
-
451
- --[=[
452
- @within Log
453
- @interface LogItem
454
- .Log (message: any, customData: table?) -- Log the message
455
- .Every (n: number) -- Log only every `n` times
456
- .AtMostEvery (n: number, timeUnit: TimeUnit) -- Log only every `n` `TimeUnit`
457
- .Throw () -- Throw an error
458
- .Wrap () -- Returns a function that can be called which will log out the given arguments
459
- .Assert (condition: boolean, args: ...) -- Assert the condition
460
- ]=]
461
-
462
- --[=[
463
- @within Log
464
- @interface TimeUnit
465
- .Milliseconds number
466
- .Seeconds number
467
- .Minutes number
468
- .Hours number
469
- .Days number
470
- .Weeks number
471
- .Months number
472
- .Years number
473
- ]=]
474
-
475
- --[=[
476
- @within Log
477
- @interface Level
478
- .Trace number
479
- .Debug number
480
- .Info number
481
- .Warning number
482
- .Error number
483
- .Fatal number
484
- ]=]
485
-
486
- --[=[
487
- @within Log
488
- @prop TimeUnit TimeUnit
489
- @readonly
490
- ]=]
491
-
492
- --[=[
493
- @within Log
494
- @prop Level Level
495
- @readonly
496
- ]=]
497
-
498
- Log.TimeUnit = timeUnits
499
- Log.Level = logLevels
500
-
501
- Log.LevelNames = {}
502
- for name, num in pairs(Log.Level) do
503
- Log.LevelNames[num] = name
504
- end
505
-
506
- --[=[
507
- @return Log
508
- Construct a new Log object.
509
-
510
- :::warning
511
- This should only be called once per script.
512
- :::
513
- ]=]
514
- function Log.new()
515
- local self = setmetatable({}, Log)
516
- local name = debug.info(2, "s"):match("([^%.]-)$")
517
- self._name = name
518
- self._stats = {}
519
- return self
520
- end
521
-
522
- function Log:_getLogStats(key)
523
- local stats = self._stats[key]
524
- if not stats then
525
- stats = LogStats.new()
526
- self._stats[key] = stats
527
- end
528
- return stats
529
- end
530
-
531
- function Log:_at(level)
532
- local l, f = debug.info(3, "lf")
533
- local traceback = debug.traceback("Log", 3)
534
- local key = (tostring(l) .. tostring(f))
535
- if level < logLevel then
536
- return LogItemBlank.new(self, Log.LevelNames[level], traceback, key)
537
- else
538
- return LogItem.new(self, Log.LevelNames[level], traceback, key)
539
- end
540
- end
541
-
542
- --[=[
543
- @param level LogLevel
544
- @return LogItem
545
- ]=]
546
- function Log:At(level)
547
- return self:_at(level)
548
- end
549
-
550
- --[=[
551
- @return LogItem
552
- Get a LogItem at the Trace log level.
553
- ]=]
554
- function Log:AtTrace()
555
- return self:_at(Log.Level.Trace)
556
- end
557
-
558
- --[=[
559
- @return LogItem
560
- Get a LogItem at the Debug log level.
561
- ]=]
562
- function Log:AtDebug()
563
- return self:_at(Log.Level.Debug)
564
- end
565
-
566
- --[=[
567
- @return LogItem
568
- Get a LogItem at the Info log level.
569
- ]=]
570
- function Log:AtInfo()
571
- return self:_at(Log.Level.Info)
572
- end
573
-
574
- --[=[
575
- @return LogItem
576
- Get a LogItem at the Warning log level.
577
- ]=]
578
- function Log:AtWarning()
579
- return self:_at(Log.Level.Warning)
580
- end
581
-
582
- --[=[
583
- @return LogItem
584
- Get a LogItem at the Error log level.
585
- ]=]
586
- function Log:AtError()
587
- return self:_at(Log.Level.Error)
588
- end
589
-
590
- --[=[
591
- @return LogItem
592
- Get a LogItem at the Fatal log level.
593
- ]=]
594
- function Log:AtFatal()
595
- return self:_at(Log.Level.Fatal)
596
- end
597
-
598
- --[=[
599
- @param condition boolean
600
- @param ... any
601
- Asserts the condition and then logs the following
602
- arguments at the Error level if the condition
603
- fails.
604
- ]=]
605
- function Log:Assert(condition, ...)
606
- if not condition then
607
- self:_at(Log.Level.Error):Throw():Log(...)
608
- end
609
- end
610
-
611
- function Log:Destroy() end
612
-
613
- function Log:__tostring()
614
- return ("Log<%s>"):format(self._name)
615
- end
616
-
617
- -- Determine log level:
618
- do
619
- local function SetLogLevel(name)
620
- local n = name:lower()
621
- for levelName, level in pairs(Log.Level) do
622
- if levelName:lower() == n then
623
- if IS_STUDIO then
624
- local attr = (IS_SERVER and "LogLevel" or "LogLevelClient")
625
- local displayName = (n:sub(1, 1):upper() .. n:sub(2))
626
- if tostring(workspace:GetAttribute(attr) or "") ~= displayName then
627
- workspace:SetAttribute(attr, displayName)
628
- end
629
- end
630
- logLevel = level
631
- return
632
- end
633
- end
634
- error("Unknown log level: " .. tostring(name))
635
- end
636
- local configType = type(config)
637
- assert(
638
- configType == "table" or configType == "string",
639
- "LogConfig must return a table or a string; got " .. configType
640
- )
641
- if configType == "string" then
642
- SetLogLevel(config)
643
- else
644
- if IS_STUDIO and config.Studio then
645
- local studioConfigType = type(config.Studio)
646
- assert(
647
- studioConfigType == "table" or studioConfigType == "string",
648
- "LogConfig.Studio must be a table or a string; got " .. studioConfigType
649
- )
650
- if studioConfigType == "string" then
651
- -- Config for Studio:
652
- SetLogLevel(config.Studio)
653
- else
654
- -- Server/Client config for Studio:
655
- if IS_SERVER then
656
- local studioServerLevel = config.Studio.Server
657
- assert(
658
- type(studioServerLevel) == "string",
659
- "LogConfig.Studio.Server must be a string; got " .. type(studioServerLevel)
660
- )
661
- SetLogLevel(studioServerLevel)
662
- else
663
- local studioClientLevel = config.Studio.Client
664
- assert(
665
- type(studioClientLevel) == "string",
666
- "LogConfig.Studio.Client must be a string; got " .. type(studioClientLevel)
667
- )
668
- SetLogLevel(studioClientLevel)
669
- end
670
- end
671
- else
672
- local default = nil
673
- local numDefault = 0
674
- local set = false
675
- local setK = nil
676
- for k, specialConfig in pairs(config) do
677
- if k == "Studio" then
678
- continue
679
- end
680
- if type(specialConfig) == "string" then
681
- default = specialConfig
682
- numDefault += 1
683
- elseif type(specialConfig) == "table" then
684
- -- Check if config can be used if filtered by PlaceId or GameId:
685
- local canUse, fallthrough = false, false
686
- if type(specialConfig.PlaceId) == "number" then
687
- canUse = (specialConfig.PlaceId == game.PlaceId)
688
- elseif type(specialConfig.PlaceIds) == "table" then
689
- canUse = (table.find(specialConfig.PlaceIds, game.PlaceId) ~= nil)
690
- elseif type(specialConfig.GameId) == "number" then
691
- canUse = (specialConfig.GameId == game.GameId)
692
- elseif type(specialConfig.GameIds) == "table" then
693
- canUse = (table.find(specialConfig.GameIds, game.GameId) ~= nil)
694
- else
695
- canUse = true
696
- fallthrough = true
697
- end
698
- if not fallthrough then
699
- assert(
700
- not set,
701
- ("More than one LogConfig mapping matched (%s and %s)"):format(setK or "", k or "")
702
- )
703
- end
704
- if canUse then
705
- if IS_SERVER then
706
- local serverLevel = specialConfig.Server
707
- assert(
708
- type(serverLevel) == "string",
709
- ("LogConfig.%s.Server must be a string; got %s"):format(k, type(serverLevel))
710
- )
711
- SetLogLevel(serverLevel)
712
- set = true
713
- setK = k
714
- else
715
- local clientLevel = specialConfig.Client
716
- assert(
717
- type(clientLevel) == "string",
718
- ("LogConfig.%s.Client must be a string; got %s"):format(k, type(clientLevel))
719
- )
720
- SetLogLevel(clientLevel)
721
- set = true
722
- setK = k
723
- end
724
- end
725
- else
726
- warn(("LogConfig.%s must be a table or a string; got %s"):format(k, typeof(specialConfig)))
727
- end
728
- end
729
- if numDefault > 1 then
730
- warn("Ambiguous default logging level")
731
- end
732
- if default and not set then
733
- SetLogLevel(default)
734
- end
735
- end
736
- end
737
- assert(type(logLevel) == "number", "LogLevel failed to be determined")
738
- if IS_STUDIO then
739
- local attr = (IS_SERVER and "LogLevel" or "LogLevelClient")
740
- workspace:GetAttributeChangedSignal(attr):Connect(function()
741
- SetLogLevel(workspace:GetAttribute(attr))
742
- end)
743
- end
744
- end
745
-
746
- return Log
1
+ -- Log
2
+ -- Stephen Leitnick
3
+ -- April 20, 2021
4
+
5
+ --[[
6
+
7
+ IMPORTANT: Only make one logger per script/module
8
+
9
+
10
+ Log.Level { Trace, Debug, Info, Warning, Error, Fatal }
11
+ Log.TimeUnit { Milliseconds, Seconds, Minutes, Hours, Days, Weeks, Months, Years }
12
+
13
+ Constructor:
14
+
15
+ logger = Log.new()
16
+
17
+
18
+ Log:
19
+
20
+ Basic logging at levels:
21
+
22
+ logger:AtTrace():Log("Hello from trace")
23
+ logger:AtDebug():Log("Hello from debug")
24
+ logger:AtInfo():Log("Hello from info")
25
+ logger:AtWarning():Log("Hello from warn")
26
+ logger:AtError():Log("Hello from error")
27
+ logger:AtFatal():Log("Hello from fatal")
28
+ logger:At(Log.Level.Warning):Log("Warning!")
29
+
30
+
31
+ Log every 10 logs:
32
+
33
+ logger:AtInfo():Every(10):Log("Log this only every 10 times")
34
+
35
+
36
+ Log at most every 3 seconds:
37
+
38
+ logger:AtInfo():AtMostEvery(3, Log.TimeUnit.Seconds):Log("Hello there, but not too often!")
39
+
40
+
41
+ Wrap the Log in a function:
42
+
43
+ local log = logger:AtDebug():Wrap()
44
+ log("Hello")
45
+
46
+
47
+ --------------------------------------------------------------------------------------------------------------
48
+
49
+ LogConfig: Create a LogConfig ModuleScript anywhere in ReplicatedStorage. The configuration lets developers
50
+ tune the lowest logging level based on various environment conditions. The LogConfig will be automatically
51
+ required and used to set the log level.
52
+
53
+ To set the default configuration for all environments, simply return the log level from the LogConfig:
54
+
55
+ return "Info"
56
+
57
+ To set a configuration that is different while in Studio:
58
+
59
+ return {
60
+ Studio = "Debug";
61
+ Other = "Warning"; -- "Other" can be anything other than Studio (e.g. could be named "Default")
62
+ }
63
+
64
+ Fine-tune between server and client:
65
+
66
+ return {
67
+ Studio = {
68
+ Server = "Info";
69
+ Client = "Debug";
70
+ };
71
+ Other = "Warning";
72
+ }
73
+
74
+ Fine-tune based on PlaceIds:
75
+
76
+ return {
77
+ Studio = {
78
+ Server = "Info";
79
+ Client = "Debug";
80
+ };
81
+ Other = {
82
+ PlaceIds = {123456, 234567}
83
+ Server = "Severe";
84
+ Client = "Warning";
85
+ };
86
+ }
87
+
88
+ Fine-tune based on GameIds:
89
+
90
+ return {
91
+ Studio = {
92
+ Server = "Info";
93
+ Client = "Debug";
94
+ };
95
+ Other = {
96
+ GameIds = {123456, 234567}
97
+ Server = "Severe";
98
+ Client = "Warning";
99
+ };
100
+ }
101
+
102
+ Example of full-scale config with multiple environments:
103
+
104
+ return {
105
+ Studio = {
106
+ Server = "Debug";
107
+ Client = "Debug";
108
+ };
109
+ Dev = {
110
+ PlaceIds = {1234567};
111
+ Server = "Info";
112
+ Client = "Info";
113
+ };
114
+ Prod = {
115
+ PlaceIds = {2345678};
116
+ Server = "Severe";
117
+ Client = "Warning";
118
+ };
119
+ Default = "Info";
120
+ }
121
+
122
+ --]]
123
+
124
+ local IS_STUDIO = game:GetService("RunService"):IsStudio()
125
+ local IS_SERVER = game:GetService("RunService"):IsServer()
126
+
127
+ local AnalyticsService = game:GetService("AnalyticsService")
128
+ local HttpService = game:GetService("HttpService")
129
+ local Players = game:GetService("Players")
130
+ local AnalyticsLogLevel = Enum.AnalyticsLogLevel
131
+
132
+ local player = (not IS_SERVER and Players.LocalPlayer or nil)
133
+
134
+ local configModule = game:GetService("ReplicatedStorage"):FindFirstChild("LogConfig", true)
135
+ local config = (configModule and require(configModule) or "Debug")
136
+
137
+ local logLevel = nil
138
+ local timeFunc = os.clock
139
+
140
+ local logLevels = {
141
+ Trace = AnalyticsLogLevel.Trace.Value,
142
+ Debug = AnalyticsLogLevel.Debug.Value,
143
+ Info = AnalyticsLogLevel.Information.Value,
144
+ Warning = AnalyticsLogLevel.Warning.Value,
145
+ Error = AnalyticsLogLevel.Error.Value,
146
+ Fatal = AnalyticsLogLevel.Fatal.Value,
147
+ }
148
+
149
+ local timeUnits = {
150
+ Milliseconds = 0,
151
+ Seconds = 1,
152
+ Minutes = 2,
153
+ Hours = 3,
154
+ Days = 4,
155
+ Weeks = 5,
156
+ Months = 6,
157
+ Years = 7,
158
+ }
159
+
160
+ local function ToSeconds(n, timeUnit)
161
+ if timeUnit == timeUnits.Milliseconds then
162
+ return n / 1000
163
+ elseif timeUnit == timeUnits.Seconds then
164
+ return n
165
+ elseif timeUnit == timeUnits.Minutes then
166
+ return n * 60
167
+ elseif timeUnit == timeUnits.Hours then
168
+ return n * 3600
169
+ elseif timeUnit == timeUnits.Days then
170
+ return n * 86400
171
+ elseif timeUnit == timeUnits.Weeks then
172
+ return n * 604800
173
+ elseif timeUnit == timeUnits.Months then
174
+ return n * 2592000
175
+ elseif timeUnit == timeUnits.Years then
176
+ return n * 31536000
177
+ else
178
+ error("Unknown time unit", 2)
179
+ end
180
+ end
181
+
182
+ local function GetPlayerFromCustomData(customData)
183
+ if type(customData) == "table" then
184
+ local id = (customData.Player or customData.PlayerId)
185
+ if id then
186
+ return Players:GetPlayerByUserId(id)
187
+ end
188
+ end
189
+ return nil
190
+ end
191
+
192
+ local FireAnalyticsLogEvent
193
+ if IS_STUDIO then
194
+ FireAnalyticsLogEvent = function(_level, _message, _traceback, _customData) end
195
+ else
196
+ FireAnalyticsLogEvent = function(level, message, traceback, customData)
197
+ local success, err = pcall(function()
198
+ local plr = (player or GetPlayerFromCustomData(customData))
199
+ AnalyticsService:FireLogEvent(plr, level, message, { stackTrace = traceback }, customData)
200
+ end)
201
+ if not success then
202
+ warn(err)
203
+ end
204
+ end
205
+ end
206
+
207
+ local LogItem = {}
208
+ LogItem.__index = LogItem
209
+
210
+ function LogItem.new(log, levelName, traceback, key)
211
+ local self = setmetatable({
212
+ _log = log,
213
+ _traceback = traceback,
214
+ _levelName = levelName,
215
+ _modifiers = {
216
+ Throw = false,
217
+ },
218
+ _key = key,
219
+ }, LogItem)
220
+ return self
221
+ end
222
+
223
+ function LogItem:_shouldLog(stats)
224
+ if self._modifiers.Every and not stats:_checkAndIncrementCount(self._modifiers.Every) then
225
+ return false
226
+ end
227
+ if self._modifiers.AtMostEvery and not stats:_checkLastTimestamp(timeFunc(), self._modifiers.AtMostEvery) then
228
+ return false
229
+ end
230
+ return true
231
+ end
232
+
233
+ function LogItem:Every(n)
234
+ self._modifiers.Every = n
235
+ return self
236
+ end
237
+
238
+ function LogItem:AtMostEvery(n, timeUnit)
239
+ self._modifiers.AtMostEvery = ToSeconds(n, timeUnit)
240
+ return self
241
+ end
242
+
243
+ function LogItem:Throw()
244
+ self._modifiers.Throw = true
245
+ return self
246
+ end
247
+
248
+ function LogItem:Log(message, customData)
249
+ local stats = self._log:_getLogStats(self._key)
250
+ if not self:_shouldLog(stats) then
251
+ return
252
+ end
253
+ if type(message) == "function" then
254
+ local msg, data = message()
255
+ message = msg
256
+ if data ~= nil then
257
+ customData = data
258
+ end
259
+ elseif type(message) == "table" then
260
+ message = HttpService:JSONEncode(message)
261
+ end
262
+ stats:_setTimestamp(timeFunc())
263
+ local logMessage = ("%s: [%s] %s"):format(self._log._name, self._levelName, message)
264
+ local logLevelNum = logLevels[self._levelName]
265
+ FireAnalyticsLogEvent(logLevelNum, ("%s: %s"):format(self._log._name, message), self._traceback, customData)
266
+ if self._modifiers.Throw then
267
+ error(logMessage .. (customData and (" " .. HttpService:JSONEncode(customData)) or ""), 4)
268
+ elseif logLevelNum < logLevels.Warning then
269
+ print(logMessage, customData or "")
270
+ else
271
+ warn(logMessage, customData or "")
272
+ end
273
+ end
274
+
275
+ function LogItem:Wrap()
276
+ return function(...)
277
+ self:Log(...)
278
+ end
279
+ end
280
+
281
+ function LogItem:Assert(condition, ...)
282
+ if condition then
283
+ self:Throw():Log(...)
284
+ end
285
+ end
286
+
287
+ local LogItemBlank = {}
288
+ LogItemBlank.__index = LogItemBlank
289
+ setmetatable(LogItemBlank, LogItem)
290
+
291
+ function LogItemBlank.new(...)
292
+ local self = setmetatable(LogItem.new(...), LogItemBlank)
293
+ return self
294
+ end
295
+
296
+ function LogItemBlank:Log()
297
+ -- Do nothing
298
+ end
299
+
300
+ local LogStats = {}
301
+ LogStats.__index = LogStats
302
+
303
+ function LogStats.new()
304
+ local self = setmetatable({}, LogStats)
305
+ self._invocationCount = 0
306
+ self._lastTimestamp = 0
307
+ return self
308
+ end
309
+
310
+ function LogStats:_checkAndIncrementCount(rateLimit)
311
+ local check = ((self._invocationCount % rateLimit) == 0)
312
+ self._invocationCount += 1
313
+ return check
314
+ end
315
+
316
+ function LogStats:_checkLastTimestamp(now, intervalSeconds)
317
+ return ((now - self._lastTimestamp) >= intervalSeconds)
318
+ end
319
+
320
+ function LogStats:_setTimestamp(now)
321
+ self._lastTimestamp = now
322
+ end
323
+
324
+ --[=[
325
+ @class Log
326
+ @server
327
+ Log class for logging to the AnalyticsService (e.g. PlayFab). The API
328
+ is based off of Google's [Flogger](https://google.github.io/flogger/)
329
+ fluent logging API.
330
+
331
+ ```lua
332
+ local Log = require(somewhere.Log)
333
+ local logger = Log.new()
334
+
335
+ -- Log a simple message:
336
+ logger:AtInfo():Log("Hello world!")
337
+
338
+ -- Log only every 3 messages:
339
+ for i = 1,20 do
340
+ logger:AtInfo():Every(3):Log("Hi there!")
341
+ end
342
+
343
+ -- Log only every 1 second:
344
+ for i = 1,100 do
345
+ logger:AtInfo():AtMostEvery(3, Log.TimeUnit.Seconds):Log("Hello!")
346
+ task.wait(0.1)
347
+ end
348
+
349
+ -- Wrap the above example into a function:
350
+ local log = logger:AtInfo():AtMostEvery(3, Log.TimeUnit.Seconds):Wrap()
351
+ for i = 1,100 do
352
+ log("Hello!")
353
+ task.wait(0.1)
354
+ end
355
+
356
+ -- Assertion:
357
+ logger:Assert(typeof(32) == "number", "Somehow 32 is no longer a number")
358
+ ```
359
+
360
+ ------------
361
+
362
+ ### LogConfig
363
+
364
+ A LogConfig ModuleScript is expected to exist somewhere within ReplicatedStorage
365
+ as well. This ModuleScript defines the behavior for the logger. If not found,
366
+ the logger will default to the Debug log level for all operations.
367
+
368
+ For instance, this could be a script located at `ReplicatedStorage.MyGameConfig.LogConfig`. There
369
+ just needs to be some `LogConfig`-named ModuleScript within ReplicatedStorage.
370
+
371
+ Below are a few examples of possible LogConfig ModuleScripts:
372
+
373
+ ```lua
374
+ -- Set "Info" as default log level for all environments:
375
+ return "Info"
376
+ ```
377
+
378
+ ```lua
379
+ -- To set a configuration that is different while in Studio:
380
+ return {
381
+ Studio = "Debug";
382
+ Other = "Warning"; -- "Other" can be anything other than Studio (e.g. could be named "Default")
383
+ }
384
+ ```
385
+
386
+ ```lua
387
+ -- Fine-tune between server and client:
388
+ return {
389
+ Studio = {
390
+ Server = "Info";
391
+ Client = "Debug";
392
+ };
393
+ Other = "Warning";
394
+ }
395
+ ```
396
+
397
+ ```lua
398
+ -- Fine-tune based on PlaceIds:
399
+ return {
400
+ Studio = {
401
+ Server = "Info";
402
+ Client = "Debug";
403
+ };
404
+ Other = {
405
+ PlaceIds = {123456, 234567}
406
+ Server = "Severe";
407
+ Client = "Warning";
408
+ };
409
+ }
410
+ ```
411
+
412
+ ```lua
413
+ -- Fine-tune based on GameIds:
414
+ return {
415
+ Studio = {
416
+ Server = "Info";
417
+ Client = "Debug";
418
+ };
419
+ Other = {
420
+ GameIds = {123456, 234567}
421
+ Server = "Severe";
422
+ Client = "Warning";
423
+ };
424
+ }
425
+ ```
426
+
427
+ ```lua
428
+ -- Example of full-scale config with multiple environments:
429
+ return {
430
+ Studio = {
431
+ Server = "Debug";
432
+ Client = "Debug";
433
+ };
434
+ Dev = {
435
+ PlaceIds = {1234567};
436
+ Server = "Info";
437
+ Client = "Info";
438
+ };
439
+ Prod = {
440
+ PlaceIds = {2345678};
441
+ Server = "Severe";
442
+ Client = "Warning";
443
+ };
444
+ Default = "Info";
445
+ }
446
+ ```
447
+ ]=]
448
+ local Log = {}
449
+ Log.__index = Log
450
+
451
+ --[=[
452
+ @within Log
453
+ @interface LogItem
454
+ .Log (message: any, customData: table?) -- Log the message
455
+ .Every (n: number) -- Log only every `n` times
456
+ .AtMostEvery (n: number, timeUnit: TimeUnit) -- Log only every `n` `TimeUnit`
457
+ .Throw () -- Throw an error
458
+ .Wrap () -- Returns a function that can be called which will log out the given arguments
459
+ .Assert (condition: boolean, args: ...) -- Assert the condition
460
+ ]=]
461
+
462
+ --[=[
463
+ @within Log
464
+ @interface TimeUnit
465
+ .Milliseconds number
466
+ .Seeconds number
467
+ .Minutes number
468
+ .Hours number
469
+ .Days number
470
+ .Weeks number
471
+ .Months number
472
+ .Years number
473
+ ]=]
474
+
475
+ --[=[
476
+ @within Log
477
+ @interface Level
478
+ .Trace number
479
+ .Debug number
480
+ .Info number
481
+ .Warning number
482
+ .Error number
483
+ .Fatal number
484
+ ]=]
485
+
486
+ --[=[
487
+ @within Log
488
+ @prop TimeUnit TimeUnit
489
+ @readonly
490
+ ]=]
491
+
492
+ --[=[
493
+ @within Log
494
+ @prop Level Level
495
+ @readonly
496
+ ]=]
497
+
498
+ Log.TimeUnit = timeUnits
499
+ Log.Level = logLevels
500
+
501
+ Log.LevelNames = {}
502
+ for name, num in pairs(Log.Level) do
503
+ Log.LevelNames[num] = name
504
+ end
505
+
506
+ --[=[
507
+ @return Log
508
+ Construct a new Log object.
509
+
510
+ :::warning
511
+ This should only be called once per script.
512
+ :::
513
+ ]=]
514
+ function Log.new()
515
+ local self = setmetatable({}, Log)
516
+ local name = debug.info(2, "s"):match("([^%.]-)$")
517
+ self._name = name
518
+ self._stats = {}
519
+ return self
520
+ end
521
+
522
+ function Log:_getLogStats(key)
523
+ local stats = self._stats[key]
524
+ if not stats then
525
+ stats = LogStats.new()
526
+ self._stats[key] = stats
527
+ end
528
+ return stats
529
+ end
530
+
531
+ function Log:_at(level)
532
+ local l, f = debug.info(3, "lf")
533
+ local traceback = debug.traceback("Log", 3)
534
+ local key = (tostring(l) .. tostring(f))
535
+ if level < logLevel then
536
+ return LogItemBlank.new(self, Log.LevelNames[level], traceback, key)
537
+ else
538
+ return LogItem.new(self, Log.LevelNames[level], traceback, key)
539
+ end
540
+ end
541
+
542
+ --[=[
543
+ @param level LogLevel
544
+ @return LogItem
545
+ ]=]
546
+ function Log:At(level)
547
+ return self:_at(level)
548
+ end
549
+
550
+ --[=[
551
+ @return LogItem
552
+ Get a LogItem at the Trace log level.
553
+ ]=]
554
+ function Log:AtTrace()
555
+ return self:_at(Log.Level.Trace)
556
+ end
557
+
558
+ --[=[
559
+ @return LogItem
560
+ Get a LogItem at the Debug log level.
561
+ ]=]
562
+ function Log:AtDebug()
563
+ return self:_at(Log.Level.Debug)
564
+ end
565
+
566
+ --[=[
567
+ @return LogItem
568
+ Get a LogItem at the Info log level.
569
+ ]=]
570
+ function Log:AtInfo()
571
+ return self:_at(Log.Level.Info)
572
+ end
573
+
574
+ --[=[
575
+ @return LogItem
576
+ Get a LogItem at the Warning log level.
577
+ ]=]
578
+ function Log:AtWarning()
579
+ return self:_at(Log.Level.Warning)
580
+ end
581
+
582
+ --[=[
583
+ @return LogItem
584
+ Get a LogItem at the Error log level.
585
+ ]=]
586
+ function Log:AtError()
587
+ return self:_at(Log.Level.Error)
588
+ end
589
+
590
+ --[=[
591
+ @return LogItem
592
+ Get a LogItem at the Fatal log level.
593
+ ]=]
594
+ function Log:AtFatal()
595
+ return self:_at(Log.Level.Fatal)
596
+ end
597
+
598
+ --[=[
599
+ @param condition boolean
600
+ @param ... any
601
+ Asserts the condition and then logs the following
602
+ arguments at the Error level if the condition
603
+ fails.
604
+ ]=]
605
+ function Log:Assert(condition, ...)
606
+ if not condition then
607
+ self:_at(Log.Level.Error):Throw():Log(...)
608
+ end
609
+ end
610
+
611
+ function Log:Destroy() end
612
+
613
+ function Log:__tostring()
614
+ return ("Log<%s>"):format(self._name)
615
+ end
616
+
617
+ -- Determine log level:
618
+ do
619
+ local function SetLogLevel(name)
620
+ local n = name:lower()
621
+ for levelName, level in pairs(Log.Level) do
622
+ if levelName:lower() == n then
623
+ if IS_STUDIO then
624
+ local attr = (IS_SERVER and "LogLevel" or "LogLevelClient")
625
+ local displayName = (n:sub(1, 1):upper() .. n:sub(2))
626
+ if tostring(workspace:GetAttribute(attr) or "") ~= displayName then
627
+ workspace:SetAttribute(attr, displayName)
628
+ end
629
+ end
630
+ logLevel = level
631
+ return
632
+ end
633
+ end
634
+ error("Unknown log level: " .. tostring(name))
635
+ end
636
+ local configType = type(config)
637
+ assert(
638
+ configType == "table" or configType == "string",
639
+ "LogConfig must return a table or a string; got " .. configType
640
+ )
641
+ if configType == "string" then
642
+ SetLogLevel(config)
643
+ else
644
+ if IS_STUDIO and config.Studio then
645
+ local studioConfigType = type(config.Studio)
646
+ assert(
647
+ studioConfigType == "table" or studioConfigType == "string",
648
+ "LogConfig.Studio must be a table or a string; got " .. studioConfigType
649
+ )
650
+ if studioConfigType == "string" then
651
+ -- Config for Studio:
652
+ SetLogLevel(config.Studio)
653
+ else
654
+ -- Server/Client config for Studio:
655
+ if IS_SERVER then
656
+ local studioServerLevel = config.Studio.Server
657
+ assert(
658
+ type(studioServerLevel) == "string",
659
+ "LogConfig.Studio.Server must be a string; got " .. type(studioServerLevel)
660
+ )
661
+ SetLogLevel(studioServerLevel)
662
+ else
663
+ local studioClientLevel = config.Studio.Client
664
+ assert(
665
+ type(studioClientLevel) == "string",
666
+ "LogConfig.Studio.Client must be a string; got " .. type(studioClientLevel)
667
+ )
668
+ SetLogLevel(studioClientLevel)
669
+ end
670
+ end
671
+ else
672
+ local default = nil
673
+ local numDefault = 0
674
+ local set = false
675
+ local setK = nil
676
+ for k, specialConfig in pairs(config) do
677
+ if k == "Studio" then
678
+ continue
679
+ end
680
+ if type(specialConfig) == "string" then
681
+ default = specialConfig
682
+ numDefault += 1
683
+ elseif type(specialConfig) == "table" then
684
+ -- Check if config can be used if filtered by PlaceId or GameId:
685
+ local canUse, fallthrough = false, false
686
+ if type(specialConfig.PlaceId) == "number" then
687
+ canUse = (specialConfig.PlaceId == game.PlaceId)
688
+ elseif type(specialConfig.PlaceIds) == "table" then
689
+ canUse = (table.find(specialConfig.PlaceIds, game.PlaceId) ~= nil)
690
+ elseif type(specialConfig.GameId) == "number" then
691
+ canUse = (specialConfig.GameId == game.GameId)
692
+ elseif type(specialConfig.GameIds) == "table" then
693
+ canUse = (table.find(specialConfig.GameIds, game.GameId) ~= nil)
694
+ else
695
+ canUse = true
696
+ fallthrough = true
697
+ end
698
+ if not fallthrough then
699
+ assert(
700
+ not set,
701
+ ("More than one LogConfig mapping matched (%s and %s)"):format(setK or "", k or "")
702
+ )
703
+ end
704
+ if canUse then
705
+ if IS_SERVER then
706
+ local serverLevel = specialConfig.Server
707
+ assert(
708
+ type(serverLevel) == "string",
709
+ ("LogConfig.%s.Server must be a string; got %s"):format(k, type(serverLevel))
710
+ )
711
+ SetLogLevel(serverLevel)
712
+ set = true
713
+ setK = k
714
+ else
715
+ local clientLevel = specialConfig.Client
716
+ assert(
717
+ type(clientLevel) == "string",
718
+ ("LogConfig.%s.Client must be a string; got %s"):format(k, type(clientLevel))
719
+ )
720
+ SetLogLevel(clientLevel)
721
+ set = true
722
+ setK = k
723
+ end
724
+ end
725
+ else
726
+ warn(("LogConfig.%s must be a table or a string; got %s"):format(k, typeof(specialConfig)))
727
+ end
728
+ end
729
+ if numDefault > 1 then
730
+ warn("Ambiguous default logging level")
731
+ end
732
+ if default and not set then
733
+ SetLogLevel(default)
734
+ end
735
+ end
736
+ end
737
+ assert(type(logLevel) == "number", "LogLevel failed to be determined")
738
+ if IS_STUDIO then
739
+ local attr = (IS_SERVER and "LogLevel" or "LogLevelClient")
740
+ workspace:GetAttributeChangedSignal(attr):Connect(function()
741
+ SetLogLevel(workspace:GetAttribute(attr))
742
+ end)
743
+ end
744
+ end
745
+
746
+ return Log