321 lines
12 KiB
Lua
321 lines
12 KiB
Lua
JSONSettings = ...
|
|
local placeId = JSONSettings["PlaceId"]
|
|
local universeId = JSONSettings["UniverseId"]
|
|
local matchmakingContextId = JSONSettings["MatchmakingContextId"]
|
|
local gameCode = JSONSettings["GameCode"]
|
|
local baseUrl = JSONSettings["BaseUrl"]
|
|
local gameId = JSONSettings["GameId"]
|
|
local machineAddress = JSONSettings["MachineAddress"]
|
|
local gsmInterval = JSONSettings["GsmInterval"]
|
|
local maxPlayers = JSONSettings["MaxPlayers"]
|
|
local maxGameInstances = JSONSettings["MaxGameInstances"]
|
|
local apiKey = JSONSettings["ApiKey"]
|
|
local preferredPlayerCapacity = JSONSettings["PreferredPlayerCapacity"]
|
|
local placeVisitAccessKey = JSONSettings["PlaceVisitAccessKey"]
|
|
|
|
local access = true
|
|
local isCloudEdit = matchmakingContextId == 3
|
|
local assetGameUrl = "https://assetgame." .. baseUrl
|
|
|
|
-- Monitor Game Status LUA Script
|
|
-- Reports game stats to a web handler on regular intervals
|
|
|
|
-- Assumes the following local variables have been defined:
|
|
-- url - root url for the environment (e.g. www.roblox.com)
|
|
-- placeId - place being run
|
|
-- maxPlayers - maximum number of players allowed in this game
|
|
-- preferredPlayerCapacity - number of player slots reserved for social matchmaking
|
|
-- machineAddress - IP address of the server
|
|
-- gsmInterval - send the message no matter what on this interval
|
|
-- baseUrl - base url to build other urls
|
|
-- isCloudEdit - if the current server context is cloudEdit
|
|
-- -----------------------------------------------------------
|
|
|
|
startTime = tick()
|
|
print("yo yo does this work??")
|
|
networkServer = game:GetService("NetworkServer")
|
|
playersService = game:GetService("Players")
|
|
httpService = game:GetService("HttpService")
|
|
pcall(function() playersService.MaxPlayers = maxPlayers end)
|
|
pcall(function() playersService.MaxPlayersInternal = maxPlayers end)
|
|
pcall(function() playersService.PreferredPlayersInternal = preferredPlayerCapacity end)
|
|
gsmUrl = "https://games.api." .. baseUrl
|
|
gameInstancesApiUrl = "https://gameinstances.api." .. baseUrl
|
|
apiProxyUrl = "https://api." .. baseUrl
|
|
playerJoinTimes = {}
|
|
|
|
local RCCKickDupeExists, RCCKickDupeEnabled = pcall(function()
|
|
return settings():GetFFlag("RCCKickDuplicatePlayersOnJoin")
|
|
end)
|
|
|
|
local kickDuplicatePlayersRCC = RCCKickDupeExists and RCCKickDupeEnabled and not isCloudEdit;
|
|
|
|
function UrlEncode(s)
|
|
s = string.gsub(s, "([&=+%c])", function (c)
|
|
return string.format("%%%02X", string.byte(c))
|
|
end)
|
|
s = string.gsub(s, " ", "+")
|
|
return s
|
|
end
|
|
|
|
|
|
function Split(str, pat)
|
|
local t = {} -- NOTE: use {n = 0} in Lua-5.0
|
|
local fpat = "(.-)" .. pat
|
|
local last_end = 1
|
|
local s, e, cap = str:find(fpat, 1)
|
|
while s do
|
|
if s ~= 1 or cap ~= "" then
|
|
table.insert(t,cap)
|
|
end
|
|
last_end = e + 1
|
|
s, e, cap = str:find(fpat, last_end)
|
|
end
|
|
if last_end <= #str then
|
|
cap = str:sub(last_end)
|
|
table.insert(t, cap)
|
|
end
|
|
return t
|
|
end
|
|
|
|
|
|
function CalculateAveragePing()
|
|
local totalPing = 0
|
|
local replicatorCount = 0
|
|
local averagePing = 0
|
|
local status, err = pcall(function()
|
|
for _, r in ipairs(stats().Network:GetChildren()) do
|
|
if r.Name ~= "Packets Thread" then
|
|
r:GetValue() -- hax
|
|
totalPing = totalPing + r.Ping:GetValue()
|
|
replicatorCount = replicatorCount + 1
|
|
end
|
|
end
|
|
if replicatorCount > 0 then
|
|
averagePing = totalPing / replicatorCount
|
|
end
|
|
end)
|
|
if (not status) then
|
|
PrintDebugMessage("CalculateAveragePing error = " .. err)
|
|
end
|
|
return averagePing
|
|
end
|
|
|
|
|
|
function PrintDebugMessage(message)
|
|
if message then
|
|
-- print ("!GameServiceMonitor: " .. message)
|
|
-- game:HttpPost(gsmUrl .. "/v1.0/LogLuaMessage?&apiKey=" .. apiKey .. "&text=" .. UrlEncode(message), "")
|
|
end
|
|
end
|
|
|
|
function PrintErrorMessage(message)
|
|
if message then
|
|
print ("!GameServiceMonitor: " .. message)
|
|
-- game:HttpPost(gsmUrl .. "/v1.0/LogLuaMessage?&apiKey=" .. apiKey .. "&text=" .. UrlEncode(message), "")
|
|
end
|
|
end
|
|
|
|
function UpdatePresence(player, isDisconnect)
|
|
local fullUrl = apiProxyUrl .. "/presence/"
|
|
local queryParams = "?visitorId=" .. player.userId
|
|
|
|
if isDisconnect then
|
|
fullUrl = fullUrl .. "register-absence"
|
|
else
|
|
fullUrl = fullUrl .. "register-game-presence"
|
|
queryParams = queryParams .. "&placeId=" .. placeId .. "&gameId=" .. gameId .. "&locationType=" .. ((isCloudEdit or matchmakingContextId == 4) and "CloudEdit" or "Game")
|
|
end
|
|
|
|
fullUrl = fullUrl .. queryParams
|
|
|
|
PrintDebugMessage("Calling Api Proxy to update presence. URL: " .. fullUrl)
|
|
|
|
game:HttpPost(fullUrl, "")
|
|
return true
|
|
end
|
|
|
|
function GetPlayerPostData()
|
|
local sessionArray = {}
|
|
for _, player in ipairs(playersService:GetPlayers()) do
|
|
local t =
|
|
{
|
|
UserId = player.userId,
|
|
IsVr = player.VRDevice ~= "",
|
|
GameTimeWhenJoined = playerJoinTimes[player.userId],
|
|
GameSessionId = player:GetGameSessionID()
|
|
}
|
|
table.insert(sessionArray, t)
|
|
end
|
|
return httpService:JSONEncode({GameSessions = sessionArray})
|
|
end
|
|
|
|
function SendMessageToGamesApi(source, player)
|
|
local postDataJson = ""
|
|
local playerCSV = ""
|
|
postDataJson = GetPlayerPostData()
|
|
|
|
local w = stats().Workspace
|
|
local gameTime = tick() - startTime
|
|
|
|
local fullUrl = ""
|
|
fullUrl = gsmUrl .. "/v2.0/Refresh/?apiKey=" .. apiKey
|
|
fullUrl = fullUrl .. "&gameId=" .. gameId .."&placeId=" .. placeId .."&gameCapacity=" .. maxPlayers
|
|
fullUrl = fullUrl .. "&maximumGameInstances=" .. maxGameInstances .."&ipAddress=" .. machineAddress
|
|
fullUrl = fullUrl .. "&port=" .. networkServer.Port .."&clientCount=" .. networkServer:GetClientCount()
|
|
fullUrl = fullUrl .. "&gameTime=" .. gameTime
|
|
fullUrl = fullUrl .. "&preferredPlayerCapacity=" .. preferredPlayerCapacity
|
|
fullUrl = fullUrl .. "&eventSource=" .. source
|
|
if player ~= nil then
|
|
fullUrl = fullUrl .. "&originatingPlayerId=" .. player.userId
|
|
end
|
|
fullUrl = fullUrl .. "&gameCode="
|
|
if gameCode ~= nil then
|
|
fullUrl = fullUrl .. gameCode
|
|
end
|
|
fullUrl = fullUrl .. "&matchmakingContextId=" .. matchmakingContextId
|
|
fullUrl = fullUrl .. "&isCloudEdit=" .. ((isCloudEdit or matchmakingContextId == 4) and "true" or "false")
|
|
fullUrl = fullUrl .. "&rccVersion=" .. version()
|
|
|
|
PrintDebugMessage("Calling Games API. Source: " .. source .. ", URL: " .. fullUrl .. " Post data (JSON}:" .. postDataJson)
|
|
|
|
game:HttpPost(fullUrl, postDataJson, false, "application/json")
|
|
return true
|
|
end
|
|
|
|
|
|
function SendMessageToGameInstancesApi(source)
|
|
local postDataJson = ""
|
|
postDataJson = GetPlayerPostData()
|
|
|
|
local w = stats().Workspace
|
|
local gameTime = tick() - startTime
|
|
local averagePing = CalculateAveragePing()
|
|
|
|
local fullUrl = ""
|
|
fullUrl = gameInstancesApiUrl .. "/v2/CreateOrUpdate/?apiKey=" .. apiKey
|
|
fullUrl = fullUrl .. "&gameId=" .. gameId .."&placeId=" .. placeId .. "&gameCapacity=" .. maxPlayers
|
|
fullUrl = fullUrl .. "&maximumGameInstances=" .. maxGameInstances .. "&serverIpAddress=" .. machineAddress
|
|
fullUrl = fullUrl .. "&serverPort=" .. networkServer.Port .. "&fps=" .. w.FPS:GetValue()
|
|
fullUrl = fullUrl .. "&heartbeatRate=" .. w.Heartbeat:GetValue()
|
|
fullUrl = fullUrl .. "&ping=" .. averagePing .. "&gameTime=" .. gameTime
|
|
fullUrl = fullUrl .. "&universeId=" .. universeId
|
|
fullUrl = fullUrl .. "&gameCode="
|
|
if gameCode ~= nil then
|
|
fullUrl = fullUrl .. gameCode
|
|
end
|
|
fullUrl = fullUrl .. "&matchmakingContextId=" .. matchmakingContextId
|
|
|
|
PrintDebugMessage("Calling Game Instances API. Source: " .. source .. ", URL: " .. fullUrl .. " Post data (json): " .. postDataJson)
|
|
|
|
game:HttpPost(fullUrl, postDataJson, false, "application/json")
|
|
return true
|
|
end
|
|
|
|
-- Send updates to Games API and Game Instances API every gsmInterval seconds
|
|
delay(0, function()
|
|
while networkServer.Port == 0 do
|
|
wait(1)
|
|
end
|
|
|
|
while true do
|
|
-- always send on gsmInterval, we also send on events when player join and leave.
|
|
pcall(function() return SendMessageToGamesApi("HeartBeat", nil) end)
|
|
pcall(function() return SendMessageToGameInstancesApi("HeartBeat") end)
|
|
wait(gsmInterval)
|
|
end
|
|
end
|
|
)
|
|
|
|
-- Events that make HTTP calls back to endpoints tracking player activity
|
|
|
|
local function onPlayerConnectingReportClientPresence(player)
|
|
if assetGameUrl and access and placeId and player and player.userId then
|
|
local didTeleportIn = "False"
|
|
if player.TeleportedIn then didTeleportIn = "True" end
|
|
|
|
game:HttpGet(assetGameUrl .. "/Game/ClientPresence.ashx?action=connect&PlaceID=" .. placeId .. "&UserID=" .. player.userId)
|
|
if not isCloudEdit then
|
|
game:HttpPost(assetGameUrl .. "/Game/PlaceVisit.ashx?UserID=" .. player.userId .. "&AssociatedPlaceID=" .. placeId .. "&placeVisitAccessKey=" .. placeVisitAccessKey .. "&IsTeleport=" .. didTeleportIn, "")
|
|
end
|
|
end
|
|
end
|
|
|
|
local function onPlayerConnectingReportClientPresence2(player)
|
|
playerJoinTimes[player.userId] = tick() - startTime
|
|
|
|
-- Games API
|
|
local success, err = pcall(function() return SendMessageToGamesApi("PlayerAdded", player) end)
|
|
if (not success) then
|
|
PrintErrorMessage("playersService.PlayerAdded error updating games api = " .. err)
|
|
end
|
|
|
|
-- Game Instances API
|
|
success, err = pcall(function() return SendMessageToGameInstancesApi("PlayerAdded") end )
|
|
if (not success) then
|
|
PrintErrorMessage("playersService.PlayerAdded error updating game instances api = " .. err)
|
|
end
|
|
|
|
-- Api Proxy - Presence
|
|
success, err = pcall(function() return UpdatePresence(player, false) end)
|
|
if (not success) then
|
|
PrintErrorMessage("playersService.PlayerAdded error updating presence = " .. err)
|
|
end
|
|
end
|
|
|
|
local function onPlayerDisconnectingReportClientPresence(player)
|
|
local isTeleportingOut = "False"
|
|
if player.Teleported then isTeleportingOut = "True" end
|
|
|
|
if assetGameUrl and access and placeId and player and player.userId then
|
|
game:HttpGet(assetGameUrl .. "/Game/ClientPresence.ashx?action=disconnect&PlaceID=" .. placeId .. "&UserID=" .. player.userId .. "&IsTeleport=" .. isTeleportingOut)
|
|
end
|
|
end
|
|
|
|
local function onPlayerDisconnectingReportClientPresence2(player)
|
|
playerJoinTimes[player.userId] = nil
|
|
|
|
-- Games API
|
|
local success, err = pcall(function() return SendMessageToGamesApi("PlayerRemoving", player) end)
|
|
if (not success) then
|
|
PrintErrorMessage("playersService.PlayerRemoving error updating games api = " .. err)
|
|
end
|
|
|
|
-- Game Instances API
|
|
success, err = pcall(function() return SendMessageToGameInstancesApi("PlayerRemoving") end)
|
|
if (not success) then
|
|
PrintErrorMessage("playersService.PlayerRemoving error updating game instances api = = " .. err)
|
|
end
|
|
|
|
-- Api Proxy - Presence
|
|
success, err = pcall(function() return UpdatePresence(player, true) end)
|
|
if (not success) then
|
|
PrintErrorMessage("playersService.PlayerRemoving error updating presence = " .. err)
|
|
end
|
|
end
|
|
|
|
if (kickDuplicatePlayersRCC or isCloudEdit) then
|
|
playersService.PlayerConnecting:connect(function(player)
|
|
pcall(function() onPlayerConnectingReportClientPresence2(player) end)
|
|
pcall(function() onPlayerConnectingReportClientPresence(player) end)
|
|
end)
|
|
|
|
playersService.PlayerDisconnecting:connect(function(player)
|
|
pcall(function() onPlayerDisconnectingReportClientPresence2(player) end)
|
|
pcall(function() onPlayerDisconnectingReportClientPresence(player) end)
|
|
end)
|
|
else
|
|
playersService.PlayerAdded:connect(onPlayerConnectingReportClientPresence)
|
|
playersService.PlayerAdded:connect(onPlayerConnectingReportClientPresence2)
|
|
|
|
playersService.PlayerRemoving:connect(onPlayerDisconnectingReportClientPresence)
|
|
playersService.PlayerRemoving:connect(onPlayerDisconnectingReportClientPresence2)
|
|
end
|
|
|
|
game.Close:connect(function()
|
|
-- when game closes, notify Game Instances API
|
|
local fullUrl = gameInstancesApiUrl .. "/v1/Close/?apiKey=" .. apiKey .. "&gameId=" .. gameId .."&placeId=" .. placeId .."&universeId=" .. universeId
|
|
PrintDebugMessage("Calling Game Instances API. Source: GameClose, URL: " .. fullUrl)
|
|
game:HttpPost(fullUrl, "", true)
|
|
end)
|