HLK
HyperCasual LiveOps Kit — Developer Documentation
v2.0.0 · Unity 6+

HyperCasual LiveOps Kit

A source-only toolkit that covers the service layer of a mobile game so your team can stay focused on gameplay. Persistence, monetization, LiveOps retention loops, platform integrations, and UI feedback all arrive as decoupled modules that boot themselves, run with zero third-party SDKs installed, and swap to real vendor backends the moment you opt in.

Every module follows the same recipe: a plain-C# service, an interface where a backend can vary, a settings section edited from one editor window, and an in-engine stand-in backend so the whole kit works in the editor from the first minute.

Unity 6+ 32 Modules 460+ Runtime Scripts BiRP / URP / HDRP
Getting Started

Game Setup Assistant

Tools > HLK > Game Setup Assistant is the recommended starting point for a new developer. It edits the real runtime assets directly; it is not a demo or a second configuration copy. Use it for game content and balancing. Use Advanced Toolset for SDK providers, store credentials, AdMob IDs, consent, platform defines, and production checks.

Assistant pageWhat a developer changes thereRuntime source of truth
Game & LevelsMenu/game scenes, visible and initially unlocked levels, win coins, multipliers, progress gifts, currency artStarterMenuConfig.asset
MinigamesWheel slices, slot symbols/payouts, chest drops, per-game cooldowns, chest keysHLKSettings.Minigames + StarterMenuConfig.asset
GoalsDaily/weekly/event goals and permanent achievements, targets, events, rewardsQuestCatalog + QuestDefinition assets
Daily GiftsOrdered streak days, reward amounts, special days, grace policy, rewarded doublingStarterGiftSchedule + HLKSettings.StarterGifts
Economy & LiveOpsIAP products, soft-currency/cosmetic shop items, unlock conditions, offers, battle pass, energy, leaderboard and achievement IDsReferenced catalog/config assets
ServicesAds, haptics, audio, native UI, sharing, rate-us, notifications, analytics, localization, cloud save, privacy, runtime optionsCommon sections of HLKSettings.asset
ValidateScenes, data references, duplicate IDs, icons, build order, documentation, export boundaryRead-only project checks

Recommended first setup

  1. Open the assistant and click Apply Missing Presets. It fills empty references only and never replaces an assigned asset.
  2. On Game & Levels, enter the menu and gameplay scene names and tune the initial level range. Click Add Both to Build Settings; the menu becomes build index 0.
  3. Edit minigame, goal, daily-gift, shop, and battle-pass assets inline. Use the Ping button whenever you want the normal Inspector or want to duplicate an asset.
  4. Open Validate and clear red items. Yellow items are recommendations or production integrations that may intentionally remain simulated during development.
Safe authoring. Creating a goal from the assistant first makes an editable catalog under Assets/HyperCasualLiveOpsKit/GameSetup/. This keeps the shipped preset intact and gives the game its own portable data asset.

Advanced module coverage

The Services page also shows the boundary between designer-safe fields and provider-heavy setup. Ads Pro, IAP Pro, app updates, attribution, crash reporting, deep links, permissions, push messaging, remote config, and web views remain in the Advanced Toolset. They are not omitted: the assistant links to their full configuration pages and its automated coverage test verifies that every public HLKSettings section belongs to exactly one of these two surfaces.

Useful editor and CI entry points

Getting Started

Starter Menu & Level Customization

The reusable portrait menu lives in StarterMenu/Scenes/HLK_MainMenu.unity. Its game destination is configured by name rather than a direct scene reference, which makes the package easy to move between projects.

Levels

Minigames

Cooldowns are authored in minutes and stored as UTC completion timestamps, so closing the app does not reset them. A value of 0 disables that minigame's cooldown. The Mystery Chest key count is clamped to the nine board cells.

MinigameMain dataStarter session rule
Lucky WheelSlice label, weight, currency/item rewardOne spin, optional rewarded multiplier
SlotsSymbols and combination payoutsThree spins plus one rewarded-ad spin
Mystery ChestsWeighted tiers and coin/diamond dropsOne key opens one of nine hidden chests

Goals, achievements, and gameplay events

A QuestDefinition.Kind decides where the item appears: Permanent is an achievement; Daily, Weekly, and Event are goals. Its ID is the persistence key—keep it stable after release. Event names are exact, case-sensitive strings.

// Count one completion for definitions listening to "level_complete".
var props = new Dictionary<string, object> {
    { "level", currentLevel },
    { "stars", earnedStars }
};
AnalyticsBus.Emit("level_complete", props);

// To sum a value, choose EventPropertySum and set PropertyName to "amount".
AnalyticsBus.Emit("currency_earned",
    new Dictionary<string, object> { { "amount", 50 } });

The Achievements DEV panel is for QA progress/claim manipulation only. Do not use DEV actions as production progression logic.

Shop, cosmetics, and reusable unlock rules

The Economy & LiveOps page can create the default Resources-loaded ShopCatalog, add ShopItem assets, and edit both inline. Shop items support coin/gem prices, consumable or permanent ownership, IAP/rewarded-ad entitlement sources, prerequisite items, free-claim cooldowns, and swappable cosmetic effects.

The same page creates level, currency, cooldown, flag, and composite UnlockCondition assets. Assign a condition to an UnlockGate on the content being gated. Conditions only evaluate availability; explicitly consume/spend the requirement from game logic when that is part of your design.

Daily gifts

Index 0 in the schedule is Day 1. Each entry can grant coins, gems, or an item and can be marked special for highlighted presentation. GraceDays controls missed-day tolerance. LoopAfterLastDay repeats the last authored reward; disable it for a finite calendar. The optional HTTP time probe reads a trusted Date header to make simple device-clock cheating harder.

UI and art replacement

Currency, shop, minigame, leaderboard, chest, and battle-pass artwork is stored below StarterMenu/Art and StarterMenu/Resources/HLK/StarterMenuArt. Power-up artwork is intentionally kept with its portable module under PowerUps/Art. Preserve transparent padding when replacing icons, use Sprite (2D and UI), and keep background art on an aspect-cover component so portrait devices do not stretch it. Decorative images must have Raycast Target disabled.

Getting Started

Using the Backend in Another Game

The kit is intentionally portable. Export the following folder as the reusable backend and menu:

Assets/HyperCasualLiveOpsKit

Third-party SDK source and binaries belong outside that folder:

Assets/__DO_NOT_PUBLISH_VENDOR_SDKS

When importing into a new project:

  1. Import the HLK folder and let Unity compile using simulated backends.
  2. For a power-up-only integration, copy the complete PowerUps folder, open Tools > HLK > Power-Ups > Setup & Catalog, and provide a small IPowerUpEffectHandler adapter for each game-specific effect.
  3. Open Game Setup Assistant, apply missing presets, and change both scene names.
  4. Duplicate/tune the catalogs in GameSetup, then connect your gameplay events.
  5. Only after the game-facing validation passes, import the vendor SDKs you need into the excluded folder and configure their matching Advanced Toolset tabs.
  6. Use test ads and fake/sandbox purchases until store console products and privacy flows are ready.
Before publishing: remove DEV access from release UI, disable on-device fake purchases, switch AdMob from test to production IDs, complete consent/privacy setup, and make a signed internal-track build. Never click live ads while testing your own app.
Getting Started

Getting Started

Requirements: Unity 6000.0+, TextMeshPro (com.unity.textmeshpro) and uGUI (com.unity.ugui). UPM installs pull these in automatically; a raw .unitypackage import expects both packages to already be present in the project.

From import to a running showcase in about five minutes:

  1. Import the package. Only the toolkit's own files are added — no registries, no manifest edits, no binaries. Everything compiles immediately because each module defaults to a self-contained in-engine backend.
  2. Open the demo. Load Assets/HyperCasualLiveOpsKit/Demo/HLK_Showcase.unity and press Play. The dashboard exercises nearly every module live: earn coins, spin the wheel, trigger toasts, simulate purchases, and more. (Content Gating / Unlocks is setup-only — it has no demo panel.)
  3. Open Game Setup Assistant. Go to Tools > HLK > Game Setup Assistant for levels, minigames, achievements, goals, daily gifts, shop data, battle pass, and validation. Open its Advanced Toolset only when configuring provider SDKs or platform services.
  4. Generate starter data. Trigger Tools > HLK > Generate Starter Config Assets — it creates ready-made preset assets (IAP catalog, wheel slices, chest tiers, quest catalog, battle pass season, and the rest) under Assets/HyperCasualLiveOpsKit/Presets/, pre-wired into HLKSettings.
  5. Go native when ready. Out of the box the modules run in their safe Dummy / Simulated / In-Engine modes so the project builds and plays with no SDKs. To get real native behaviour on device, switch each module's mode on its Toolset tab — see Real native features on device below for exactly what each one costs (several need only a mode change and no account at all).
Real native features on device. This is the most common surprise: with the shipped defaults, most modules simulate the native action on a phone (log it, copy to the clipboard, or auto-grant) rather than performing it — that is by design, so a fresh import needs no SDKs, permissions, or accounts. Switch each module's mode on its Toolset tab to turn on the real thing. What each costs:
ModuleSet on the ToolsetAlso needs
HapticsMode → Production (now the default)Nothing — real Vibrator / CoreHaptics is built in
Native UI (alerts, action sheets, toasts)Mode → NativeNothing on Android; iOS needs the HLK_NATIVE_UI define
SharingMode → NativeThe HLK_NATIVE_SHARE define — the share bridge is bundled, no SDK
PermissionsAlready Auto — real OS prompts on deviceNothing — the <uses-permission> entries are injected from the usage-description fields you fill in
Local NotificationsMode → ProductionInstall com.unity.mobile.notifications (free Unity package — no account). The HLK_MOBILE_NOTIFICATIONS define switches on automatically once it resolves, and the package adds the POST_NOTIFICATIONS permission for Android 13+ itself.
Deep LinksAdd your scheme(s) to CustomSchemes (and hosts to UniversalLinkHosts)Nothing for custom schemes — the launcher <intent-filter> is injected into the manifest at build time. Universal / App Links additionally need an assetlinks.json hosted on your domain.
Ads, IAP, Push, Game Services, Cloud Save, Attribution, Remote Config, App Update, Rate Us, Crash ReporterMode → its live value + the vendor defineThe vendor SDK and a real account/console (AdMob, Google Play, Firebase, AppsFlyer, …)
No bootstrap scene needed. Services register themselves through RuntimeInitializeOnLoadMethod hooks before your first scene wakes up, in the editor and in builds alike.
Getting Started

