Adapters and backend (reference)#

The technical reference behind Shop, Ads, Online services and Analytics: how the real SDKs plug in, what each service does, and what to set up. For programmers and for anyone who wants the details; the step-by-step pages are enough to publish. Code comments point at the sections of this page.

How adapters switch on#

Install the package, and the adapter compiles. Every adapter in Services/Adapters/ is its own assembly with a version define on its package: the define exists only while the package is installed. Remove the package and the folder stops compiling; the template never names an SDK type, so nothing else breaks. No Scripting Define Symbol is needed for these:

AdapterFolderSwitches on withPlatforms
Unity IAPServices/Adapters/UnityIapcom.unity.purchasing 5.4+ (IDLE_UNITY_IAP)all
LevelPlayServices/Adapters/LevelPlaycom.unity.services.levelplay 9+ (IDLE_LEVELPLAY); consent with com.google.ads.mobile 8+ (IDLE_UMP); iOS tracking with com.unity.ads.ios-support (IDLE_ATT)Android, iOS, Editor
Store + ads profileServices/Adapters/StoreAndAdsboth of the aboveAndroid, iOS, Editor
Unity Gaming ServicesServices/Adapters/Ugscom.unity.services.authentication, then each service with its package (IDLE_UGS_CLOUD_SAVE, IDLE_UGS_SERVER Cloud Code, IDLE_UGS_REMOTE_CONFIG, IDLE_UGS_ANALYTICS, IDLE_UGS_LEADERBOARDS, IDLE_UGS_FRIENDS)all
Sign in with AppleServices/Adapters/AppleSignIncom.lupidan.apple-signin-unity (IDLE_APPLE_SIGNIN)iOS, Editor (answers on a device only)
FirebaseServices/Firebase (in the Game assembly)com.google.firebase.analytics (IDLE_FIREBASE), com.google.firebase.crashlytics (IDLE_CRASHLYTICS)all
NotificationsServices/MobileNotificationBridge.cscom.unity.mobile.notifications (IDLE_MOBILE_NOTIFICATIONS)Android, iOS

Set by hand in Player Settings > Other Settings > Scripting Define Symbols, per platform:

  • IDLE_GPGS (Android): the Google Play Games plugin is imported into Assets/GooglePlayGames, not a package.
  • IDLE_FIREBASE only when Firebase came as a .unitypackage.
  • IDLE_UMP only when the Google Mobile Ads plugin came as a .unitypackage.
  • IDLE_DEV_TOOLS for tester builds only (Dev tools).

A define whose package is missing is defines.missing-package in Errors.md.

Unity Gaming Services also needs its settings and a linked project. The UGS adapter registers itself as the game's online backend at start (UgsBackend) only when:

  1. IdleRpgUgsSettings (Hub > Services > Online > Settings; Create > Idle RPG > Unity Gaming Services Settings in a Resources folder) allows it: Use UGS (off = fully offline), Use In Editor (off = Play stays offline) and one switch per service: Cloud Save, Game Server (Cloud Code), Leaderboards, Friends, Remote Config, Analytics. A service runs when its switch is on and its package is installed. No asset = all on.
  2. The project is linked to a Unity Cloud project (Edit > Project Settings > Services).

Otherwise the game runs offline and logs why. With it: [Online] Using Unity Gaming Services.

Which service profile runs#

A service profile is an asset that says which store, ads and other services the game uses. The scene's GameBootstrap has two fields:

FieldHoldsWho runs it
Service ProfileFakeServicesPlay in the editor
Release ProfileStoreAndAdsServices when the adapters are installed (Hub > Services > Use it), else emptyevery player build (Android, iOS)
  • A player build runs Release Profile when set, else Service Profile. An empty Service Profile means the fakes.
  • A platform the adapter does not compile for (WebGL, desktop) reads Release Profile as empty and runs the fakes.
  • Use Release Profile In Editor (off by default) runs the real profile in Play, to try real ads or store in the editor.
  • The boot logs one line: [IdleRpg] Services: StoreAndAdsServices (release profile).
  • A profile can leave any service to its default; UnityIapServices (store only) and LevelPlayServices (ads only) also ship. Pre-flight's release.fake-store stops a release build that would sell nothing.

Your own online backend#

