{"id":"hurreman/roblox-liveops","name":"roblox-liveops","scope":"hurreman","platform":"roblox","description":"Live-game systems for hurreman's Roblox games: analytics (funnel, economy, progression, perf), join-source attribution, a session log, AFK detection, badges, milestones, leaderboards, daily streak and playtime reward rules, validated settings and low-FPS detection","version":"0.1.0","latest":"0.1.0","versions":["0.1.0"],"license":"MIT","licenseRating":"safe","licenseCaveats":[],"licenseVerified":true,"dependencies":{},"integrity":"0d0dfcacb1edbbaf17e8dff7d5394266880dc0d84fde53d18458a9794c858863","likes":0,"downloads":0,"install":"forest install hurreman/roblox-liveops","url":"https://forest.dev/p/roblox/hurreman/roblox-liveops","files":"https://api.forest.dev/ai/package/roblox/hurreman/roblox-liveops/files","readme":"# roblox-liveops\n\nThe systems that keep a live Roblox game measurable and bring players back, taken mostly from Magic Neighborhood (the only one of the three games that has all of them) and checked against Slime Nebula's reward rules.\n\n```toml\n# wally.toml\n[dependencies]\nLiveOps = \"hurreman/roblox-liveops@0.1.0\"\n```\n\nModules load on first access. Each one receives the game's own data through callbacks (its save tables, stat readers and grants), so none of them requires game code.\n\n| Module | Side | What it does |\n|---|---|---|\n| `Telemetry` | server | Onboarding funnel (append-only, new players only), economy flows summed and flushed every 60 s and on leave, progression, custom events with two fields, perf samples (fps, memory, receive rate, server-read ping). Place version in every event. Prints instead of sending in Studio. |\n| `JoinSource` | pure | Tagged link launch data (`src=tt&c=clip07`, JSON, or a bare word) parsed to source + campaign. Only the game's own sources are kept; `none` / `unknown` / `invalid` / `other` otherwise. |\n| `Sessions` | server | A visit log in the save: length, place version, last action, idle and still time, shutdown or leave, join source, device, plus the game's own fields. |\n| `Activity` | server + client | AFK detection: active if the client pinged input in the last 120 s. Crowd-sized things count `ActiveCount()`. |\n| `Badges` | server | Save-derived badges and event badges owed until awarded. Holds back during onboarding, counts an award only if it went through, dry run in Studio. |\n| `Milestones` | pure | First-time progression stamps with play seconds, and listeners. |\n| `Leaderboards` | server | All-time, daily (a store per day) and per-server boards on OrderedDataStores; log encoding for huge values; this server's players when DataStores are off. |\n| `Streak` | pure | Daily login streak: consecutive, broken, an optional gift cycle that survives a miss (Slime Nebula), an optional buy-back window (Magic Neighborhood). |\n| `Playtime` | pure | A daily ladder of gifts by seconds played; claimed as a bitmask or a set; rolling over to a new day hands back what went unclaimed. |\n| `Settings` | server | Client-written settings validated per key (`Bool`, `Choice`, `Volume`, `Flags`), with a change hook. |\n| `FpsWatch` | client | Sustained low frame rate (windowed averages, grace period, hitches and unfocused time ignored), once per session. |\n\n## A few of them in use\n\n```lua\nlocal LiveOps = require(ReplicatedStorage.Packages.LiveOps)\n\nlocal telemetry = LiveOps.Telemetry.new({\n\tsteps = { \"Joined\", \"BoughtHouse\", \"FirstMerge\" },             -- only ever append\n\tfunnel = function(player) local s = data(player) return s and { done = s.stats.funnel, at = s.stats.funnelAt } end,\n\tplaySeconds = function(player) return data(player).stats.playSeconds end,\n\tsourceFields = function(player) local s = data(player).stats return s.joinSrc, s.joinCamp end,\n\tbalance = function(player, currency) return data(player).cash end,\n\ttransactions = { Daily = \"TimedReward\", Offline = \"TimedReward\" },\n\tiap = function(sku) return Products.List[sku] ~= nil end,\n\tcounters = function(name, value, a, b) adminStats.add(name, value, a, b) end,\n})\ntelemetry:Start()\ntelemetry:Onboarding(player, \"BoughtHouse\")\ntelemetry:Source(player, \"Converter\", 120)\n\nlocal clock = Core.ResetClock.Stockholm                            -- roblox-core\nlocal claimed = LiveOps.Streak.Claim(save.daily, clock.GetDayNumber(os.time()), { cycle = 7 })\n\nlocal boards = LiveOps.Leaderboards.new({\n\tboards = { { id = \"Earned\", log = true }, { id = \"Today\", scope = \"daily\" }, { id = \"Here\", scope = \"server\" } },\n\tvalue = function(board, player) ... end,\n\tdayOf = clock.GetDayNumber,\n})\nboards:Start()\n```\n\n## Moving the games over\n\n- **Magic Neighborhood.** Each service keeps its game-specific parts, and the engine underneath becomes the package's:\n\n  | Today | Keeps | Becomes |\n  |---|---|---|\n  | `TelemetryService` | the ONBOARDING step list, the tutorial step mapping, `place()` / `housesBucket()` | `Telemetry` |\n  | `JoinSource` | its SOURCES table | `JoinSource.new(SOURCES)` (its spec passes unchanged through an adapter, and a corpus agrees with the old module) |\n  | `SessionService` | goal, tutorial step and cash, as `extra` | `Sessions` |\n  | `ActivityService` and `client/Activity` | | `Activity` |\n  | `BadgeService` and `shared/Badges` | the rules | `Badges` |\n  | `Milestones` | | `Milestones`, with the TOTD-specific helpers (`houses`, `seedBought`) staying beside it |\n  | `LeaderboardService` | leaderstats, `rollDay`, `statValue` | `Leaderboards` |\n  | `RewardsService` | offline earnings, payouts, welcome card | the claim rules: `Streak` with `save = { minStreak = 2, maxMissedDays = 3 }`, and `Playtime` |\n  | `SettingsService` | | `Settings` with its keys |\n  | `FpsWatch` | | `FpsWatch` with `watching = preset is \"auto\"` |\n\n- **Slime Nebula:**\n  - `DailyRewardConfig.EvaluateClaimEligibility` becomes `Streak.Status(..., { cycle = #Days })`, with days from `ResetClock.Stockholm`. They agree on 2000 random states.\n  - `PlaytimeRewardConfig` tiers become `Playtime` with its `ClaimedTiers` set.\n  - `Analytics.lua` and `OnboardingFunnel` can move onto `Telemetry` (keep the `RobloxAdminServer` stats copy as `counters`).\n- **Steal an Egg:** `TelemetryService` becomes `Telemetry` the same way.\n\n## Developing\n\n```bash\nlune run tests/run    # specs on hurreman/luau-spec (DevPackages, else ../luau-spec); fakes in tests/fixtures\n```\n\nWhen the sibling checkouts are present, three specs check against the games' own code:\n- `JoinSource` runs Magic Neighborhood's spec and a launch-data corpus against its module.\n- `Streak` diffs Slime Nebula's `EvaluateClaimEligibility` over 2000 random states. A deliberate change to the cycle rule made it fail.\n- `Playtime` diffs Magic Neighborhood's `giftsReady` on its real gift ladder.\n\nIn Studio, these were checked: the analytics enum keys and transaction type names the package sends, and that the AnalyticsService, BadgeService, OrderedDataStore, ProximityPrompt and Players APIs it calls exist.\n\nNot run yet:\n- real AnalyticsService calls, which need a real Player in Play mode;\n- real badge awards;\n- OrderedDataStore reads and writes.\n","readmeTruncated":false}