Skip to content

Commit af65f34

Browse files
authored
Merge pull request #1 from metrik-tech/feature/api-implementation
feature/api-implementation
2 parents 42e2063 + 376aedc commit af65f34

17 files changed

+514
-103
lines changed

Src/API/Action.lua

+44-16
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,19 @@
44

55
local Signal = require(script.Parent.Parent.Packages.Signal)
66

7+
local ActionType = require(script.Parent.Parent.Enums.ActionType)
8+
79
--[=[
810
@class Action
911
1012
Actions enable developers to interact with the Metrik backend, when an action is instantiated the backend is notified so that servers
1113
that have this action is callable from the Metrik site.
1214
]=]
13-
local Action = { }
15+
local Action = {}
1416

15-
Action.Public = { }
16-
Action.Prototype = { }
17-
Action.Instantiated = { }
17+
Action.Public = {}
18+
Action.Prototype = {}
19+
Action.Instantiated = {}
1820

1921
Action.Public.ActionAdded = Signal.new()
2022

@@ -26,7 +28,8 @@ Action.Public.ActionAdded = Signal.new()
2628
@within Action
2729
2830
@return boolean
29-
]=]--
31+
]=]
32+
--
3033
function Action.Prototype.CanActivate(self: Action): boolean
3134
return true
3235
end
@@ -41,7 +44,8 @@ end
4144
@param arguments { [any]: any }
4245
4346
@return arguments { [any]: any }
44-
]=]--
47+
]=]
48+
--
4549
function Action.Prototype.PreRun(self: Action, arguments: { [any]: any }): { [any]: any }
4650
return arguments
4751
end
@@ -55,8 +59,9 @@ end
5559
@param exception string
5660
5761
@return ... any
58-
]=]--
59-
function Action.Prototype.OnRun(self: Action, ...: any): ... any
62+
]=]
63+
--
64+
function Action.Prototype.OnRun(self: Action, ...: any): ...any
6065
return
6166
end
6267

@@ -69,7 +74,8 @@ end
6974
@param exception string
7075
7176
@return ()
72-
]=]--
77+
]=]
78+
--
7379
function Action.Prototype.OnError(self: Action, exception: string): ()
7480
return warn(`Action '{self.Name}' failed to execute ':OnActivated' call with error:\n{exception}`)
7581
end
@@ -88,7 +94,8 @@ end
8894
@param results { any: any }
8995
9096
@return ... any
91-
]=]--
97+
]=]
98+
--
9299
function Action.Prototype.PostRun(self: Action, arguments: { [any]: any }, results: { [any]: any }): ()
93100
return
94101
end
@@ -120,7 +127,8 @@ end
120127
@param arguments { any: any }
121128
122129
@return boolean
123-
]=]--
130+
]=]
131+
--
124132
function Action.Prototype.OnRemoteServerInputRecieved(self: Action, arguments: { [any]: any }): boolean
125133
if not self:CanActivate() then
126134
return false
@@ -175,17 +183,27 @@ end
175183
@param actionSettings { Name: string }
176184
177185
@return Action
178-
]=]--
186+
]=]
187+
--
179188
function Action.Public.new(actionSettings: ActionSettings): Action
180189
assert(not Action.Instantiated[actionSettings.Uuid], `Action '{actionSettings.Uuid}' already exists.`)
181190

182-
local self = setmetatable({ }, {
183-
__index = Action.Prototype
191+
local self = setmetatable({}, {
192+
__index = Action.Prototype,
184193
})
185194

186195
self.Name = actionSettings.Name
187196
self.Uuid = actionSettings.Uuid
188197

198+
self.Arguments = actionSettings.Arguments or {}
199+
200+
for _, argumentObject in self.Arguments do
201+
assert(
202+
ActionType[argumentObject.Type] ~= nil,
203+
`Invalid argument type: '{tostring(argumentObject.Type)}', only accepts '{table.concat(ActionType, ", ")}'`
204+
)
205+
end
206+
189207
Action.Instantiated[actionSettings.Uuid] = self
190208
Action.Public.ActionAdded:Fire(self)
191209

@@ -207,7 +225,8 @@ end
207225
@param actionName string
208226
209227
@return Action?
210-
]=]--
228+
]=]
229+
--
211230
function Action.Public.fromUuid(actionUuid: string): Action?
212231
return Action.Instantiated[actionUuid]
213232
end
@@ -217,6 +236,15 @@ export type Action = typeof(Action.Prototype) & ActionSettings
217236
export type ActionSettings = {
218237
Name: string,
219238
Uuid: string,
239+
240+
Arguments: {
241+
{
242+
Name: string,
243+
Type: string,
244+
Default: any?,
245+
IsRequired: boolean?,
246+
}
247+
}?,
220248
}
221249