The game names no backend SDK: cloud save, account, analytics, live config, game server, leaderboards, friends, player names and server time come from OnlineBackendRegistry.Current when the profile does not supply them, and from the offline Null* services when nothing is registered. To use your own backend instead of UGS:

  1. In your own code (it sees the template's assemblies), implement IOnlineBackend: Name, one Create… per service (CreateCloudSave, CreateAccount, CreateAnalytics, CreateLiveConfig, CreateGameServer, CreateLeaderboards, CreateFriends, CreatePlayerNames, CreateServerTime) and RetrySignInNow. Return null for a service you do not offer: that part runs offline.
  2. Register it from a [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)] method with OnlineBackendRegistry.Register(new MyBackend()). Services/Adapters/Ugs/UgsBackend.cs is the worked example.
  3. Only one backend is active. Turn Use UGS off, or remove the UGS packages, so both do not register.

The purchase and ad contract#

The game's rules are told what happened, nothing else: never a receipt, a price or an ad request.

AdapterWhenWhat it must do
IPurchaseServicea purchase succeeded, or the store delivered one againGrantPurchase(productId, transactionId, isRestore: false), then confirm the transaction with the store, only once the command answered Ok
IPurchaseServicethe command refused it (unknown product, already owned)do not confirm: the store delivers it again, and a build that knows the product honours it
IPurchaseService"Restore purchases"GrantPurchase(..., isRestore: true) for each owned non-consumable
IPurchaseServicea price is neededthe store's own localised text and number + currency code; no price is ever in content
IAdServicebefore offering a rewarded buttonRewardedPlacementRules.Availability(...): the game's cap, cooldown and "anything to apply it to"
IAdServicea rewarded ad finishedGrantAdReward(placementId)
IAdServicefailed / cancelled / skipped / no fillnothing at all

productId is the store product id. transactionId must be the store's own id: GrantPurchase is idempotent by it, which is what makes a redelivery safe.

What the game calls#

// The shop's buy button.
services.Purchases.Purchase(productId, result => { /* Purchased, Pending, Cancelled, or failed: "you were not charged" */ });

// The "Restore purchases" row.
services.Purchases.RestorePurchases(r => toast.Show(r.AnythingRestored ? "Purchases restored" : "Nothing to restore"));

// A price on a card. Only the store knows where the currency symbol goes.
if (services.Purchases.TryGetLocalizedPrice(productId, out string price)) { /* "$4.99" */ }

// Every rewarded button, whatever the player owns (Remove Ads, VIP and tickets skip the ad).
services.Rewarded.Claim(placementId, outcome => { /* outcome.Ok, outcome.AdShown */ });

Unity IAP adapter#

Services/Adapters/UnityIap/, compiled when com.unity.purchasing 5.4+ is installed.

  • Catalogue from content: one store product per product asset; id = the asset's id, type = its kind. No id or price in code.
  • Prices are the store's. Until the products are fetched the shop shows its paused state.
  • A purchase: the store's transaction id, local receipt validation, the grant, and ConfirmPurchase only when the grant answered Ok. A refusal is left unconfirmed for the store to deliver again. An app killed between the store and the grant gets the purchase again at the next start (never twice: the grant is idempotent).
  • Cancel / pending (Ask to Buy) / failure map to Cancelled, Pending (the grant arrives later), Failed ("you were not charged").
  • Restore: every owned non-consumable is granted again (a no-op when the save has it; brings Remove Ads back after a reinstall).
  • Local receipt validation with the generated GooglePlayTangle / AppleTangle classes (Receipt Validation Obfuscator; see Shop). An invalid receipt is neither granted nor confirmed (Google refunds it after 3 days). Apple purchases are verified by StoreKit 2. It never runs in the editor.
  • Server validation (GameConfig > serverReceiptValidation, off): the game server's RedeemPurchase checks the order before the grant (Google: its signature with your licence key; Apple: the StoreKit 2 signature) and records it once. Whose purchases it accepts is read from Deploy/Idle.rc (srv_androidPackage, srv_iosBundleId, srv_googlePlayKey), which the Hub fills in; see Server receipt validation. An answer the server could not decide leaves the order unconfirmed for the store to deliver again; "not configured" (a key empty) grants on the device's check, as with the switch off.
  • Minified builds: Assets/Plugins/Android/BillingKeepRules.androidlib keeps Google Play Billing's classes from the code shrinker (R8). Keep that folder when you move plugins.
  • In the editor, Unity IAP's fake store shows a small Buy / Cancel window (add the define IAP_FAKE_STORE_DEFAULT for none).

