Docs
In-game reports
Let players report from inside your game. A small Luau module writes reports into a MemoryStore queue, Modhelm reads them through Open Cloud. HTTP requests stay off.
How it works
Your game adds each report to the MemoryStore queue ModhelmReports. Every 30 seconds Modhelm reads the queue through Open Cloud with your API key, opens a normal report card and removes the item from the queue.
There is no HttpService request in this flow. Allow HTTP Requests stays off, and nothing in your game depends on Modhelm being online. Unread reports wait in the queue for up to seven days.
Enable it
- Add the permission
memory-store.queuewith Dequeue and Discard to your API key (see API keys). - Click Check again on the game in the dashboard. The game now shows in-game reports as available.
- Switch on In-game reports for the game.
- Add the module and the server script below to your game.
The module
Put this ModuleScript into ServerScriptService and name it ModhelmReports. It checks the input, limits each player to one report per minute and adds the report to the queue.
--!strict
-- ModhelmReports (ModuleScript in ServerScriptService)
-- Sends in-game reports to Modhelm through a MemoryStore queue.
-- No HttpService requests: "Allow HTTP Requests" can stay off.
local MemoryStoreService = game:GetService("MemoryStoreService")
local Players = game:GetService("Players")
local QUEUE_NAME = "ModhelmReports" -- Modhelm reads exactly this queue
local EXPIRATION = 604800 -- seconds an unread report stays in the queue (7 days)
local COOLDOWN = 60 -- seconds between two reports of the same player
local MAX_TEXT = 1000 -- characters
local queue = MemoryStoreService:GetQueue(QUEUE_NAME)
local lastReport: { [number]: number } = {}
local function truncate(s: string, maxChars: number): string
local len = utf8.len(s)
if not len then
return "" -- not valid UTF-8
end
if len <= maxChars then
return s
end
return string.sub(s, 1, (utf8.offset(s, maxChars + 1) :: number) - 1)
end
local ModhelmReports = {}
-- Queue a report. Returns true, or false plus a reason ("invalid", "cooldown", "queue_error").
function ModhelmReports.report(reporter: Player, targetUserId: number, category: string, text: string?): (boolean, string?)
if typeof(targetUserId) ~= "number" or targetUserId <= 0 or targetUserId % 1 ~= 0 or targetUserId == reporter.UserId then
return false, "invalid"
end
local now = os.clock()
local last = lastReport[reporter.UserId]
if last and now - last < COOLDOWN then
return false, "cooldown"
end
lastReport[reporter.UserId] = now
local item = {
reporter = reporter.UserId,
target = targetUserId,
category = string.lower(truncate(category, 50)),
text = truncate(text or "", MAX_TEXT),
}
local ok, err = pcall(function()
queue:AddAsync(item, EXPIRATION)
end)
if not ok then
lastReport[reporter.UserId] = nil
warn("[Modhelm] Report could not be queued: " .. tostring(err))
return false, "queue_error"
end
return true
end
Players.PlayerRemoving:Connect(function(player: Player)
lastReport[player.UserId] = nil
end)
return ModhelmReports
Connect your UI
The server script creates a RemoteEvent. The reporter is always the player who fired it, never a value from the client.
-- Script in ServerScriptService, next to the ModhelmReports module
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local ModhelmReports = require(script.Parent.ModhelmReports)
local remote = Instance.new("RemoteEvent")
remote.Name = "ModhelmReport"
remote.Parent = ReplicatedStorage
-- The reporter is always the player who fired the event, never a value sent by the client.
remote.OnServerEvent:Connect(function(player: Player, targetUserId: any, category: any, text: any)
if typeof(targetUserId) ~= "number" or typeof(category) ~= "string" then
return
end
if text ~= nil and typeof(text) ~= "string" then
return
end
ModhelmReports.report(player, targetUserId, category, text)
end)
Then fire the event from your own report button:
-- LocalScript, for example behind a "Report player" button in your own UI
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local remote = ReplicatedStorage:WaitForChild("ModhelmReport")
local function reportPlayer(target: Player, category: string, text: string)
remote:FireServer(target.UserId, category, text)
end
-- reportPlayer(somePlayer, "exploiting", "Flying through walls near the spawn")
Report format
| Field | Type | Content |
|---|---|---|
reporter | number | User ID of the reporting player |
target | number | User ID of the reported player |
category | string | exploiting, harassment, scamming, inappropriate, other or one of your own categories |
text | string | Description, up to 1000 characters |
Unknown categories become other. In-game reporters have no Discord account linked, so they get no direct message about the result.
If nothing arrives
- Is in-game reporting switched on for the game in the dashboard?
- Does the key have Dequeue and Discard for
memory-store.queueand cover this experience? - Is the queue name exactly
ModhelmReports? - MemoryStore does not work in a local Studio test without API access. Test in a published game or switch on Enable Studio Access to API Services.