Architecture Overview

The kit is organized as independent modules under Runtime/, each with the same internal shape. Understanding the shape once means you understand every module:

The persistent host

MonoHost is a hidden, scene-surviving MonoBehaviour created before the first scene loads. It runs coroutines for headless services (MonoHost.Run), exposes per-frame events, and — critically — provides MonoHost.Post(Action) so background SDK callbacks can hand work back to the main thread.

Threading rules

Stay on the main thread. The service registry, all Unity object access, and every service API in this kit are designed for main-thread use. When a vendor SDK calls you back from a worker thread, wrap the reaction in MonoHost.Post(() => ...) before touching services, scenes, or UI. Async methods in the kit (cloud save, consent, updates) already marshal their engine work back for you — your continuation code should still assume main-thread context and avoid blocking with .Wait() or .Result, which can deadlock the frame.

Results instead of exceptions

Fallible operations return HlkResult / HlkResult<T> — a success flag plus a structured HlkError — rather than throwing across module boundaries. Check result.Success, or use ValueOr(fallback) / TryGet(out value) on the generic form.

Getting Started

Service Registry

HLK.Core.Services is a minimal type-keyed locator. Modules register one shared instance per capability at bootstrap (usually keyed by interface); game code resolves it anywhere without wiring references through the scene.

using HLK.Core; using HLK.Audio; // Throwing lookup — use when the service must exist. var sfx = Services.Resolve<IAudioService>(); sfx.Play(new SoundId("ui_click")); // Soft lookup — use for optional capabilities. if (Services.TryResolve(out IHapticService haptics)) haptics.Play(HapticPatterns.LightTap());
MemberPurpose
Register<T>(impl)Store (or replace) the implementation for capability T. Registering by interface is the recommended pattern.
Resolve<T>()Fetch the implementation; throws InvalidOperationException when nothing is registered.
TryResolve<T>(out impl)Non-throwing fetch; returns false with default when absent.
IsRegistered<T>()Query without resolving.
Unregister<T>() / Clear()Remove one registration / drop everything (mainly for tests).
CountNumber of live registrations.

Registration keys you can rely on after boot include SaveService, IFlowService, IAudioService, IHapticService, IWallet, IAPBridge, AdService, IShopService, IOffersService, StarterGiftService, WheelService, IQuestService, IBattlePassService, IEnergyService, IGameServices, CloudSaveService, IConsentService, IPushService, SharingService, WebViewService, IDeepLinkService, IRemoteConfig, IAppUpdateService, IAttributionService, CrashReporterService, INotificationService, IRateUsService, IPermissionsService, IPopupService, IToastService, ILocalizationService, IEntitlements, ITimeSource, and ICheatService.

Getting Started

Configuration Assets

All module configuration funnels into a single ScriptableObject, HLKSettings, which must live at Assets/HyperCasualLiveOpsKit/Resources/HLK/HLKSettings.asset to be found at runtime (Resources path HLK/HLKSettings). Access it in code through HLKSettings.Instance — if the asset is missing, a transient, fully-defaulted instance is served so nothing null-refs.

using HLK.Core; var settings = HLKSettings.Instance; int fps = settings.General.TargetFrameRate; // default 60 bool verbose = settings.General.VerboseLogging; // default false

Sections

One serializable section per module: Ads, AdsPro, Analytics, AppUpdate, Attribution, Audio, BattlePass, CloudSave, Consent, CrashReporter, DeepLinks, Energy, GameServices, General, Haptics, IAP, IAPPro, Localization, Minigames, NativeUI, Notifications, Offers, Permissions, Push, Quests, RateUs, RemoteConfig, Sharing, WebView, and StarterGifts.

Preset assets

Data-heavy modules reference standalone ScriptableObjects. The generator at Tools > HLK > Generate Starter Config Assets creates a working set under Assets/HyperCasualLiveOpsKit/Presets/ and assigns each into the right settings slot:

Preset assetFeeds
IAPCatalogPreset.assetIAP.CatalogAsset
WheelSlicesPreset.assetMinigames.WheelDatabase
SlotMachinePreset.assetMinigames.SlotsConfig
ChestPreset.assetMinigames.ChestsConfig
StarterGiftsPreset.assetStarterGifts.Schedule
QuestCatalogPreset.assetQuests.Catalog
BattlePassPreset.assetBattlePass.Config
EnergyPreset.assetEnergy.Config
OfferCatalogPreset.assetOffers.Catalog
Localization/ tablesLocalization.TableSet
General tab. Project-wide knobs (TargetFrameRate, NeverSleep, VerboseLogging) live in the General section and are applied by ServiceBootstrap before the first scene. Edit them on the General tab of the Toolset.
Core Infrastructure

Save System

A versioned, migration-aware persistence layer built around one consolidated record. Game state lives in a single SaveBlob (currency balances, owned items, streak data, string flags) that is serialized as JSON through a pluggable ISaveBackendPlayerPrefsSaveBackend by default.

using HLK.Core; using HLK.Save; var save = Services.Resolve<SaveService>(); // Mutate the in-memory blob, then schedule a debounced write. var blob = save.Current; blob.SetCurrency("Coins", 500); blob.SetFlag("tutorial_done", "1"); save.RequestSave(); // coalesced; flushes ~1 s later save.FlushNow(); // synchronous write for critical moments
MemberPurpose
CurrentThe live SaveBlob; loaded lazily on first access.
Load() / Peek()Read from the backend (Peek does not adopt the result).
RequestSave()Debounced write — multiple calls in the window collapse into one flush (default window: DefaultDebounceSeconds = 1.0f).
FlushNow()Immediate write; returns an HlkResult.
DeleteAll()Wipe stored data.
Migrate(json)Run the migration chain over a raw stored payload.
Loaded / SavedEvents after a load resolves / a flush succeeds.
AttachLifecycle()Hook pause/quit so unsaved changes auto-flush.

Schema versioning

The current schema version is SaveSchema.Current = 2. Older payloads pass through SaveMigrator, which chains ISaveMigration steps (Migration_V0_to_V1, Migration_V1_to_V2) until the record is current. Add your own step when you change the blob's shape.

Storage keys are namespaced under the HLK_ prefix — the blob itself sits at HLK_SAVE_V1 with a schema marker alongside it.

Core Infrastructure

Scene Flow & Timing

Scene navigation with a built-in fade, plus the small timing primitives the rest of the kit leans on: serializable cooldowns, swappable clocks, and a lightweight tween helper.

Faded scene loads

using HLK.Core; using HLK.Flow; var flow = Services.Resolve<IFlowService>(); // By name, build index, or SceneReference; optional FadeConfig + LoadSceneMode. await flow.LoadSceneAsync("Level_02"); await flow.FadeToBlackAsync(); // fade only, no load await flow.FadeFromBlackAsync();

SceneFadeService implements IFlowService, raises TransitionStarted / TransitionCompleted, and reports IsTransitioning while a fade-load-fade sequence runs. A SceneReference field gives designers a drag-and-drop scene picker that survives renames.

Cooldowns

var cd = new Cooldown(durationSeconds: 3600); cd.Stamp(time); // start now (ITimeSource) if (cd.IsReady(time)) { /* claim */ } float t = cd.Progress01(time); // 0..1 for radial fills string s = cd.Serialize(); // persist; restore via FromSerialized

Time sources

ITimeSource abstracts "what time is it" so reward logic can resist device-clock cheating. LocalTimeSource uses the device clock; ServerTimeSource anchors to a remote clock. TimeSourceFactory registers the configured one as the shared ITimeSource.

Tweens

TweenRunner (in HLK.Core) animates floats, vectors, transforms, and CanvasGroup alpha on the persistent host. The Easing library covers the standard curve families — Sine, Quad, Cubic, Quart, Quint, Expo, Circ, plus Back, Elastic, and Bounce — each with in / out / in-out variants selected via EaseType.

Settings pointer: startup frame-rate and sleep behavior belong to the General tab (Tools > HLK > Open Toolset).
Core Infrastructure

Audio Engine

Pooled one-shot SFX and crossfading music behind a tiny interface. Designers author a SoundBank asset — named entries with a clip, category, volume, pitch, and optional pitch jitter — and code addresses sounds by SoundId (implicitly convertible from string).

using HLK.Core; using HLK.Audio; var sound = Services.Resolve<IAudioService>(); sound.Play("coin_pickup"); // string → SoundId sound.Play(new SoundId("explosion"), 0.6f); // scaled volume sound.PlayMusic("theme_main", fadeSeconds: 1.5f); sound.StopMusic(); sound.StopAll();

Portable game sound palette

The included AudioLibrary/Clips folder contains 45 clearly named WAV effects. Run Tools > HLK > Audio > Install Game Sound Library after importing or replacing clips. The installer builds HLKGameSoundBank, assigns it to settings, and configures a 32-voice pool so fast repeated sounds—including the same sound—can overlap.

// Semantic ids keep gameplay independent from clip filenames. GameSoundManager.Play(GameSoundCue.UiButtonTap); GameSoundManager.Play(GameSoundCue.CoinCollect); GameSoundManager.Play(GameSoundCue.PowerUpUse); // This is the same preference used by the StarterMenu Settings toggle. GameSoundManager.SetSoundEnabled(false);
MemberPurpose
Play(id) / Play(id, volumeScale)Fire a one-shot from the pool.
PlayMusic(id, fadeSeconds)Start or crossfade the music track (default fade comes from settings).
StopMusic(fadeSeconds) / StopAll()Fade out music / stop all one-shot SFX (music is untouched — use StopMusic for the track).
IsMusicPlayingTrue while a music track is audible.

Mixing model

The final gain of a voice is the product of three sliders: Master × Category × entry volume. Categories (Music, Sfx, UI, Voice, Ambient) each have a volume and — for Master, Music, and Sfx — a mute toggle in settings. AudioSettings.PoolSize controls how many pooled AudioSource components serve one-shots. The included game palette installs 32 voices; keep at least 24 for dense currency/confetti sequences.

Settings pointer: assign the DefaultSoundBank and tune the mixer on the Audio tab (Tools > HLK > Open Toolset).
Core Infrastructure

Haptic Feedback