LevelPlay adapter#

Services/Adapters/LevelPlay/, compiled for Android, iOS and the editor when com.unity.services.levelplay 9+ is installed. LevelPlaySettings on the profile holds the app keys, the default rewarded ad unit per platform, the placement table and the consent options (Hub > Monetization edits them).

Consent, in order, before the first ad request (AdConsentFlow): Google UMP's consent update and form (when required) → on iOS the tracking prompt, skipped when GDPR applies and the player refused → LevelPlay.Init(appKey). If UMP says ads may not be requested, the SDK does not start; Settings > Privacy choices can allow it later. An offline start uses the previous answers.

  • Analytics consent comes from the same answer: under GDPR only when the purposes for measurement are consented.
  • Without UMP (no IDLE_UMP): no form, ads start at once, analytics consent = the switch alone, with a warning in the log. Only right where no consent is required.
  • Rewarded ads: a placement plays its own ad unit (Placements table) or the platform's default; each ad unit is loaded ahead and reloaded after every show and after an error (5 s up to 60 s). The reward is sent only from the SDK's "rewarded" callback, once.
  • Init: every step logs with [LevelPlay]. A failed init is retried after 15, 30, 60, 120, 300 s. Development builds turn on the adapters' debug logs; the profile's Launch Test Suite opens LevelPlay's Test Suite (development builds only).
  • Unity 6000.6 and the Google Mobile Ads plugin: two Android libraries share one name; Editor/Build/GmaNamespaceFix.cs renames one in the exported Gradle project after every export (build.agp-namespace-gma). Nothing to do.

The fake services#

FakeDoesKnobs (Settings/FakeServices.asset)
FakePurchaseServicean obviously fake price from the product's baseGemValue, waits, then grants with a generated transaction idpurchaseDelaySeconds, purchaseOutcome (force Cancelled / Failed / Pending / Unavailable), priceOverrides
FakeAdServicechecks the game's availability, waits, then grantsadDelaySeconds, adOutcome (Cancelled / Failed / NoFill), adsHaveFill
Fake cloud save, account, server, server time, leaderboards, friends, live config, analyticsthe online features without an accountone switch each; fakeOnlineOffline is airplane mode, live while playing

They are how the paused shop, "you were not charged" and "no ad available" can be seen without any SDK. Create your own with Create > Idle RPG > Service Profile (Fake).

Things not to do#

  • Do not grant a purchase from a screen. The purchase service is the only caller of GrantPurchase.
  • Do not call GrantAdReward from anywhere but "the ad finished": not on a click, not on a timeout.
  • Do not put a price in a content asset: the store owns prices.
  • Do not let the interface pretend an ad played: ClaimPlacementWithoutAd exists so it never has to.
  • Do not skip RewardedPlacementRules.Availability: it keeps a watched ad from being wasted.

Local notifications#

The game schedules its own reminders on the phone while it is closed (package com.unity.mobile.notifications). No server.

  • What and when is NotificationPlanner: Treasure Cart full or filling up, an offer ending within the hour, the daily reset, "your heroes are waiting" 24 h and 72 h after leaving, the Growth Pass season end, a new featured summon hero. Quiet hours 22:00-08:00, never two within an hour, at most 8 waiting. Every number is a notification* field of Game Config; notificationsEnabled = false removes the feature.
  • Scheduled when the app goes to the background, all cancelled when it returns. The game asks for permission after the first Treasure Cart claim, never at launch.
  • A new reminder: one INotificationSource in NotificationSources.CreateDefault() (a DeadlineNotificationSource line for "ends in 24 h").

Before shipping:

  1. Android icons: Project Settings > Mobile Notifications > Android > Notification Icons: an entry icon_small (type Small, a white silhouette on transparent, one size per density) and icon_large (type Large). The template's are in Art/AppIcon/notifications/. Other ids: change notificationSmallIconId / notificationLargeIconId.
  2. iOS: untick Request Authorization on App Launch (the game asks at the right moment). No push capability is needed.
  3. The channel name Android shows is the text ui.notify.channel.name: rename it with your game. Keep notificationChannelId fixed after release.

Sound files#

