Skip to content
Docs

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

  1. Add the permission memory-store.queue with Dequeue and Discard to your API key (see API keys).
  2. Click Check again on the game in the dashboard. The game now shows in-game reports as available.
  3. Switch on In-game reports for the game.
  4. 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.

ServerScriptService/ModhelmReports
--!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.

ServerScriptService/ModhelmReportHandler
-- 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:

StarterPlayerScripts/ReportButton
-- 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

FieldTypeContent
reporternumberUser ID of the reporting player
targetnumberUser ID of the reported player
categorystringexploiting, harassment, scamming, inappropriate, other or one of your own categories
textstringDescription, 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.queue and 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.