Vibration with a pattern library, expressed as duration/amplitude pairs that translate to Android vibrator waveforms and iOS haptic calls. The static Haptics facade forwards to the registered IHapticService.

using HLK.Haptics; Haptics.LightTap(); // canned patterns Haptics.Success(); Haptics.Play(HapticPatterns.Heartbeat()); Haptics.Play(durationMs: 40, amplitude: 180); // raw pulse Haptics.Cancel();

Pattern library

HapticPatterns ships: Tick, LightTap, MediumTap, HeavyThud, LongBuzz, DoubleTap, TripleTap, Heartbeat, RampUp, RampDown, Success, Error, CoinPickup, LevelComplete, GameOver, and Explosion. HapticPattern assets let designers author their own patterns without touching code — create one via Assets ▸ Create ▸ HLK ▸ Haptics ▸ Pattern, and a few sample patterns ship under Presets/Haptics. (HapticClip is the runtime struct these compile down to, not an authorable asset.)

Backends & modes

ModeBackendNotes
DisabledNullHapticBackendEverything no-ops.
DummyEditorHapticsBackendLogs simulated pulses in the editor.
ProductionAndroidHapticsBackend / IOSHapticsBackendReal vibration; Android needs the VIBRATE permission (one-click button on the Haptics tab).
Settings pointer: mode and per-platform enables on the Haptics tab (Tools > HLK > Open Toolset).
Core Infrastructure

Virtual Economy

A persisted wallet for the game's currencies. CurrencyId enumerates Coins (soft) and Gems (premium); balances are stored by stable string key so saved data survives refactors. EconomyService wires the wallet to persistence and registers it as IWallet.

using HLK.Core; using HLK.Economy; var wallet = Services.Resolve<IWallet>(); wallet.Credit(CurrencyId.Coins, 250, reason: "level_win"); if (wallet.CanAfford(CurrencyId.Gems, 10)) wallet.Debit(CurrencyId.Gems, 10, reason: "skip_timer"); long coins = wallet.GetBalance(CurrencyId.Coins); wallet.BalanceChanged += (currency, newAmount) => RefreshHud();
MemberPurpose
Credit / DebitAdjust a balance; returns HlkResult<long> with the new amount. Debit fails cleanly on insufficient funds.
CanAfford(currency, amount)Pre-check without mutating.
SetBalance / ResetDirect writes (cheats, migrations).
GetRecentChanges(count)Ring-buffer transaction history (capacity 64 by default) with reasons.
BalanceChangedEvent: (CurrencyId, long newBalance).

UI companions: CurrencyHud binds a live balance readout, and CoinGatherEffect flies collected coin sprites toward the HUD counter before the credit lands.

Currency icons & names. Presentation data (display name, icon) for each currency is driven by a CurrencyCatalog asset. Create one via Assets ▸ Create ▸ HLK ▸ Economy ▸ Currency Catalog and place it at Assets/HyperCasualLiveOpsKit/Resources/HLK/CurrencyCatalog.asset; it is auto-loaded from that Resources path (HLK/CurrencyCatalog) by the HUD and icon resolver. It is not generated by default — until you author one, the wallet still works but has no per-currency icons/names.
Core Infrastructure

Localization

Key-to-string translation with per-locale tables. A LocalizationTableSet groups one LocalizationTable per locale code; LocalizationService picks the active locale (persisted across sessions under HLK.Locale) and falls back gracefully — missing keys return the supplied fallback or the key itself instead of blowing up.

using HLK.Core; var loc = Services.Resolve<ILocalizationService>(); string title = loc.Get("shop.title", fallback: "Shop"); loc.SetLocale("fr"); // raises LocaleChanged var locales = loc.AvailableLocales; // e.g. ["en", "fr"]
MemberPurpose
Get(key, fallback) / TryGet(key, out value)Translate a key in the active locale, consulting the fallback table when absent.
SetLocale(locale)Switch language at runtime; persists the choice.
CurrentLocale / AvailableLocalesActive code and everything the table set covers.
LocaleChangedEvent fired with the new locale code.

For scene text, add the LocalizedText component next to a label: it looks up its key on enable and re-resolves automatically when the locale changes. Sample tables for en and fr ship with the kit, and the preset generator can scaffold a table set under Presets/Localization/.

Settings pointer: table set, fallback table, and DefaultLocale (default "en") on the Localization tab (Tools > HLK > Open Toolset).
Monetization

In-App Purchases

Store-neutral purchasing through IAPBridge. Products are authored as IAPProduct assets collected into an IAPCatalog; each product carries a PurchaseReward hook that grants its contents on purchase, restore, or coupon redemption — one grant path for all three.

using HLK.Core; using HLK.IAP; var iap = Services.Resolve<IAPBridge>(); await iap.InitializeAsync(); // idempotent PurchaseResult r = await iap.PurchaseAsync("coins_medium"); if (r.Success) Celebrate(); else Debug.Log($"Failed: {r.Reason} ({r.Detail})"); string price = iap.GetLocalizedPrice("coins_medium"); RestoreResult restored = await iap.RestoreAsync();
MemberPurpose
InitializeAsync()Bring the store backend up against the catalog; returns false instead of throwing when the store cannot become ready.
PurchaseAsync(productId)Full flow: visibility window check → store purchase → optional validation → reward grant → funnel analytics.
RestoreAsync()Re-grant non-consumables and subscriptions; consumables are never restored.
GetLocalizedPrice(productId)Store-localized price string with a configurable fallback.
RedeemCouponAsync(code)Single-use promo code → mapped product grant.
Purchased / PurchaseFailedEvents after any grant / any failure with its normalized reason.

Product kinds & failure reasons

IAPProductKind: Consumable, NonConsumable, Subscription. Failures normalize to PurchaseFailureReason: UserCancelled, UnknownProduct, AlreadyOwned, PaymentDeclined, StoreNotReady, and more.

Backends

ModeBackendRequires
DummyDummyStoreBackendNothing. Simulated outcome (AlwaysSucceed / AlwaysFail / AlwaysCancel / Random), configurable latency (default 400 ms), optional fake store dialog in the editor.
Test / ProductionUnityPurchasingBackendUnity IAP package + HLK_UNITY_PURCHASING
Settings pointer: mode, catalog asset, and dummy simulation on the Purchases tab (Tools > HLK > Open Toolset).
Monetization

Purchases Pro

The advanced purchasing layer on top of the base flow: subscription tiers, receipt validation, limited-time visibility, and coupon mappings, all configured in the IAPPro settings section.

Subscriptions

var iap = Services.Resolve<IAPBridge>(); if (iap.HasActiveSubscription) ApplyVipPerks(iap.ActiveSubscriptionTier); bool gold = iap.HasSubscriptionTier("gold"); // rank-aware tier gate if (iap.TryGetSubscriptionExpiry("sub_gold_monthly", out var expiry)) ShowRenewalDate(expiry);

Tiers are declared as SubscriptionTier rows (product id, tier name, rank); a higher rank satisfies checks for lower ones. SubscriptionGraceDays (default 3) keeps perks alive briefly past expiry, and ReverifyOnAppStart re-checks receipts each launch. GetSubscriptionOfferText(productId) surfaces free-trial / introductory / promotional / win-back marketing copy from the product's SubscriptionOffers.

Receipt validation

Enable EnableServerValidation and point ValidatorEndpoint at your server. ValidationFailurePolicy decides what an unverifiable purchase does: Block (withhold the grant), GrantOptimistically, or Defer (queue and re-check via ReverifyQueuedAsync()).

Limited-time products & coupons

LimitedTimeProducts constrain when a product may be bought — IsWithinVisibilityWindow(productId) tells the shop UI whether to show it, and NotifyOfferSeen(productId) records an impression. Coupons map redeem codes to products for RedeemCouponAsync; each code redeems once per device.

Settings pointer: everything above lives on the Purchases Pro tab (Tools > HLK > Open Toolset).
Monetization

Ads & Mediation

One ad API across every network. AdService resolves an IAdBackend from the configured mediation provider, checks pacing and the ad-free entitlement before every intrusive show, and normalizes the outcome into AdShowResult / RewardedResult.

using HLK.Core; using HLK.Ads; var ads = Services.Resolve<AdService>(); await ads.InitializeAsync(); RewardedResult reward = await ads.ShowRewardedAsync(rewardedPlacement); if (reward.Completed) GrantDoubleCoins(); else if (reward.Suppressed) Debug.Log(reward.Reason); // pacing or ad-free bool shown = await ads.ShowInterstitialAsync(interstitialPlacement); ads.LoadBanner(bannerPlacement); ads.HideBanner();

Placements

AdPlacement is a ScriptableObject naming one ad slot: an id, an AdUnitKind (Rewarded, Interstitial, Banner, AppOpen, RewardedInterstitial, Native), and optional per-placement pacing overrides (min interval, session/day caps, quiet-first-levels). Rewarded placements can opt out of the ad-free entitlement via IgnoreNoAdsEntitlement — players who paid to remove ads usually still want to choose rewarded ones.

Outcomes

AdShowOutcomeMeaning
CompletedWatched to the end — grant the reward.
SkippedDismissed early; the ad was shown but no reward is due.
SuppressedBlocked before display (pacing rule or ad-free entitlement) — see Reason.
FailedLoad or show error.
NotSupportedThe active backend cannot serve this format.

Providers

MediationProviderBackendDefine
AdMobDirectAdMobBackendHLK_ADMOB
UnityLevelPlayLevelPlayBackendHLK_LEVELPLAY
AppLovinMaxAppLovinBackendHLK_APPLOVIN_MAX
UnityAdsDirectUnityAdsBackendHLK_UNITY_ADS

AdMode selects Dummy (the MockAdBackend renders a fake ad overlay in-engine — no SDK at all), Test (real SDK, test units), or Production. AdsDiagnosticsOverlay can be toggled in development builds to watch fill, pacing decisions, and revenue callbacks live.

AdMob: set your App ID before building. Enter your real AdMob App ID in Assets ▸ Google Mobile Ads ▸ Settings before you build — a missing or left-in sample App ID crashes the app at launch. Per-placement unit ids and the App IDs are entered on the Ads tab's AdMob card; leave the test unit ids in place during development and they default to Google's public sample ids.
Settings pointer: mode, provider, app keys, and unit ids on the Ads tab (Tools > HLK > Open Toolset).
Monetization

Ads Pro & Ad-Free