Sound needs no adapter. The sound table Settings/Audio.asset maps ids to clips; AudioCues maps game moments to ids. A new sound for an existing moment is a file; a new moment is one row in AudioCues. Films: Assets/StreamingAssets/Cinematic/ (a <film>_vo.mp4 beside the silent cut is preferred; the Voice setting mutes it).

The stores want a privacy policy, terms and a support contact inside the app. Content/LegalInfo.asset (Hub > Project > Legal) holds the privacy policy address, terms address, support e-mail, the licences text and the iOS tracking sentence. An empty field hides its Settings row; a release build warns while an address is a placeholder (legal.*). Contact support opens an e-mail with the Support ID, version and device. The iOS build writes ITSAppUsesNonExemptEncryption = NO, the tracking sentence when set, and adds Assets/Plugins/iOS/PrivacyInfo.xcprivacy.

Saves off the phone#

The save is one file, Application.persistentDataPath/save.json (+ save.json.bak), versioned and migrated.

  • Android Auto Backup (on, nothing to do): Assets/Plugins/Android/SaveBackup.androidlib adds backup rules that include only the save files, the backups folder and PlayerPrefs. If you rename the save file, rename it in both XML files there. An SDK that turns backup off now fails the manifest merge: decide with tools:replace in your manifest. Test: adb shell bmgr backupnow <package>, uninstall, reinstall.
  • iOS: the app's Documents folder is in iCloud and device backups by default.
  • Back up save / Restore save (Settings rows): Back up writes backups/last-backup.json and shares it (Android share sheet; desktop also Documents). Restore reads it back after a confirmation. For a real file picker on both platforms implement ISaveFiles with a plugin.

Cloud save#

ICloudSaveService: sign in, upload, download. NullCloudSave (default, no cloud), FakeCloudSave (a folder beside the save; can pretend another phone is ahead or offline), UnityCloudSaveService (UGS Authentication + Cloud Save, keys save and save_meta).

  • When it syncs: once at boot after the game loaded; after save moments (a purchase at once, the rest at most every cloudPushMinSeconds); when the app goes to the background.
  • Which copy wins (SaveSync.Decide): only one side played since the last sync → it wins; both did → the newer, or the player is asked when they are cloudConflictProgress apart. **A copy with more purchases is never overwritten without asking**; a copy from a newer build is left alone.
  • GameConfig > cloudSaveEnabled switches it off.

Accounts: linking and deletion#

Three Settings rows: Sign in with Apple, Sign in with Google Play Games and Delete cloud data.

  • Linking keeps the same player and save. If the store account already has a player (a reinstall), the game signs into that player instead and the cloud sync asks which save to keep.
  • Delete cloud data asks twice (the second is Hold to delete) and removes: the server's records and the player's leaderboard scores (the module's DeletePlayer, which needs the service account below), every friend and request, the Cloud Save data and the account; then a new anonymous player starts. If a part fails, the player is told to write to support with the Support ID.
  • The sign-in plugins register themselves (ISignInTokens); with none installed the rows stay hidden.
  • Setup: Online services. If a plugin names its assembly differently, fix the name in the adapter's asmdef.
  • Store forms: Google Play Data safety > Account creation and the deletion path (the in-app row, plus a web page); Apple guideline 5.1.1(v) is met by the same row.

Server time#

Every rule reads TrustedClock, which on its own trusts the phone's clock as long as it never goes backwards. With a server answer (TrustedClock.Anchor), for serverTimeAnchorHours (24) of uptime the game's time is the server's plus the uptime since, and a phone clock more than serverTimeToleranceSeconds ahead is ignored: timers wait, nothing is lost. It is asked at boot (after the cloud save's sign-in) and on every return to the app; nothing waits for it.

IServerTime: NullServerTime (default), FakeServerTime (an offset to test a moved clock), UgsServerTime (Cloud Code GetServerTime.js). Setup: install Cloud Code, turn it on in the dashboard, deploy Deploy/CloudCode/GetServerTime.js, Game Server on in IdleRpgUgsSettings.

Remote Config#

Change a few numbers and switch features off without a store update. Remote Config values are laid over the shipped numbers, only for keys on a whitelist (LiveConfigKeys.cs), and only when the result still validates. Ids and table shapes are never live, so saves can never break.