222-
return Action.Public
250+
return Action.Public

Src/Actions/DisplayMessageAction.lua

+47
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
local TextService = game:GetService("TextService")
2+
3+
local Action = require(script.Parent.Parent.API.Action)
4+
5+
return function()
6+
local DisplayMessageAction = Action.new({
7+
Name = "Display Message Action",
8+
Uuid = "internal:display-message-action",
9+
10+
Arguments = {
11+
{
12+
Name = "Target Player Id",
13+
Type = "Number",
14+
-- to-do, fix default so that it's a number, not a string
15+
Default = "-1",
16+
IsRequired = false,
17+
},
18+
{
19+
Name = "Server Message",
20+
Type = "String",
21+
Default = "Hello, World!",
22+
IsRequired = true,
23+
}
24+
}
25+
})
26+
27+
function DisplayMessageAction:OnRun(targetPlayerId: number?, serverMessage: string)
28+
local textFilterResult: TextFilterResult = TextService:FilterStringAsync(serverMessage, 1, Enum.TextFilterContext.PublicChat)
29+
local filteredServerMessage
30+
31+
if targetPlayerId then
32+
filteredServerMessage = textFilterResult:GetNonChatStringForUserAsync(targetPlayerId)
33+
else
34+
filteredServerMessage = textFilterResult:GetNonChatStringForBroadcastAsync()
35+
end
36+
37+
if not string.match(filteredServerMessage, "(%S+)") then
38+
filteredServerMessage = "This message was filtered"
39+
end
40+
41+
-- TO-DO: broadcast server message.
42+
43+
warn(`Broadcasting '{filteredServerMessage}' to '{targetPlayerId or 0}'`)
44+
end
45+
46+
return DisplayMessageAction
47+
end

Src/Actions/TestAction.lua

-14
This file was deleted.

Src/Data/ApiPaths.lua

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
local Api = require(script.Parent.Parent.Enums.Api)
2+
3+
return table.freeze({
4+
[Api.BaseUrl] = "api.metrik.app/api",
5+
6+
[Api.ServerStart] = "/server/start",
7+
[Api.ServerEnd] = "/server/stop",
8+
[Api.ServerHeartbeat] = "/server/heartbeat",
9+
10+
[Api.ServerLogBatch] = "/log/error/bulk",
11+
12+
[Api.RegisterAction] = "/actions/register",
13+
})

Src/Data/ErrorFormats.lua

+3-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
local Error = require(script.Parent.Parent.Enums.Error)
22

33
return table.freeze({
4-
[Error.AlreadyInitializedError] = "MetrikSDK has already been initialized."
4+
[Error.AlreadyInitializedError] = "MetrikSDK has already been initialized.",
5+
[Error.ExpectedCallAfterCall] = "Expected '%s' to be called after '%s'",
6+
[Error.InvalidActionArgumentType] = "Invalid argument type '%s' in action '%s'"
57
})

Src/Enums/ActionType.lua

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
return table.freeze({
2+
String = "String",
3+
Number = "Number",
4+
Boolean = "Boolean",
5+
Player = "Player",
6+
})

Src/Enums/Api.lua

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
return table.freeze({
2+
BaseUrl = "BaseUrl",
3+
4+
ServerStart = "ServerStart",
5+
ServerEnd = "ServerEnd",
6+
ServerHeartbeat = "ServerHeartbeat",
7+
8+
ServerLogBatch = "ServerLogBatch",
9+
10+
RegisterAction = "RegisterAction"
11+
})

Src/Enums/Error.lua

+3-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
11
return table.freeze({
2-
AlreadyInitializedError = "AlreadyInitializedError"
2+
AlreadyInitializedError = "AlreadyInitializedError",
3+
ExpectedCallAfterCall = "ExpectedCallAfterCall",
4+
InvalidActionArgumentType = "InvalidActionArgumentType"
35
})

Src/Enums/ServerType.lua

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
return table.freeze({
2+
Public = "Public",
3+
Private = "Private",
4+
Reserved = "Reserved",
5+
Studio = "Studio"
6+
})

Src/Services/ActionService.lua

+76-6
Original file line numberDiff line numberDiff line change
@@ -2,31 +2,69 @@
22
33
]]
44