Extended formats, global frequency capping, and the paid ad-removal path, configured in the AdsPro settings section.

Extended formats

Opt-in toggles enable App-Open ads (with a minimum interval, first-run suppression, and optional on-resume showing), rewarded interstitials, and native ads — each with per-platform test/production unit ids. Preload switches (PreloadRewardedOnStart, PreloadInterstitialOnStart, ReloadAfterShow) keep inventory warm.

Frequency capping

AdPacing enforces the global rules before any intrusive ad shows: InterstitialMinIntervalSec (default 60), InterstitialMaxPerSession and InterstitialMaxPerDay (0 = uncapped), NoInterstitialsBeforeLevel, and a boot grace window. Feed it progress via Pacing.SetLevel(n) / AdvanceLevel(). A blocked show resolves as Suppressed with the failing rule in Reason.

Ad-free entitlement

using HLK.Core; // Granted by an IAP reward hook; checked automatically by AdService. var owned = Services.Resolve<IEntitlements>(); if (owned.NoAds) HideBannerContainer();

The entitlement flag defaults to the key hce.entitlement.ad_free (AdFreeEntitlementKey). Three switches decide what it silences: AdFreeHidesBanners, AdFreeHidesInterstitials, and AdFreeHidesAppOpen — all on by default. Rewarded ads keep working unless a placement says otherwise. Impression-revenue forwarding (EnableImpressionRevenue) publishes paid events through AdRevenueHub for attribution and analytics.

Settings pointer: the Ads Pro tab (Tools > HLK > Open Toolset).
Monetization

Shop & Cosmetics

A catalog-driven store for currency-priced, ad-unlocked, IAP-linked, gated, and cooldown-gift items, with an equip system that applies cosmetic effects to scene objects.

using HLK.Core; using HLK.Shop; var shop = Services.Resolve<IShopService>(); PurchaseOutcome bought = shop.TryPurchase("skin_neon"); if (bought.IsSuccess()) shop.TryEquip("skin_neon"); bool owned = shop.IsOwned("skin_neon"); string active = shop.EquippedId; shop.PurchaseCompleted += e => RefreshShopGrid(); shop.EquipChanged += e => RefreshCharacter();

Item anatomy

A ShopItem asset combines an id, display info, a ShopItemKind (NonConsumable / Consumable), and exactly one acquisition style:

Cosmetics

Equippable items reference a CosmeticEffect: MaterialSwapEffect, MeshSwapEffect, or ChildToggleEffect. Equipping is exclusive per EquipTarget, and ShopItemView renders one catalog entry with its price, state, and buttons. Ownership persists through PlayerPrefsShopStore.

Settings pointer: the shop catalog is a ShopCatalog asset referenced by your scene's ShopCatalogProvider; minigame-adjacent data lives on the Mini-Games tab (Tools > HLK > Open Toolset).
Monetization

Timed Offers

Limited-window promotions that appear for the right player at the right moment. Offers are OfferDefinition assets in an OfferCatalog; the service tracks eligibility, impressions, and lifetime caps per offer.

using HLK.Core; using HLK.Offers; var offers = Services.Resolve<IOffersService>(); // PULL: what may I show right now? foreach (var offer in offers.GetEligibleOffers()) ShowOfferBanner(offer); // PUSH: react the moment something becomes available. offers.OfferActivated += offer => PopOfferDialog(offer); offers.NotifyEvent("level_fail"); // arms event-triggered offers offers.ReportShown("starter_pack"); // count an impression offers.Purchase("starter_pack", r => { if (r == OfferPurchaseResult.Success) Confetti(); });

Offer kinds

OfferKindTrigger
FirstSessionsActive between session numbers FirstSessionStart and FirstSessionEnd (feed the count via SetSessionCount).
TimedWindowActive between two UTC timestamps (WindowStartUtc / WindowEndUtc).
EventTriggeredArmed for ArmWindowSeconds after a named gameplay event (e.g. level_fail) arrives via NotifyEvent.
AlwaysAlways a candidate — reads none of the trigger fields; only MaxLifetimeShows and CooldownSeconds gate when it surfaces.

Presentation fields cover discount display: marketing price tags, an original-price strikethrough, DiscountPercent, banner art, and an accent color. MaxLifetimeShows (default 3) and CooldownSeconds throttle repeats; state persists per offer and can be cleared with ResetState / ResetAllState during QA.

Settings pointer: catalog and analytics binding on the Offers tab (Tools > HLK > Open Toolset).
LiveOps & Engagement

Daily Rewards

A login-streak reward loop ("starter gifts"): the player claims one gift per UTC day from a designer-authored StarterGiftSchedule, streaks advance the ramp, and a rewarded ad can multiply the claim.

using HLK.Core; using HLK.DailyReward; var gifts = Services.Resolve<StarterGiftService>(); gifts.RegisterLogin(); // rollover + grace bookkeeping if (gifts.IsClaimAvailable) { var claim = gifts.Claim(); // HlkResult<ClaimResult> if (claim.Success) Debug.Log($"Day {claim.Value.DayNumber}: {claim.Value.Gift}"); } TimeSpan wait = gifts.TimeUntilNextClaim; // countdown for the UI

Streaks & grace

Claiming on consecutive UTC days grows CurrentStreak. Missing days is judged against StarterGifts.GraceDays: a gap is forgiven while the number of fully-missed days stays at or below the grace allowance; one more and the streak resets to zero so the next claim restarts at day 1. The default grace is 0 — strictly consecutive. Re-entering the app on the same day is always a no-op.

Stock schedule

The generated preset is a looping 5-day cycle (LoopAfterLastDay = true):

DayGiftLabel
1150 CoinsWelcome Bonus
2275 CoinsWarming Up
312 GemsGem Drop
4450 CoinsAlmost There
535 Gems (special)Streak Finale

Boosted claims & UI

ClaimBoosted(multiplier) multiplies currency amounts; ClaimWithAdBoost runs an IRewardedAdGate first (wired to the DoubleAdPlacement setting) and claims only on completion. StarterGiftPanel, StarterGiftTile, and StarterGiftCountdown render the calendar — each tile reflects a GiftDayState of Claimed, Today, or Locked. Preview helpers (PreviewCurrent, PreviewDay, PreviewUpcoming) never mutate state.

Clock hardening

DailyCooldownService offers a simpler stamped-cooldown variant with a paid retry (TryPaidRetry). To resist device-clock rollback, set a TimeProbeUrl: RemoteHttpTimeSource estimates server UTC from a single HTTP HEAD request's Date header — no payload, no identifiers. Leave the field empty to rely on the device clock only.

Settings pointer: schedule asset, grace days, launch overlay, and server time on the Starter Gifts tab (Tools > HLK > Open Toolset).
LiveOps & Engagement

Reward Wheel

A weighted prize wheel split into headless selection and cosmetic spin. The outcome is decided by weight before the wheel ever moves; the animation then lands on the pre-selected slice, so presentation can never desync from the grant.

using HLK.Core; using HLK.Wheel; var wheel = Services.Resolve<WheelService>(); // or WheelService.FromSettings() if (wheel.TrySelect(out WheelSpinResult result)) { GrantReward(result.RewardId, result.Amount); // Hand result.Index to the WheelView so the spin lands there. }

Slice data

A WheelSliceDatabase asset holds the slices. Each WheelSlice carries RewardId, Amount, Weight (relative probability; 0 or negative excludes the slice), icon, label, an optional localization key, and a wedge color. Selection uses WeightedSelector; SampleDistribution(n) returns an empirical histogram so you can sanity-check drop rates, and UseFixedSeed(seed) makes outcomes reproducible for tests.

Spin presentation

WheelSpinSettings.Default spins for 3.2 seconds over 5 full turns with a cubic-out deceleration. WheelSpinner.ComputeTargetAngle converts the winning index into a final rotation (with jitter inside the wedge), and SegmentTicker raises a Tick event each time the pointer crosses a slice boundary — hook it to a click sound and a light haptic for a convincing ratchet. WheelView builds the wedge visuals from the database.

Settings pointer: the slice database lives under Mini-Games (Tools > HLK > Open Toolset).
LiveOps & Engagement

Chest & Slot Minigames

Two compact reward minigames sharing one RNG and reward-granting core (MinigameRng, WeightedPicker, MinigameRewardGranter). Both are headless services you can drive from any UI.

Chests

using HLK.Minigames.Chests; var chests = ChestService.FromServices(config); ChestTier[] board = chests.PresentRoll(); // e.g. 3 chests to pick from ChestOpenResult opened = chests.Open(board[0]); if (opened.Success) Show(opened.Drop);

ChestConfig defines the tier list (each tier has an id, display name, key cost, and a weighted drop table) plus ChestsToShowAtOnce (default 3) and whether duplicate tiers may appear on one board. For animated reveals, split the flow: RollDrop decides the prize, your animation plays, then GrantRolled pays it out.

Slot machine

using HLK.Minigames.Slots; var slots = SlotMachine.FromServices(config); if (slots.TrySpin(out SlotMachineResult spin)) ShowReels(spin); // symbols per reel + payout, spin cost already debited

SlotMachineConfig defaults: 3 reels, a spin cost of 10 coins, a 1.2-second spin with 0.3-second reel stagger. Symbols are weighted; payouts map symbol combinations to rewards. Validate(problems) on either config lists authoring mistakes before they reach players.

Settings pointer: chest and slot configs on the Mini-Games tab (Tools > HLK > Open Toolset).
LiveOps & Engagement

Quests

Windowed goal tracking driven by the same event names you already emit for analytics. Quests are QuestDefinition assets in a QuestCatalog; the service scores incoming events against them, tracks per-window progress, and pays currency on claim.

using HLK.Core; using HLK.Quests; var quests = Services.Resolve<IQuestService>(); // Progress everything that matches this event. quests.IngestEvent("stage_clear", new Dictionary<string, object> { { "score", 4200 } }); if (quests.CanClaim("daily_win_3")) quests.Claim("daily_win_3"); // grants the reward int claimed = quests.ClaimAllCompleted(); quests.Completed += q => Services.Resolve<IToastService>().Show("Quest complete!", ToastKind.Success);

Windows & matching

QuestKind scopes a quest to a window: Daily, Weekly, Event, or Permanent. When the window rolls over (UTC), progress resets automatically. A definition matches by EventName, counts either occurrences or a numeric event property (QuestMatchKind + PropertyName), can require a property filter (ConditionPropertyName / ConditionValue), and finishes at Goal. Rewards are a CurrencyId + RewardAmount.