KeyOverrides
afkCapHoursthe offline cap's base
cloudSaveEnabled, cloudPushMinSecondscloud save
snapshotPeriodMinutesminutes between progress reports to the server
summonPaidAllowedpaid summons
stallOfferGraceMinutesthe stuck-player offer
ad_<placement id>_dailyCapa placement's daily cap
<offer id>_durationHours, <offer id>_cooldownHoursan offer's timers
rc_enabledthe kill switch: false = everybody on the shipped numbers
  • A value that does not parse, or a result that fails validation, is ignored and logged ([LiveConfig] …); the rest applies, or the whole set is ignored if it would break the content.
  • Next start, never mid-session: the game applies the last fetched values at boot and fetches in the background for the next one.
  • To make another number live, add one line to LiveConfigKeys.cs and export again.
  • Setup: Online services. Tools > Idle RPG > Export Live Config writes Deploy/Idle.rc; run it again after every balance change, before deploying.

LiveOps patches#

Five JSON keys change the shop, rewards and texts without an update, each a patch naming only what it changes:

KeyMay set
cfg_gameConfigany Game Config field (not the live-config switch, gift codes, Featured offers or ghosts)
cfg_pricessummon gate prices and pity, daily-deal pools, gem items, Featured offers, product rewards, offers
cfg_rewardsmissions, mission chests, daily calendars, the Growth Pass, achievements, biome first-kill rewards, event templates, placements
cfg_stringstexts a language already has (keeping its {0})
cfg_minVersion{ "android": { "soft": 12, "hard": 10, "url": "…" } }: below hard a blocking Update required sheet, below soft Update available with Later

Rows are matched by id (or by index for calendar days, pass goals, tiers); arrays given whole replace the list. Anything unknown or invalid is ignored and logged; a key whose result fails validation is ignored whole. Real-money prices and product ids stay in the store consoles. Deploy/LiveOps.json holds the same keys for Export Live Config (which refuses a file with any invalid part) and Deploy/LiveOps.reference.json lists every live table and field at the build's values: copy rows from there.

Server rules and snapshots#

The game stays playable offline; the server checks what the phone reports and never plays.

  • Every few minutes (only when something changed) and at key moments (a summon, an Ascension, a purchase, a claim, going to the background, coming back online) the phone sends a snapshot: best wave, Ascensions, the gem ledger (earned, bought, from ads, spent), ads, heroes, summons, castle rank, days played, and the save.
  • The Cloud Code module compares it with the last accepted one using the game's own rules (SnapshotRules) and numbers from Remote Config (srv_* keys written by Export Live Config): wave and Ascension rates, a wave ceiling for the account's age, gem balances, ad rates, hero count, server-seeded summons, castle rank.
  • Soft rules (rates and gem balances, with an error margin of the larger of 300 gems and 10 %) give a first strike the player never sees; a second in a row flags. Hard rules (impossible waves, heroes, summons, castle ranks) flag at once. A counter going down is not a flag: it is another save of the account (a reinstall, a second device).
  • A flagged account keeps playing but is hidden from the boards and gets no season or event prizes, and an Inbox letter tells it. See Lifting a flag.
  • After a balance change: Export Live Config and deploy Idle.rc before a build with more heroes, castle ranks or gem rewards reaches players, and raise srv_adsPerDay when you raise a placement's daily cap in the dashboard. The module needs no rebuild.
  • GameConfig > summonServerSeeded: summons use the server's seed (the buttons say Verifying…; offline, "Connect to the internet to summon"). A build without a game server rolls on the phone.

The game server#