5+
local Players = game:GetService("Players")
6+
57
local Console = require(script.Parent.Parent.Packages.Console)
6-
local Loader = require(script.Parent.Parent.Packages.Loader)
8+
local Runtime = require(script.Parent.Parent.Packages.Runtime)
79
local State = require(script.Parent.Parent.Packages.State)
810
local Promise = require(script.Parent.Parent.Packages.Promise)
911

12+
local Api = require(script.Parent.Parent.Enums.Api)
13+
local ActionType = require(script.Parent.Parent.Enums.ActionType)
14+
local Error = require(script.Parent.Parent.Enums.Error)
15+
16+
local ErrorFormats = require(script.Parent.Parent.Data.ErrorFormats)
17+
1018
local Action = require(script.Parent.Parent.API.Action)
1119

12-
local ActionService = { }
20+
local ApiService = require(script.Parent.ApiService)
21+
22+
local ActionService = {}
1323

1424
ActionService.Priority = -1
1525
ActionService.Reporter = Console.new(`🎬 {script.Name}`)
1626

17-
ActionService.Actions = { } :: { [string]: Action.Action }
27+
ActionService.Actions = {} :: { [string]: Action.Action }
1828
ActionService.InternalActionsLoaded = State.new(false)
1929

30+
function ActionService.DeserialiseArgumentArray(
31+
self: ActionService,
32+
eventArguments: { [number]: { type: string, name: string, value: any } }
33+
)
34+
local deserialisedArguments = {}
35+
36+
for _, argumentData in eventArguments do
37+
local value
38+
39+
if argumentData.type == string.upper(ActionType.Number) then
40+
value = tonumber(argumentData.value)
41+
elseif argumentData.type == string.upper(ActionType.String) then
42+
value = tostring(argumentData.value)
43+
elseif argumentData.type == string.upper(ActionType.Boolean) then
44+
value = argumentData.value == "true" and true or false
45+
elseif argumentData.type == string.upper(ActionType.Player) then
46+
local playerId = argumentData.value
47+
local player = Players:GetPlayerByUserId(playerId)
48+
49+
value = player
50+
end
51+
52+
table.insert(deserialisedArguments, value)
53+
end
54+
55+
return deserialisedArguments
56+
end
57+
2058
function ActionService.InvokeActionAsync(self: ActionService, actionUuid: string, eventArguments: { [any]: any })
2159
local actionObject = Action.fromUuid(actionUuid)
2260

2361
return Promise.try(function()
24-
return actionObject:OnRemoteServerInputRecieved(eventArguments)
62+
return actionObject:OnRemoteServerInputRecieved(self:DeserialiseArgumentArray(eventArguments))
2563
end)
2664
end
2765

2866
function ActionService.OnStart(self: ActionService)
29-
local Actions = Loader.LoadDescendants(script.Parent.Parent.Actions)
67+
local Actions = Runtime:RequireDescendants(script.Parent.Parent.Actions)
3068

3169
for actionModuleName, actionConstructorFunction in Actions do
3270
local actionObject = actionConstructorFunction()
@@ -38,6 +76,38 @@ function ActionService.OnStart(self: ActionService)
3876
self.InternalActionsLoaded:Set(true)
3977
end
4078

79+
function ActionService.OnInit(self: ActionService)
80+
Action.ActionAdded:Connect(function(actionObject: Action.Action)
81+
local camelCaseActionArguments = {}
82+
83+
if actionObject.Arguments then
84+
for index, actionMetadata in next, actionObject.Arguments do
85+
self.Reporter:Assert(
86+
ActionType[actionMetadata.Type] ~= nil,
87+
string.format(ErrorFormats[Error.InvalidActionArgumentType], actionMetadata.Type, actionObject.Name)
88+
)
89+
90+
camelCaseActionArguments[index] = {
91+
["default"] = actionMetadata.Default,
92+
["required"] = actionMetadata.IsRequired,
93+
["name"] = actionMetadata.Name,
94+
["type"] = string.upper(actionMetadata.Type),
95+
}
96+
end
97+
end
98+
99+
ApiService:PostAsync(Api.RegisterAction, {
100+
["serverId"] = ApiService.JobId,
101+
["placeVersion"] = tostring(game.PlaceVersion),
102+
["key"] = actionObject.Uuid,
103+
["name"] = actionObject.Name,
104+
["arguments"] = camelCaseActionArguments,
105+
}):andThen(function()
106+
self.Reporter:Log(`Registered action '{actionObject.Name}' with metrik backend`)
107+
end)
108+
end)
109+
end
110+
41111
export type ActionService = typeof(ActionService)
42112

43-
return ActionService
113+
return ActionService

0 commit comments

Comments
 (0)