Advance(questId, amount) progresses a quest directly when you'd rather not go through events. Progress events: ProgressChanged, Completed, Claimed, and a coarse Changed.

Settings pointer: catalog and analytics binding on the Quests tab (Tools > HLK > Open Toolset).
LiveOps & Engagement

Battle Pass

A seasonal XP ladder with parallel free and premium reward tracks. XP arrives from gameplay events or direct grants; premium unlocks through an IAP product; every tier holds up to two claimable rewards.

using HLK.Core; using HLK.BattlePass; var pass = Services.Resolve<IBattlePassService>(); pass.GrantXp(50); // or IngestEvent("level_complete") int tier = pass.CurrentTier; if (pass.CanClaim(tier, BattlePassTrack.Free)) pass.Claim(tier, BattlePassTrack.Free); if (!pass.IsPremiumUnlocked) ShowPremiumUpsell(pass.Config.PremiumProductId); pass.TierAdvanced += a => PlayTierUpFanfare();

Season configuration

BattlePassConfig holds the SeasonId (changing it starts a fresh season), the tier ladder (each BattlePassTier sets an XP threshold and a reward per track), the premium product id, and the XP feed: a CSV of qualifying event names (or AcceptAllEvents), with XpSource choosing between a flat XpPerEvent and reading a numeric event parameter. Reward kinds cover currency and item grants — item fulfillment routes through your IBattlePassItemFulfiller.

Events: XpChanged, TierAdvanced, RewardClaimed, ItemRewardGranted, PremiumUnlocked, Changed.

Wiring it up

Settings pointer: season config on the Battle Pass tab (Tools > HLK > Open Toolset).
LiveOps & Engagement

Energy System

A regenerating lives/energy meter that gates level attempts, refills over wall-clock time (including while the app is closed), and sells refills through ads or IAP.

using HLK.Core; using HLK.Energy; var energy = Services.Resolve<IEnergyService>(); if (energy.TrySpend(1).Success) StartLevel(); else ShowRefillDialog(); energy.Refill(energy.RewardedAdRefillAmount); // after a rewarded ad energy.AmountChanged += n => hud.SetHearts(n, energy.Max); energy.Emptied += () => ShowOutOfEnergyPopup();

Defaults

Field (EnergyConfig)Default
Max / Start5 / 5
RegenSeconds480 (one unit per 8 minutes)
CostPerLevel1
FullRefillProductIdenergy_full_refill
RewardedAdRefillAmount3

Reconcile() recomputes regeneration against elapsed real time — the EnergyFocusWatcher calls it on app focus when RecalcRegenOnFocus is enabled, so returning players find the hearts they earned while away. Regeneration stops at Max, and every top-up path is clamped to Max as well — Refill / RefillToMax discard any excess and return the number of units actually added.

Settings pointer: config asset and focus behavior on the Energy tab (Tools > HLK > Open Toolset).
LiveOps & Engagement

Content Gating

Declarative lock/unlock rules for buttons, levels, and features. Designers author UnlockCondition assets, attach an UnlockGate component to the UI, and the gate keeps the target's interactable state, label, and locked overlay in sync.

Condition types

AssetUnlocks when…
CurrencyUnlockConditionthe wallet holds enough of a currency. It only checks affordability and never debits — performing the actual spend is the game's job.
LevelUnlockConditionthe player level reaches a threshold.
FlagUnlockConditiona named boolean flag is set (tutorials, one-time unlocks).
CooldownUnlockConditiona timed cooldown has elapsed; consuming re-stamps it.
CompositeUnlockConditionall / any of a list of child conditions pass.
using HLK.Gating; // Evaluate anywhere, not just on a component: GateStatus status = GatingService.Instance.Evaluate(condition); if (!status.Unlocked) label.text = status.LockReason; GatingService.Instance.SetFlag("beat_world_1", true); GatingService.Instance.RequestReevaluate(); // nudge every gate to refresh

UnlockGate re-evaluates on a timer (default every 0.5 s) and raises StatusChanged. With ConsumeOnClick enabled, clicking the unlocked target runs the condition's consume step. Only stateful conditions mutate here: a CooldownUnlockCondition re-stamps its timer on consume. Currency conditions do not spend on consume — they only check affordability, so your game must perform the actual wallet debit itself.

Player state flows through an IPlayerContext. The built-in DefaultPlayerContext bridges the wallet and save flags automatically, but the level number is not auto-wired — with no level provider it reports 0, so a LevelUnlockCondition stays locked (unless its threshold is 0). Supply a level provider, or register your own context, before level gates work:

using HLK.Gating; using HLK.Gating.Context; // Option A — feed your progression level into the default context: GatingService.CreateAndRegister( new DefaultPlayerContext(() => SaveModel.Level)); // () => int provider // Option B — register a fully custom IPlayerContext (server-authoritative, tests): GatingService.CreateAndRegister(myPlayerContext);
LiveOps & Engagement

Rate-Us Prompts

Store review requests with strict throttling, an optional soft pre-prompt, and platform-correct backends. The service tracks sessions and prompt history so review dialogs appear rarely and only to players who have had time to enjoy the game.

using HLK.Core; using HLK.RateUs; var rate = Services.Resolve<IRateUsService>(); // rate.RegisterSession(); // already called once per launch by RateUsBootstrap — do NOT call again var outcome = await rate.MaybeRequestReviewAsync(); // gated by throttles // QA helpers: await rate.ForceRequestReviewAsync(); // bypass every gate await rate.OpenStorePage(); // direct store listing

Throttle gates

SettingDefaultEffect
SessionsBeforeFirstPrompt5Never ask before this many sessions.
DaysBetweenPrompts14Cooldown after any prompt.
LifetimePromptCap3Hard ceiling per install.
StopAfterRatedtrueOnce rated, never ask again.

Soft pre-prompt

When PrePromptEnabled is on (default), a friendly in-game question runs before the real store dialog, protecting your quota from players likely to decline. The default copy — title "Having fun so far?", body "Would you mind rating us?", buttons "Rate it" / "Maybe later" — is fully editable in settings. Provide the dialog UI by assigning a RateUsSoftPrompter delegate via SetSoftPrompter — for example route it through the Popup Stack.

Backends

RateUsMode: Disabled, Dummy (logs, no dialog), or Production — Android uses Google Play In-App Review, iOS uses the system review controller, and GetGateState() plus ResetGates() support QA. Fill in AndroidPackageName / IOSAppId for the store-page fallback link.

The Android backend (AndroidRateUsBackend) reaches the Play In-App Review library entirely by reflection, so no scripting define or managed reference is needed to compile. To make the native stars popup actually fire on device, add the Play review Gradle dependency com.google.android.play:review:2.0.1 — the Rate Prompt tab has a one-click button that inserts it into your Android mainTemplate.gradle. Without that dependency the request reports "unavailable" at runtime and the service falls back to the Play Store listing.

Settings pointer: throttles, copy, and store ids on the Rate Prompt tab (Tools > HLK > Open Toolset).
LiveOps & Engagement

Local Notifications

Schedule on-device reminders — energy refilled, gift ready, streak about to break — with permission handling on both mobile platforms and a log-only backend everywhere else.