The module Deploy/CloudCode/IdleServer.ccm (built from the game's own rules) answers:

EndpointWhat
SubmitSnapshotjudges a snapshot (Accepted, Flagged, or too soon: sent once more later)
RollSummonthe seed for a summon, checked against the wallet
SubmitScore, SubmitEventScorewrites a board score after an accepted snapshot (a flagged account's are dropped)
ClaimSeasonReward, ClaimEventPrizepays the monthly season rank and event prizes, once per receipt
RedeemPurchasechecks a store purchase before the grant (serverReceiptValidation), once per transaction
RedeemCodechecks a gift code
GetMyCode, FindByCode, GetPublicProfile, RegisterInvitefriend codes, player cards, Invite Week
DeletePlayerthe server side of Delete cloud data
ClaimGifts, AdminGiftgifts to one player

Boards: season_best_wave (monthly, reset on the 1st at the daily reset hour, archived for the season rewards; change its Schedule if you change dailyResetUtcHour) and lifetime_prestiges. Each board event run gets its own board evt_<runId>, created by the module with the service account; evt_slot_a / evt_slot_b are the fallback. Access control (Deploy/Idle.ac): players may read boards but never write a score.

Service account (Unity Dashboard > Administration > Service Accounts): role Leaderboards Admin (deleting scores, event boards) and Player Authentication Admin or a viewer role (Invite Week's linked-account check). Its key id and secret go in Cloud Code > Secrets as IDLE_SERVICE_ACCOUNT_KEY and IDLE_SERVICE_ACCOUNT_SECRET.

Your app's ids (srv_androidPackage, srv_iosBundleId, srv_googlePlayKey in Deploy/Idle.rc): whose purchases RedeemPurchase accepts. The module has no app id of its own; the Hub writes yours (Server receipt validation).

Limits: Cloud Code calls take up to 15 s, and the first call after 15 idle minutes is slow (cold start); the game waits up to about 20 s for a summon. Nothing else waits for the network.

Records: the module keeps its data in the player's Protected Cloud Save keys (srv_snapshot, srv_flagged, srv_summons, srv_season_rewards, srv_redeemed, srv_code...), which a player can read but never write.

Changing the answers: the Cloud Code SDK reads answers strictly, so an older game build fails on a field it does not know. The shipped module and game match; do not deploy a different module to players on older builds.

Redeem codes#

Gift codes for your community: the player types one in Settings > Redeem code (or the menu's Redeem tile); the server checks it and the game grants what it answered.

  • The codes live in the Remote Config key srv_redeemCodes (JSON), as SHA-256 hashes only, so nobody can read them out of the config. Codes are 4-16 letters and digits, matched without case, spaces or dashes.
  • First deploy: Tools > Idle RPG > Export Live Config (first deploy, with redeem codes), deploy Idle.rc, then run the plain Export Live Config again. Or create the key by hand: type JSON, value { "codes": [] }. After that the dashboard owns the key; later deploys never touch it (deploy without Reconcile).
  • The template's demo codes are samples: DEMO100 (open, once per player: 100 gems and 3 Summon Scrolls) and DEMOLIMITED (the first 1000 players between 1 January 2026 and 1 January 2031: 50 gems and 60 minutes of income). They are in Game Config > Redeem Codes. Every buyer of the template knows them, so Pre-flight shows keys.demo-redeem-code until you delete them (its Delete the demo codes button) or replace them.
  • Add a code (no build): hash it (the code in capitals, no spaces or dashes: printf %s SUMMER26 | shasum -a 256 on Mac), then add a row to srv_redeemCodes and publish:
{ "codeHash": "<the 64 hex characters>",
  "rewards": [ { "kind": "Currency", "id": "gems", "amount": 200 }, { "kind": "IncomeMinutes", "amount": 30 } ],
  "validFrom": "2026-07-01T00:00:00Z", "validTo": "2026-08-01T00:00:00Z", "maxRedemptions": 5000, "oncePerPlayer": true }

kind is Currency (with a currency id), HeroShards / Hero (a hero id), ItemFragments (an item id), Cosmetic, VipPoints, IncomeMinutes or Boost. Dates are UTC and optional; maxRedemptions 0 = unlimited. A new or ended code takes effect within 5 minutes. Keep the plain code in your own notes.

  • Give support codes 8+ characters, a limit and an end date: a short code can be guessed.

Live events on the server#

Board events (Boss Rush, Frontier Week, Friends Rally), their prizes and Invite Week run on the server clock. The module reads srv_event_templates (written by Export Live Config from your content) and srv_events (the dated runs and content-drop days: edit it in the dashboard to move a date; a drop may move later, never earlier). A run's prizes arrive as Inbox letters, once per receipt. Invite Week needs both players with a linked sign-in and friends. The event prizes widen the server's gem allowance, so they never flag a player. See also content drops.

Player codes#

A friend code is short, personal and permanent (X7KQ-4MZP: 8 symbols without 0, O, 1, I), never the public name. The module derives it from the player id with a salt and keeps it unique in a registry; once given it never changes. Lookups are limited (20 per 10 minutes, 10 unknown codes in a row block for 10 minutes), so guessing codes is hopeless.

Set once, before players get codes: Remote Config key srv_codeSalt (string) with a random value of your own (for example openssl rand -hex 16). Never put it in a file; Idle.rc leaves it out. Without it a built-in default is used (codes still work).

Offline and back#

One connection state for the whole game (ConnectionMonitor): online means the device has a network and the services answered. Online screens (Summon, the shop, events, Friends, Rankings, Redeem) redraw when it changes. When the services are down, the game asks again after 20 s, 40 s, then every minute, and at once when the network or the app comes back or the player taps an online feature (its Connect key). Back online, the game sends a snapshot and catches up on everything that waited (name, season, scores, prizes, cloud save, the store).

In the editor, FakeServices > fakeOnlineOffline is airplane mode, live while playing.

Ghost players#

At launch the boards would be empty, so the game fills them with ghost players: a deterministic crowd that every phone and the server compute alike and merge into the real boards when they are read. Nothing is stored and no score is written. Each ghost has a name, a look, a play style and a join day; its progress follows realistic curves and stays below the content's frontier. Real players replace ghosts one for one.

Settings (GameConfig > ghosts, Remote Config key srv_ghosts): enabled (the kill switch), seed, launchDay (set it to your release day), start, perDay, cap, fadePerReal, ceilingPct. {"enabled": false} in the dashboard removes them at the next start. The names are in GhostNames.Pool: replace them for your game.

Gifts to one player#

The server can send one online player a gift (currency, heroes, cosmetics, a boost...) as an Inbox letter: the AdminGift endpoint, which accepts only a service account token (never a player's). Its gems widen the server's allowance, so a gift never flags the player. The template ships no tool to call it: a programmer calls the Cloud Code endpoint with the UGS CLI or REST API and a service account allowed to run Cloud Code. Run Export Live Config after adding content so the server knows its ids (srv_rewardIds).

Lifting a flag#

A flag is the server's verdict on one snapshot, so no later snapshot clears it: you do, when a player writes to support.

  1. Unity Dashboard > your project and environment > Player Management: find the player (by name, linked account or their leaderboard row) > Cloud Save > Protected data.
  2. Read srv_flag_reason (why) and srv_flagged.Snapshot (what the phone said) against srv_snapshot.
  3. To lift: edit srv_flagged and set "Lifted": true, or delete srv_flagged. If the player's save starts over for good, also delete srv_snapshot.

Analytics events in detail#

  • The table is GameConfig > analyticsEvents (AnalyticsEventTable.Default()). A row: the game event, the name, up to 8 parameters (each reads the event's id, hero, amount or wave, or a lifetime statistic), everyNthWave sampling and newGroundOnly. Never sent whatever the table says: kills, hits, single summon pulls, reward grants, currency spent, played time.
  • IAnalytics has two calls, SetConsent(bool) and Send(AnalyticsEvent), and never throws. NullAnalytics (default), FakeAnalytics (logs to the Console), UgsAnalytics (Unity Analytics 6), FirebaseAnalyticsAdapter (added next to the others with IDLE_FIREBASE, through CompositeAnalytics). AnalyticsSender is the one listener.
  • Firebase: names Firebase reserves are renamed (game_session_start, game_session_end, game_purchase) or not sent; private parameters (names, e-mails, codes, typed text) are dropped whatever a row says; events with price and currency get value; coarse user properties (payer, tutorial, push answer, Ascension band, play style, days since install, app version). Firebase's session timeout matches the game's 300 s. Before consent it collects nothing; on consent it grants consent mode's four signals (needed for Google Ads).
  • R8 keep rules for Firebase are in BillingKeepRules.androidlib/proguard-rules.txt.
  • Unity Analytics counts at most 500 custom events per player a month; heavy players go past it with everything on. Lower telemetrySummonResultsPerSession, delete rows, or use Firebase, which has no such cap.

Dev tools#

The dev key opens the developer panel: currency, shards, time travel, replaying the intro, deleting the save, the analytics event log. All of it is behind one define, IDLE_DEV_TOOLS:

BuildDev keyDev code compiled
Player, no define (release or Development Build)nono
Player with IDLE_DEV_TOOLSyesyes
Editor, no definenoyes (for tests and editor tools)
Editor with IDLE_DEV_TOOLSyesyes

For testers add the define (Project Settings > Player > Scripting Define Symbols, or the build profile's list), build, and take it out before a store build. The server never runs dev commands. To remove the tools from your game: delete DevGrant.cs, DevSetWave.cs, RegisterDevCommands, Framework/Dev/ and the shell's dev panel fields.