--[[ ================================================================================ RoList — In-game like reward bridge (server-only script) ================================================================================ Copyright --------- © RoList. All rights reserved. This script is sample/reference code for connecting a Roblox game to the RoList website you use or operate. You may modify and ship it with your game. RoList is not affiliated with Roblox Corporation. -------------------------------------------------------------------------------- What this script does -------------------------------------------------------------------------------- 1) When someone likes your game on the RoList website, RoList sends that event through Roblox so a MessagingService message reaches your universe. 2) This script subscribes to that channel using CONFIG.TOPIC (must match the Message Key configured on your RoList listing). The payload is JSON with userId, likeId, nickname, UTC day, timestamps, and an optional HMAC signature. 3) If that player is in this server right now, grantRoListLikeReward runs immediately. 4) If they are offline, the event is queued in DataStore and delivered the next time they join. Duplicate likeId values are not queued twice; UpdateAsync helps when many servers receive the same broadcast. -------------------------------------------------------------------------------- Creator checklist (RoList + Roblox) -------------------------------------------------------------------------------- [ ] Your game is listed and approved on RoList. [ ] You sign in to RoList with Roblox as the listing owner and complete any Roblox authorization prompts (so RoList can deliver likes to your game). [ ] If likes still don’t arrive, sign out of RoList and sign in again once, then check In-game like rewards on your listing (connection should show as OK). [ ] CONFIG.TOPIC here matches the Message Key on RoList exactly (default: RoListLike). [ ] Optional: set a signing secret on RoList and verify data.sig in your game. -------------------------------------------------------------------------------- Install (Roblox Studio) -------------------------------------------------------------------------------- • Create a Script under ServerScriptService (not a LocalScript). • Paste this entire file into that Script. • Tune CONFIG (Message Key, DataStore name, demo reward). • Replace or extend grantRoListLikeReward for your economy. Studio DataStore testing: Home → Game Settings → Security → enable “Enable Studio Access to API Services”. -------------------------------------------------------------------------------- Payload fields (message.Data → JSON) -------------------------------------------------------------------------------- v number Schema version (currently 1) type string Always "rolist_like" userId string Roblox user id as a string placeId string Start place id registered on RoList likeId string Unique RoList like id (dedupe key) claimedDay string UTC date "YYYY-MM-DD" nickname string Display name entered on RoList for that day ts number Unix time (seconds) sig string? Optional HMAC-SHA256 hex if signing secret is set HMAC canonical string (must match RoList server): "v1|rolist_like|{userId}|{placeId}|{likeId}|{claimedDay}|{ts}" -------------------------------------------------------------------------------- Notes -------------------------------------------------------------------------------- • MessagingService messages are size-limited; payloads stay small. • Respect DataStore limits at very large scale (consider MemoryStore patterns). • If grantRoListLikeReward errors, that queue entry is retried on next join. ================================================================================ ]] -------------------------------------------------------------------------------- -- CONFIG — edit for your game (TOPIC must match RoList listing settings) -------------------------------------------------------------------------------- local CONFIG = { TOPIC = "RoListLike", -- Offline delivery queue (recommended): stores like events for players who were offline. PENDING_DATASTORE_NAME = "RoList_PendingLikes_v1", -- Optional in-game announcement. SHOW_GLOBAL_POPUP = true, POPUP_DURATION_SEC = 4, --[[ Reward hook ----------- Implement your economy inside `grantRoListLikeReward(player, payload)`. This template includes a simple demo currency grant: - Adds `DEMO_REWARD_AMOUNT` to `leaderstats[DEMO_STAT_NAME]` (IntValue). - This is not persistent unless you enable the optional DataStore persistence below. To disable the demo and only log events, set ENABLE_DEMO_REWARD = false. ]] ENABLE_DEMO_REWARD = true, DEMO_STAT_NAME = "Cash", DEMO_REWARD_AMOUNT = 10000, --[[ Optional demo persistence (DataStore) ------------------------------------ If your game already has a proper save system, keep this OFF and integrate with your own code instead. When enabled, the demo currency value is saved per-player and restored on join. You must enable "Studio Access to API Services" to test DataStores in Studio. ]] ENABLE_DEMO_PERSISTENCE = false, DEMO_CURRENCY_DATASTORE_NAME = "RoList_DemoCurrency_v1", DEMO_CURRENCY_KEY_PREFIX = "cash_", } -------------------------------------------------------------------------------- -- Services -------------------------------------------------------------------------------- local MessagingService = game:GetService("MessagingService") local HttpService = game:GetService("HttpService") local Players = game:GetService("Players") local DataStoreService = game:GetService("DataStoreService") local TextChatService = game:GetService("TextChatService") local pendingStore = DataStoreService:GetDataStore(CONFIG.PENDING_DATASTORE_NAME) local demoCurrencyStore = DataStoreService:GetDataStore(CONFIG.DEMO_CURRENCY_DATASTORE_NAME) local function clampInt(n, minV, maxV) n = tonumber(n) or 0 if n < minV then return minV end if n > maxV then return maxV end return math.floor(n) end local function currencyKeyForUser(userId) return tostring(CONFIG.DEMO_CURRENCY_KEY_PREFIX or "cash_") .. tostring(userId) end local function loadDemoCurrency(userId) if not CONFIG.ENABLE_DEMO_PERSISTENCE then return nil end local key = currencyKeyForUser(userId) local value = nil local ok, err = pcall(function() value = demoCurrencyStore:GetAsync(key) end) if not ok then warn("[RoList] demo currency GetAsync failed:", err) return nil end if type(value) ~= "number" then return nil end return clampInt(value, 0, 2_000_000_000) end local function saveDemoCurrency(userId, newValue) if not CONFIG.ENABLE_DEMO_PERSISTENCE then return end local key = currencyKeyForUser(userId) newValue = clampInt(newValue, 0, 2_000_000_000) local ok, err = pcall(function() demoCurrencyStore:SetAsync(key, newValue) end) if not ok then warn("[RoList] demo currency SetAsync failed:", err) end end local function showGlobalLikePopup(line) if not CONFIG.SHOW_GLOBAL_POPUP then return end local ok, err = pcall(function() local popup = nil local okMsg, obj = pcall(function() return Instance.new("Message") end) if okMsg and obj then popup = obj else popup = Instance.new("Hint") end popup.Text = line popup.Parent = workspace task.delay(math.max(1, tonumber(CONFIG.POPUP_DURATION_SEC) or 4), function() if popup and popup.Parent then popup:Destroy() end end) end) if not ok then warn("[RoList] global popup failed:", err) end end local function resolveSystemTextChannel(timeoutSec) local deadline = os.clock() + (timeoutSec or 2) repeat local channels = TextChatService:FindFirstChild("TextChannels") if channels then local general = channels:FindFirstChild("RBXGeneral") if general and general:IsA("TextChannel") then return general end for _, child in ipairs(channels:GetChildren()) do if child:IsA("TextChannel") then return child end end end task.wait(0.1) until os.clock() >= deadline return nil end local function announceLikeToServer(payload) local nickname = tostring(payload.nickname or "") if nickname == "" then nickname = "Someone" end local line = ("💙 %s liked this game on RoList."):format(nickname) print("[RoList] " .. line) showGlobalLikePopup(line) local ok, err = pcall(function() local channel = resolveSystemTextChannel(2) if channel and channel.DisplaySystemMessage then channel:DisplaySystemMessage(line) else warn("[RoList] no TextChannel available for system message; check TextChatService settings") end end) if not ok then warn("[RoList] system chat announce failed:", err) end end -------------------------------------------------------------------------------- -- DataStore queue helpers -------------------------------------------------------------------------------- local function pendingKeyForUser(userId) return "u_" .. tostring(userId) end local function normalizeDoc(val) if type(val) ~= "table" then return { items = {} } end if type(val.items) == "table" then return { items = val.items } end if val[1] ~= nil then return { items = val } end return { items = {} } end local function hasLikeId(items, likeId) for _, e in ipairs(items) do if e.likeId == likeId then return true end end return false end local function sortByTs(items) table.sort(items, function(a, b) return (a.ts or 0) < (b.ts or 0) end) end local function enqueuePending(userId, payload) local likeId = tostring(payload.likeId or "") if likeId == "" then return end local entry = { likeId = likeId, claimedDay = tostring(payload.claimedDay or ""), ts = tonumber(payload.ts) or 0, nickname = tostring(payload.nickname or ""), placeId = tostring(payload.placeId or ""), } local key = pendingKeyForUser(userId) local ok, err = pcall(function() pendingStore:UpdateAsync(key, function(old) local doc = normalizeDoc(old) local items = doc.items if hasLikeId(items, likeId) then return old end table.insert(items, entry) sortByTs(items) return { items = items, v = 1 } end) end) if not ok then warn("[RoList] enqueue UpdateAsync failed:", err) end end -------------------------------------------------------------------------------- -- Demo: leaderstats IntValue (remove or replace in production) -------------------------------------------------------------------------------- local function getOrCreateLeaderstatInt(player, statName, initialIfCreated) local ls = player:FindFirstChild("leaderstats") if not ls then ls = Instance.new("Folder") ls.Name = "leaderstats" ls.Parent = player end local stat = ls:FindFirstChild(statName) if not stat then stat = Instance.new("IntValue") stat.Name = statName stat.Value = initialIfCreated or 0 stat.Parent = ls end if not stat:IsA("IntValue") then warn("[RoList] leaderstats.", statName, " is not IntValue; skip demo reward.") return nil end return stat end --[[ Main hook — `player` is always in this server. `payload` includes likeId, nickname, claimedDay, placeId, ts, type, etc. Called immediately on like if online, or on join after an offline like. ]] local function grantRoListLikeReward(player, payload) if CONFIG.ENABLE_DEMO_REWARD then local initial = 0 if CONFIG.ENABLE_DEMO_PERSISTENCE then local loaded = loadDemoCurrency(player.UserId) if type(loaded) == "number" then initial = loaded end end local stat = getOrCreateLeaderstatInt(player, CONFIG.DEMO_STAT_NAME, initial) if stat then stat.Value += CONFIG.DEMO_REWARD_AMOUNT if CONFIG.ENABLE_DEMO_PERSISTENCE then saveDemoCurrency(player.UserId, stat.Value) end end print( ("[RoList] Demo reward: %s | +%d %s | likeId=%s | nickname=%s"):format( player.Name, CONFIG.DEMO_REWARD_AMOUNT, CONFIG.DEMO_STAT_NAME, tostring(payload.likeId), tostring(payload.nickname) ) ) else print( ("[RoList] Like event (demo off): %s | likeId=%s — add your reward code here."):format( player.Name, tostring(payload.likeId) ) ) end end -------------------------------------------------------------------------------- -- Routing: online vs offline queue -------------------------------------------------------------------------------- local function onRoListMessage(userId, data) announceLikeToServer(data) local player = Players:GetPlayerByUserId(userId) if player then grantRoListLikeReward(player, data) else enqueuePending(userId, data) end end local function flushPendingForPlayer(player) local uid = player.UserId local key = pendingKeyForUser(uid) local doc = nil local okGet, getErr = pcall(function() doc = pendingStore:GetAsync(key) end) if not okGet then warn("[RoList] pending GetAsync failed:", getErr) return end local norm = normalizeDoc(doc) local items = norm.items if #items == 0 then return end local remaining = {} for _, entry in ipairs(items) do local payload = { type = "rolist_like", likeId = entry.likeId, claimedDay = entry.claimedDay, ts = entry.ts, nickname = entry.nickname, placeId = entry.placeId, } local okGrant = pcall(function() grantRoListLikeReward(player, payload) end) if not okGrant then table.insert(remaining, entry) end end local okSet, setErr = pcall(function() if #remaining == 0 then pendingStore:RemoveAsync(key) else sortByTs(remaining) pendingStore:SetAsync(key, { items = remaining, v = 1 }) end end) if not okSet then warn("[RoList] pending clear SetAsync failed:", setErr) end end Players.PlayerAdded:Connect(function(player) task.defer(function() if CONFIG.ENABLE_DEMO_PERSISTENCE and CONFIG.ENABLE_DEMO_REWARD then local loaded = loadDemoCurrency(player.UserId) if type(loaded) == "number" then local stat = getOrCreateLeaderstatInt(player, CONFIG.DEMO_STAT_NAME, loaded) if stat then stat.Value = loaded end end end flushPendingForPlayer(player) end) end) local subscribeOk, subscribeErr = pcall(function() MessagingService:SubscribeAsync(CONFIG.TOPIC, function(message) local decodeOk, data = pcall(function() return HttpService:JSONDecode(message.Data) end) if not decodeOk or type(data) ~= "table" then return end if data.type ~= "rolist_like" then return end local uid = tonumber(data.userId) if not uid then return end onRoListMessage(uid, data) end) end) if not subscribeOk then warn("[RoList] MessagingService SubscribeAsync failed:", subscribeErr) end