using HLK.Core; using HLK.Notifications; var notify = Services.Resolve<INotificationService>(); var perm = await notify.RequestPermissionAsync(); if (perm.Success && perm.Value == NotificationPermissionStatus.Granted) { var handle = notify.Schedule( "Energy refilled!", "Your hearts are full — jump back in.", TimeSpan.FromMinutes(50)); // Later: notify.Cancel(handle.Value); } notify.CancelAll();
MemberPurpose
RequestPermissionAsync()Prompt for authorization; resolves to a NotificationPermissionStatus.
Schedule(request) / Schedule(title, body, fireAfter)Queue a one-shot or repeating (Once/Hourly/Daily/Weekly) notification; returns a cancellable NotificationHandle.
Cancel(handle) / CancelAll()Remove pending notifications; stale handles no-op.
GetPending()Diagnostic snapshot of everything still scheduled.

NotificationMode: Disabled, Dummy (log-only; the editor default), or Production — the mobile backend requires Unity's Mobile Notifications package and HLK_MOBILE_NOTIFICATIONS. Only the default hce_default Android channel is created automatically; there is no settings list of extra channels — supply a custom category/channel per notification via NotificationRequest.CategoryId. The iOS alert/badge/sound options do come from settings.

Settings pointer: mode, iOS options, and auto-request behavior on the Notifications tab (Tools > HLK > Open Toolset).
Platform Services

Game Services

Leaderboards, achievements, and platform sign-in behind one interface. Your code speaks in logical ids; a GameServicesCatalog asset maps each one to its Game Center and Play Games store ids, so gameplay code never branches per platform.

using HLK.Core; using HLK.GameServices; var gs = Services.Resolve<IGameServices>(); await gs.InitializeAsync(); var signIn = await gs.SignInAsync(); if (signIn.Success) Debug.Log($"Hello {signIn.Value.DisplayName}"); await gs.SubmitScoreAsync("weekly_score", 12_400); await gs.UnlockAchievementAsync("first_win"); await gs.IncrementAchievementAsync("collect_100_coins", 25.0); await gs.ShowLeaderboardUIAsync("weekly_score");
MemberPurpose
SignInAsync() / SignOutAsync()Authenticate the local player; SignInChanged fires with the player info.
SubmitScoreAsync(id, score)Post to a logical leaderboard; optionally mirrored to analytics.
LoadTopScoresAsync(id, count)Fetch entries for a custom leaderboard UI.
UnlockAchievementAsync / IncrementAchievementAsyncBinary and percentage-based achievements.
ShowLeaderboardUIAsync / ShowAchievementsUIAsyncOpen the platform overlay.

LeaderboardScoreFormatter renders scores per the catalog's LeaderboardScoreFormat (Number, TimeCentiseconds, Currency).

Put the catalog in Resources. Create the catalog via Assets ▸ Create ▸ HLK ▸ GameServices ▸ Catalog and place it at a Resources/HLK/ path — the bootstrap loads it from Resources.Load("HLK/GameServicesCatalog"). If the asset lives anywhere else it is never found, and your logical ids are submitted to the platform verbatim (the per-platform Game Center / Play Games id mapping never applies).

Providers

GameServicesProviderBackendDefine
AutoPicks per platform
DummyDummyGameServicesBackend (in-memory)
GameCenterGameCenterBackendHLK_GAME_CENTER
PlayGamesPlayGamesBackendHLK_PLAY_GAMES_SERVICES
Settings pointer: provider, auto sign-in, and sample ids on the Game Services tab (Tools > HLK > Open Toolset).
Platform Services

Cloud Save

Synchronizes the local save blob with a per-player cloud slot, with an explicit, configurable conflict policy instead of silent last-writer-wins.

using HLK.Core; using HLK.CloudSave; var cloud = Services.Resolve<CloudSaveService>(); await cloud.InitializeAsync(); await cloud.SignInAsync(); CloudSaveResult sync = await cloud.SyncAsync(); // two-way, policy-driven if (sync.Status == CloudSaveStatus.ConflictResolved) Debug.Log($"Adopted {sync.Direction}"); await cloud.UploadAsync(); // explicit push await cloud.DownloadAndAdoptAsync(); // explicit pull cloud.RemoteAdopted += payload => ReloadUiFromSave();

Conflict policies

ConflictPolicyWinner when both sides changed
PlayerWinsThe payload with the better player progress.
SchemaWinsThe payload with the newer save schema / timestamp.
ForceLocal / ForceRemoteAlways this device / always the cloud copy.

CloudSaveAutoSync can sync on app pause (default on) and on currency changes (default off). Results carry a CloudSaveStatusOk, ConflictResolved, ConflictUnresolved, NotSignedIn, NetworkError, QuotaExceeded — plus which SyncDirection actually happened.

Providers

CloudSaveProviderBackendDefine
AutoPlatform pick
LocalFallbackLocalFileCloudBackend — a file-based stand-in for editor testing
ICloudiCloud key-value storageHLK_ICLOUD
PlayGamesPlayGamesCloudBackend (Saved Games)HLK_PLAY_GAMES_SAVE
Settings pointer: provider, policy, and auto-sync triggers on the Cloud Save tab (Tools > HLK > Open Toolset).
Platform Services

Push Messaging

Remote push notifications with token lifecycle, topic subscriptions, and an in-editor simulator so you can build the receiving side without a backend.

using HLK.Core; using HLK.Push; var push = Services.Resolve<IPushService>(); await push.InitializeAsync(); await push.RequestPermissionAsync(); await push.SubscribeAsync("news"); push.TokenChanged += token => SendTokenToBackend(token); push.MessageReceived += msg => HandlePush(msg.Title, msg.Data); // Editor testing without Firebase: push.SimulateIncoming("Sale!", "The starter pack is 50% off", "news");
MemberPurpose
InitializeAsync()Start the backend and obtain a device token.
SubscribeAsync / UnsubscribeAsyncTopic management; settings can auto-subscribe a topic list after registration.
Token / HasToken / ClearTokenAsync()Token access and invalidation.
MessageReceivedRaised on the main thread for foreground/opened messages.
SimulateIncoming(...)Inject a fake PushMessage in Dummy mode.

PushMode: Disabled, Dummy, or Firebase — the FCM backend compiles behind HLK_FIREBASE_MESSAGING and requires the Firebase Messaging package plus your platform config files.

Settings pointer: mode, permission timing, and auto-subscribe topics on the Push tab (Tools > HLK > Open Toolset).
Platform Services

Social Sharing

Native share sheets for text, links, images, screenshots, email, and SMS, wrapped in the static Share facade. Every call resolves to a ShareResult that distinguishes success, cancellation, and failure with a reason.

using HLK.Sharing; var r = await Share.TextAsync("I reached level 40!", url: "https://example.com"); if (r.WasCancelled) return; // Capture + share in one call (superSize upscales the capture): await Share.ScreenshotAsync("Beat my score!"); if (Share.CanSendMail()) await Share.EmailAsync(new EmailRequest { /* to, subject, body… */ });
MemberPurpose
DefaultAsync()Share the configured default URL + tagline.
TextAsync / ImageAsync / ScreenshotAsyncPayload variants; screenshots capture via ScreenCaptureHelper before opening the sheet.
EmailAsync / MessageAsyncChannel-specific composers, with CanSendMail() / CanSendMessage() preflight.

SharingMode: Disabled, Dummy (logs the request), or Native behind HLK_NATIVE_SHARE. Share attempts and outcomes can be mirrored onto the analytics bus.

Settings pointer: mode and default share URL/tagline on the Sharing tab (Tools > HLK > Open Toolset).
Platform Services

In-App Web View

Open web content — privacy policy, support portal, seasonal event page — inside the app instead of bouncing players to an external browser. Supports raw HTML as well as URLs, with a JS message bridge back into the game.

using HLK.Core; using HLK.WebView; var web = Services.Resolve<WebViewService>(); await web.OpenUrlAsync("https://example.com/events", WebViewLayout.Fullscreen); await web.OpenPrivacyPolicyAsync(); // uses the configured URL var req = WebViewRequest.ForHtml("<h1>Patch notes</h1>") .WithTitle("What's new") .WithJavaScript(true); await web.OpenAsync(req); web.MessageReceived += m => HandleJsMessage(m); await web.CloseAsync();
MemberPurpose
OpenUrlAsync / OpenHtmlAsync / OpenAsync(request)Show content with a chosen WebViewLayout; requests can restrict navigable schemes.
OpenPrivacyPolicyAsync / OpenTermsAsyncOne-liners for the two URLs every store review asks about.
SendMessageAsync(msg)Game → page messaging; MessageReceived is the reverse channel.
PageStarted / PageFinished / ClosedNavigation lifecycle events.

WebViewMode: Dummy, Disabled, or Native. Dummy is a system-browser fallback with an emulated lifecycle — URLs (and HTML, written to a temp file://) are handed to the OS browser via Application.OpenURL and the "view" reports synthetic page-started/finished/closed events; it is not an in-engine panel.

Native binds to the UniWebView plugin. To use it: import the UniWebView asset into the project, enable the HLK_NATIVE_WEBVIEW scripting define, and ensure HLK.Runtime.asmdef references the UniWebView assembly (this reference already ships in the kit's asmdef).

Enabling HLK_NATIVE_WEBVIEW without the UniWebView plugin present breaks compilation — the native backend references UniWebView types directly. Add the plugin and the define together, or neither.
Settings pointer: mode and policy/terms URLs on the Web View tab (Tools > HLK > Open Toolset).
Platform Services

Remote Config

Server-tunable values with typed accessors and offline-safe defaults. Seed key/value defaults in settings; they answer immediately and act as the fallback whenever a fetch fails or a key is missing.

using HLK.Core; using HLK.RemoteConfig; var rc = Services.Resolve<IRemoteConfig>(); await rc.InitializeAsync(); await rc.FetchAndActivateAsync(); int maxLives = rc.GetInt("max_lives", 5); bool hardMode = rc.GetBool("hard_mode", false); var tuning = rc.GetJson<DifficultyTuning>("difficulty", fallback: null); rc.ConfigActivated += () => ApplyLiveTuning();
MemberPurpose
FetchAsync(force) / ActivateAsync() / FetchAndActivateAsync()Two-phase fetch-then-apply, or the combined call. Fetches respect CacheSeconds (default 3600) unless forced.
GetString/Bool/Int/Long/Double/FloatTyped reads with per-call fallbacks; values coerce sensibly across types.
GetJson<T>(key, fallback)Parse a JSON payload stored under one key straight into your own type.
State / LastFetchTimeUtc / KeysLifecycle (DefaultsReadyFetchingActivated / FetchFailed) and diagnostics.

Providers

RemoteConfigProviderBackendDefine
DummyDefaults only — fully offline
FirebaseFirebase Remote ConfigHLK_FIREBASE_REMOTE_CONFIG
UnityGamingServicesUGS Remote ConfigHLK_UGS_REMOTE_CONFIG

The backend factory preflights each provider and refuses to promote past Dummy until its prerequisites are met:

Settings pointer: provider, cache window, and seed defaults on the Remote Config tab (Tools > HLK > Open Toolset).
Platform Services

App Updates

Detect newer builds and walk players through updating — via Google's in-app update flows on Android, a store version check on iOS, and a simulated backend for QA everywhere.

using HLK.Core; using HLK.AppUpdate; var updates = Services.Resolve<IAppUpdateService>(); var check = await updates.CheckForUpdateAsync(); if (check.Success && check.Value.Availability == UpdateAvailability.UpdateAvailable) { if (updates.SupportsInAppFlow) await updates.StartFlexibleUpdateAsync(); // download in background else await updates.OpenStorePageAsync(); // iOS / fallback } // Flexible flow, once downloaded: await updates.CompleteFlexibleUpdateAsync();
MemberPurpose
CheckForUpdateAsync()Resolve an UpdateInfo (availability, version, staleness); CheckCompleted mirrors it as an event.
StartFlexibleUpdateAsync() / StartImmediateUpdateAsync()Background download vs. blocking full-screen flow.
CompleteFlexibleUpdateAsync()Apply/finalize a downloaded flexible update (Play completeUpdate).
OpenStorePageAsync()Store-listing fallback via StoreDeepLink.
SimulateAvailability(state)Force a state in the simulated backend for QA.

AppUpdateMode: Disabled, Simulated (settings choose the forced availability and version), AppStoreVersionCheck (queries Apple's public iTunes Lookup endpoint and deep-links to the store listing — no SDK needed), or PlayInApp behind HLK_PLAY_APP_UPDATE. UpdateAvailability.CriticalUpdate supports force-update gates; VersionComparer handles semantic version ordering.

iOS store id. AppStoreVersionCheck reads the numeric App Store id from the Rate Us settings section (RateUsSettings.IOSAppId) — the same id used for the rate-prompt store link. Without it the iTunes Lookup falls back to a bundle-id query and the store deep-link cannot be built, so fill in IOSAppId on the Rate Prompt tab before shipping iOS update checks.
Settings pointer: mode, simulated values, and boot prompting on the App Update tab (Tools > HLK > Open Toolset).
Platform Services

Attribution

Install-source measurement (MMP) integration: where did this player come from, and which campaigns produce payers. Ships with an AppsFlyer backend and a fully offline in-memory stand-in.

using HLK.Core; using HLK.Attribution; var attr = Services.Resolve<IAttributionService>(); attr.AttributionResolved += data => Debug.Log($"Source: {data.MediaSource} / {data.Campaign}"); attr.SendEvent("tutorial_complete"); attr.SendRevenue(4.99, "USD"); // purchase signal attr.SetCustomerUserId(playerId); attr.SetTrackingConsent(consentGranted);
MemberPurpose
AttributionResolved / LastAttributionConversion data once the MMP resolves the install source.
SendEvent / SendRevenueIn-app and monetary events for campaign optimization.
SetTrackingConsent(granted)Gate data collection on the consent module's outcome.
InstallIdStable per-install identifier.

AttributionMode: Disabled, Dummy (in-memory), or Production — AppsFlyer behind HLK_APPSFLYER with your dev key. On iOS the SDK start can wait up to AttPromptTimeoutSeconds (default 60) for the App Tracking Transparency prompt; conversion data can be forwarded onto the analytics bus.

Settings pointer: mode, dev key, and ATT timing on the Attribution tab (Tools > HLK > Open Toolset).
Platform Services

Crash Reporting

Uncaught-exception capture, breadcrumbs, and custom keys behind a swappable backend. The static facade on CrashReporterService keeps call sites one line long.

using HLK.CrashReporter; CrashReporterService.Log("Entered shop"); // breadcrumb CrashReporterService.Key("ab_bucket", "wheel_v2"); // custom key CrashReporterService.User(playerId); try { RiskyThing(); } catch (Exception ex) { CrashReporterService.Report(ex); } CrashReporterService.Report("Soft failure", CrashSeverity.Warning);
MemberPurpose
Initialize() / Shutdown()Hook / unhook the log and exception listeners (bootstrap does this for you).
LogBreadcrumb / SetCustomKey / SetUserIdContext attached to subsequent reports.
LogException(ex, context)Non-fatal exception with optional key/values.
SetCollectionEnabled(bool)Runtime kill-switch (tie it to consent).
TriggerTestCrash()Verify the pipeline end-to-end on device.

CrashReporterMode: Disabled (the fresh-import default — nothing is captured), LogOnly (console echo; select this to see reports without an SDK), or Production via Firebase Crashlytics behind HLK_CRASHLYTICS. Settings choose whether uncaught exceptions and error-level logs are reported, and can stamp a build label on every report.

Settings pointer: the Crash Reporter tab (Tools > HLK > Open Toolset).
Platform Services

Native Dialogs

OS-styled alerts, confirmations, action sheets, text prompts, and short toasts through one awaitable API — with a uGUI-rendered fallback so the same calls work in the editor and on unsupported platforms.

using HLK.NativeUI; await NativeDialogs.ShowAlertAsync("Update ready", "Restart to apply."); bool quit = await NativeDialogs.ConfirmAsync("Quit?", "Progress is saved."); var pick = await NativeDialogs.ShowActionSheetAsync( "Choose avatar", new[] { "Knight", "Rogue", "Wizard" }); string name = await NativeDialogs.PromptTextAsync("Name your pet", "Be nice."); NativeDialogs.ShowToast("Saved!");
MemberPurpose
ShowAlertAsyncSingle-button notice; returns which button closed it.
ConfirmAsyncYes/no question resolving to a bool.
ShowActionSheetAsyncOption list; resolves to the chosen index.
ShowPromptAsync / PromptTextAsyncText-entry dialog.
ShowToast(message, length)Short OS toast (Android native; rendered in-engine elsewhere).

NativeUIMode: InEngine (the built-in renderer) or Native behind HLK_NATIVE_UI — with UseInEngineWhenUnsupported (default on) falling back automatically per dialog type.

Settings pointer: the Native UI tab (Tools > HLK > Open Toolset).
Platform Services

Runtime Permissions

Cross-platform permission checks and requests for camera, microphone, photo library (read and add), and when-in-use location, normalized into one PermissionState model.

using HLK.Permissions; // One-liner: request only if needed, true when usable. if (await Permissions.EnsureAsync(HLKPermission.Camera)) StartArMode(); PermissionState state = Permissions.Check(HLKPermission.Photos); if (state == PermissionState.DeniedPermanently) Permissions.OpenAppSettings(); // the only remaining path
MemberPurpose
Check(permission)Non-prompting status: Undetermined, Granted, Denied, DeniedPermanently, NotApplicable.
RequestAsync(permission)Show the OS prompt when allowed; resolves to the new state.
EnsureAsync(permission)Check-then-request convenience returning bool.
OpenAppSettings()Deep-link to the app's OS settings page.

PermissionsMode: Auto, Simulated (always auto-grants — no OS prompt), or Native behind HLK_NATIVE_PERMISSIONS. Build post-processors inject the iOS usage-description strings you enter in settings into Info.plist, and align the Android manifest — no manual plist editing.

Settings pointer: mode and iOS usage strings on the Permissions tab (Tools > HLK > Open Toolset).
Platform Services

Analytics Pipeline

A fan-out event bus: game code emits once, every attached IAnalyticsSink receives the event. One misbehaving sink can't take the pipeline down — sink exceptions are contained per dispatch.

using HLK.Analytics; AnalyticsApi.Emit("level_complete"); AnalyticsApi.Emit("level_complete", ("level", 12), ("stars", 3)); AnalyticsApi.Emit("iap_shown", new Dictionary<string, object> { { "placement", "out_of_energy" } }); // Custom destination: AnalyticsApi.Bus.AddSink(new MyBackendSink());

Sinks

SinkDestinationRequires
DebugLogSinkUnity console (toggleable)
UnityAnalyticsSinkUnity AnalyticsHLK_UNITY_ANALYTICS
FirebaseAnalyticsSinkFirebase AnalyticsHLK_FIREBASE
ForwardingSinkAdapter for hooking arbitrary delegates

AnalyticsMode gates the fan-out: Disabled, ConsoleOnly (default), or Live (console plus every enabled provider). Other modules publish onto the same bus when their "bind to analytics" switches are on — quests, offers, IAP funnel steps, ad outcomes, sharing results, and game-services submissions emit automatically. The battle pass emits its outbound telemetry the same way, but its inbound XP relies on a bus→IngestEvent forwarder (shipped at bootstrap) so gameplay events on the bus also drive the pass — see the Battle Pass section.

Going live (UGS / Firebase)

The stand-in sinks need no setup, but the real providers do:

Then switch the mode to Live and enable the matching sink. The Analytics tab checks each of these prerequisites and reports the effective backend.

Settings pointer: mode and sink toggles on the Analytics tab (Tools > HLK > Open Toolset).
UI & Feedback

Toast Messages

Transient, non-blocking notices shown strictly one at a time in arrival order. Each toast carries a kind for coloring and its own duration; the queue drains on unscaled time, so toasts keep flowing while the game is paused.

using HLK.Core; using HLK.UI; var toasts = Services.Resolve<IToastService>(); toasts.Show("Progress saved"); // Info, default length toasts.Show("Purchase complete!", ToastKind.Success); toasts.Show("No connection", ToastKind.Error, duration: 4f);
MemberPurpose
Show(message, kind, duration)Enqueue; a duration ≤ 0 falls back to DefaultDuration.
DefaultDurationSeconds a toast stays visible when unspecified — 2.2 by default, settable at runtime.
QueuedCount / IsShowing / ShownCountQueue introspection.
BindView(view) / ClearQueue()Attach the on-screen renderer / drop pending messages.

Kinds: Info, Success, Warning, Error. With no view bound (early boot, headless tests) messages are logged instead and the queue still advances — a later-bound view is never flooded with a backlog. Drop a ToastView prefab in the scene and ToastBinder connects it to the service.

UI & Feedback

UI Widgets

Small, dependency-free components that cover the recurring polish work of mobile UI:

ComponentWhat it does
SafeAreaFitterAnchors a RectTransform to Screen.safeArea and re-fits on rotation and resolution changes — notch and cutout safety with zero code.
NumberTickerRolls a TextMeshPro number from its current value to a target with easing, allocation-free (no per-frame string garbage).
PressFeedbackSqueezes a transform while the pointer is held and springs back on release; runs on unscaled time so buttons stay lively in pause menus.
RewardClaimButtonClaim flow in a box: designer callback, dismiss animation (Animator trigger or CanvasGroup fade fallback), delayed removal.
FadeTransitionTriggerInspector-wired scene change through IFlowService; logs and no-ops when the flow service is absent instead of throwing.
Tooling & Debug

Toolset Window

The kit's one configuration surface. Open it via Tools > HLK > Open Toolset (or Tools > HLK > Guided Setup — same window; the toolset is the setup flow). It binds directly to the HLKSettings asset and remembers your last tab.

Tab layout

Tabs are arranged in two rows — the core row first, the Pro row after:

RowTabs
CoreOverview · Ads · Analytics · Audio · Haptics · Purchases · Mini-Games · Notifications · Rate Prompt · Starter Gifts · General
ProAds Pro · App Update · Attribution · Battle Pass · Cloud Save · Consent · Crash Reporter · Deep Links · Energy · Game Services · Purchases Pro · Localization · Native UI · Offers · Permissions · Push · Quests · Remote Config · Sharing · Web View

What the tabs give you beyond fields

If the settings asset is missing, the window offers to create it at Assets/HyperCasualLiveOpsKit/Resources/HLK/HLKSettings.asset — the only path the runtime loads from.
Tooling & Debug

Cheat Console

An on-screen QA console for the actions testers repeat all day. Its compile-time gate keeps releases clean: with HLK_CHEATS defined you get the live CheatService plus the console overlay; without it, an inert NoOpCheatService is registered instead, so any code resolving ICheatService still works — it just refuses politely.

using HLK.Core; using HLK.Cheats; var cheats = Services.Resolve<ICheatService>(); if (cheats.Enabled) { cheats.GrantCurrency("Coins", 5000); cheats.ResetDailyStreak(); cheats.Execute(CheatActionId.WipeAllData); }

Opening the console

Both gestures, keyboard shortcuts, grant amounts (defaults: 1000 coins / 100 gems), and currency ids are configured in a CheatBindings asset loaded from Resources/HLK/CheatBindings.

Built-in actions

CheatActionId: GrantCoins, GrantGems, ResetDailyStreak, ClearShopOwnership, WipeAllData — plus ReloadActiveScene() on the service. When compiled in, a warning is logged every launch as a reminder to strip the define from release builds.

Tooling & Debug

Scripting Defines

Vendor SDK code never compiles until you opt in. Each integration sits behind an HLK_* define; install the vendor SDK, make sure the define is set, and the real backend replaces the stand-in. How the define gets set depends on how the SDK is distributed:

DefineEnablesModule
HLK_ADMOBGoogle Mobile Ads backendAds
HLK_LEVELPLAYUnity LevelPlay (ironSource) mediationAds
HLK_APPLOVIN_MAXAppLovin MAX mediationAds
HLK_UNITY_ADSUnity Ads directAds
HLK_UNITY_PURCHASINGUnity IAP store backendIAP
HLK_UNITY_ANALYTICSUnity Analytics sinkAnalytics
HLK_FIREBASEFirebase Analytics sinkAnalytics
HLK_FIREBASE_MESSAGINGFirebase Cloud MessagingPush
HLK_FIREBASE_REMOTE_CONFIGFirebase Remote Config backendRemote Config
HLK_UGS_REMOTE_CONFIGUnity Gaming Services backendRemote Config
HLK_CRASHLYTICSFirebase Crashlytics backendCrash Reporting
HLK_UMPGoogle User Messaging PlatformConsent
HLK_ATTiOS App Tracking Transparency promptConsent
HLK_APPSFLYERAppsFlyer SDKAttribution
HLK_PLAY_GAMES_SERVICESGoogle Play Games sign-in / leaderboardsGame Services
HLK_GAME_CENTERApple Game CenterGame Services
HLK_PLAY_GAMES_SAVEPlay Games Saved GamesCloud Save
HLK_ICLOUDiCloud key-value storageCloud Save
HLK_PLAY_APP_UPDATEGoogle Play in-app update flowsApp Updates
HLK_MOBILE_NOTIFICATIONSUnity Mobile Notifications backendNotifications
HLK_NATIVE_SHARENative share sheet pluginSharing
HLK_NATIVE_WEBVIEWNative web-view pluginWeb View
HLK_NATIVE_UINative dialog backendsNative Dialogs
HLK_NATIVE_PERMISSIONSNative permission requestsPermissions
HLK_CHEATSCompiles the cheat console inCheats
HLK_LOG_VERBOSEVerbose diagnostic logging at compile timeCore
Rule of thumb: a define without its SDK produces compile errors; an SDK without its define is silently ignored. Add both, or neither. Verbose logging can also be flipped at runtime via General.VerboseLogging without any define.
Tooling & Debug

Troubleshooting

Issues are grouped by module. Diagnostic logging helps with nearly all of them: enable VerboseLogging on the General tab and watch for [HLK]-tagged lines.

Core & boot

SymptomCauseFix
Resolve<T>() throws InvalidOperationException You resolved before the module's bootstrap ran, or from a script executing before assemblies-loaded hooks. Resolve in Start() rather than field initializers, or use Services.TryResolve and retry.
Settings edits ignored at runtime The asset isn't at the required Resources path, so a transient default instance is being served. Keep the asset at Resources/HLK/HLKSettings.asset; the Toolset window creates it correctly.
Exceptions from SDK callbacks touching Unity objects The vendor invoked your handler on a worker thread. Wrap the body in MonoHost.Post(() => ...).

Save & cloud

SymptomCauseFix
Progress lost when the app is killed quickly RequestSave() debounces (~1 s) and the flush never ran. Call FlushNow() at critical moments and AttachLifecycle() once at boot.
SyncAsync reports NotSignedIn The cloud backend needs platform sign-in that hasn't happened. Await SignInAsync() first; in the editor use the LocalFallback provider.
Cloud adopts the "wrong" copy on conflict The active ConflictPolicy doesn't match your expectation. Pick the policy explicitly on the Cloud Save tab, or pass one to SyncAsync(policy).

Monetization

SymptomCauseFix
Rewarded call resolves Suppressed A pacing rule blocked it (min interval, session/day cap, quiet levels) or the ad-free entitlement is active. Read the Reason string; review Ads Pro caps and the placement's overrides.
Only mock ads ever appear on device AdMode is still Dummy, or the mediation SDK/define pair is missing. Set Test/Production on the Ads tab, install the SDK, add its define; the tab's probe shows the effective backend.
PurchaseAsync fails with StoreNotReady Initialization didn't finish (or failed) before purchasing. Await InitializeAsync() and check its bool; verify the catalog has your product id.
Restore brings back nothing Consumables are not restorable by design. Model permanent items as NonConsumable; verify with a test account that owns them.

LiveOps

SymptomCauseFix
Daily streak resets after one missed day GraceDays defaults to 0 — strictly consecutive claims. Raise GraceDays on the Starter Gifts tab; consider setting a TimeProbeUrl against clock cheats.
Wheel's TrySelect returns false No database assigned, or every slice has weight ≤ 0. Assign WheelDatabase under Mini-Games and give at least one slice a positive weight.
Quests never progress Event names don't match the definitions, or the window rolled over. Compare the emitted name with QuestDefinition.EventName; remember Daily/Weekly windows reset UTC.
Battle pass XP stays at zero The event isn't in EventNamesCsv (and AcceptAllEvents is off), or no bus→IngestEvent forwarder is feeding the pass. Add the event name to the season config; make sure the bus→IngestEvent forwarder is attached (it ships at bootstrap) so bus events reach the pass, or grant directly with GrantXp.
Energy doesn't refill overnight Reconciliation never ran after the app resumed. Keep RecalcRegenOnFocus enabled or call Reconcile() when your home screen opens.
Review prompt never shows Throttle gates: 5 sessions first, 14-day spacing, lifetime cap 3, and "stop after rated". That's working as intended. For QA, use ForceRequestReviewAsync() or ResetGates().

Platform services

SymptomCauseFix
Notifications never fire in the editor The editor runs the log-only Dummy backend. Test on device with Production mode, the Mobile Notifications package, and its define.
Push token stays null Firebase configuration files are missing or the messaging define is absent. Ship the Firebase platform files (google-services.json on Android, GoogleService-Info.plist on iOS) plus the messaging define; in Dummy mode use SimulateIncoming.
Deep link on cold start seems dropped The link arrived before any subscriber existed. It's buffered — call DrainPending() right after subscribing to Received.
Remote config values never change The cache window hasn't elapsed, so fetches short-circuit. Lower CacheSeconds during development or call FetchAsync(force: true).
Localized text shows raw keys No table set assigned, or the key is missing from the active locale. Assign a TableSet on the Localization tab and add the key; the fallback table covers gaps.

UI, audio & debug

SymptomCauseFix
Toasts log to console instead of showing No IToastView is bound in the scene. Add a ToastView prefab with a ToastBinder; queued behavior is otherwise identical.
Play(id) produces silence The id doesn't exist in the bank, or the bank isn't assigned; category or master may also be muted. Check the entry name on the Audio tab, the DefaultSoundBank slot, and the mixer toggles.
No vibration on Android in Production mode The VIBRATE permission was never added to the manifest. Use the Haptics tab's manifest button; verify device settings allow vibration.
Cheat console won't open in a build HLK_CHEATS wasn't defined for that build target. Add the define for QA builds only; on device use the 3-finger 1-second hold.
Toon materials render magenta The material uses a variant from a different render pipeline. Switch to the family matching your pipeline (BiRP/URP/HDRP); RenderPipelineUtil tells you which is active.
Core Infrastructure

Currency Icon Setup

CurrencyIcons.Get(CurrencyId) first uses the icon assigned to the active CurrencyCatalog. If no catalog icon is assigned, it falls back to Resources.Load<Sprite>("HLK/CurrencyIcons/<CurrencyId>").

For drop-in icons, create Resources/HLK/CurrencyIcons/ and add files such as Coins.png and Gems.png. Each filename must match its CurrencyId enum member exactly. Import the image as Sprite (2D and UI), Single mode, with mipmaps and Read/Write disabled.

Lookups, including misses, are cached. Call CurrencyIcons.InvalidateAll() after changing icon art or CurrencyIcons.Invalidate(id) to refresh one currency. A fresh play-mode session starts with an empty cache.

Core Infrastructure

Portable Power-Ups

The PowerUps module keeps definitions, catalogs, inventory state, effect handlers, the runtime bar, buttons, tutorial popup, setup tools, shared artwork, and the sample Rainbow Release preset together without embedding game-specific behavior in the definitions.

Add a power-up

  1. Open Tools > HLK > Power-Ups > Setup & Catalog.
  2. Choose a permanent ID and display name, then create the definition.
  3. Assign its center icon and tutorial illustration. The shared slot frame can normally stay unchanged.
  4. Implement IPowerUpEffectHandler in the host game and register it while the gameplay scene is active.
  5. Run Tools > HLK > Power-Ups > Validate.
public sealed class HammerHandler : IPowerUpEffectHandler { public string PowerUpId => "hammer"; public PowerUpUseResult TryApply(UnityEngine.Object target) { if (target is not BreakableBlock block) return PowerUpUseResult.Rejected("Pick a breakable block."); block.Break(); return PowerUpUseResult.Used("Block smashed!"); } } PowerUpEffectRegistry.Register(handler); if (PowerUpBar.Instance != null && PowerUpBar.Instance.TryUseArmed(tappedTarget)) return;

The default PlayerPrefsPowerUpStateStore supports offline inventory. Install an account or cloud implementation of IPowerUpStateStore through PowerUpServices.SetStateStore(...). Catalog ordering, enable flags, duplicate-ID validation, and an optional display limit control how definitions appear; a zero display limit shows all enabled definitions in the horizontal scroller.

Tooling & Debug

Release Notes

2.0.0

Upgrade note. Version 2.0.0 intentionally changes serialized settings names, preset paths, and the default ad-free entitlement key to hce.entitlement.ad_free. Existing HLK_* scripting defines remain unchanged.
Tooling & Debug

Licenses & Notices

Robert Penner's Easing Equations

The standard easing constants and curve shapes used by Runtime/Core/Easing.cs, including the back-overshoot, elastic-period, and bounce-segment parameterizations, are derived from Robert Penner's easing equations and are used under the BSD License.

Copyright (c) 2001 Robert Penner All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 3. Neither the name of the author nor the names of contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE, ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
Tooling & Debug

Support

Stuck on something this manual doesn't cover? Reach out — real answers from the developer, usually within two business days.

To get the fastest turnaround, include:

Bug reports that arrive with a minimal reproduction scene get priority. Feature requests are welcome too — a lot of this kit exists because someone asked.