{"id":"aslyumm/mirage","name":"mirage","scope":"aslyumm","platform":"roblox","description":"Client-side prediction & reconciliation for streamed Roblox state.","version":"0.1.0","latest":"0.1.0","versions":["0.1.0"],"license":"MIT","licenseRating":"safe","licenseCaveats":[],"licenseVerified":true,"dependencies":{},"integrity":"36a3a1f59bc150cd7e8458f0498b1d4404ee6eb2ef26e8ebcf353d819ff36bd9","likes":0,"downloads":0,"install":"forest install aslyumm/mirage","url":"https://forest.dev/p/roblox/aslyumm/mirage","files":"https://api.forest.dev/ai/package/roblox/aslyumm/mirage/files","readme":"# Mirage\n\nClient-side prediction & reconciliation for streamed Roblox state, packaged for [Wally](https://wally.run/).\n\nMirage lets a server stream opt-in pieces of state to clients at a configurable rate. Each client keeps its own read/write **Predicted** copy that renders a player's actions *immediately*, then reconciles against the server's **Confirmed** truth as acknowledgements arrive. The server stays fully authoritative — prediction only changes *when* a player sees the result of their own action, never *who* decides it.\n\nThe classic case: at 200 ms ping a coin pickup feels sluggish, because the client waits a full round trip before showing the coin collected. With Mirage the coin disappears the instant it's touched; if the server later rejects the action, Mirage hands you a rollback hook so you can re-show it. You choose which state opts in, so you can mix Mirage with ordinary Roblox replication however you like.\n\n## Installation\n\nAdd Mirage to your `wally.toml`:\n\n```toml\n[dependencies]\nMirage = \"aslyumm/mirage@0.1.0\"\n```\n\nThen run:\n\n```sh\nwally install\n```\n\nMirage has **zero runtime dependencies** — installing it pulls in nothing else.\n\n## Quick start\n\nRegister the same key on both sides, mutate it optimistically on the client, and let the server validate.\n\n```lua\n-- Server\nlocal Mirage = require(ReplicatedStorage.Packages.Mirage)\n\nlocal Coins = Mirage.Server.RegisterKey({\n    name = \"Coins\",\n    rate = 20,                 -- broadcast Hz\n    initialState = coinGrid,   -- your own data, owned by the server\n    blendMode = \"Discrete\",\n    applyAction = CoinActions.apply,\n    validate = function(player, action, serverState)\n        -- return { accepted = true } or { accepted = false, reason = \"...\" }\n        return { accepted = true }\n    end,\n})\n\n-- Mirage emits signals and never kicks; the anti-cheat policy is yours.\nCoins.ValidationFailed:Connect(function(player, action, reason)\n    -- e.g. strike the player, and kick past a threshold\nend)\n```\n\n```lua\n-- Client\nlocal Mirage = require(ReplicatedStorage.Packages.Mirage)\n\nlocal Coins = Mirage.Client.RegisterKey({\n    name = \"Coins\",\n    blendMode = \"Discrete\",\n    applyAction = CoinActions.apply, -- MUST be the same function as the server's\n})\n\n-- Touched a coin? Show it collected right now, before the server answers:\nCoins.Predicted:Do({ kind = \"Collect\", payload = { coinId = 5 } })\n\n-- Draw whatever the player should see (Mirage owns no renderer):\nCoins.Render:OnBlend(function(coinId, blendedState)\n    -- spawn VFX/SFX, update UI counters, tween the coin out, ...\nend)\n\n-- Server rejected an action? Roll that specific visual back:\nCoins.Predicted:OnMispredict(function(actionId, reason, payload)\n    -- re-show the coin, play an \"undo\" cue\nend)\n```\n\n## How it works\n\n| Layer | Written by | Role |\n| --- | --- | --- |\n| **Server Truth** | the server | the canonical value for a key |\n| **Confirmed** | incoming broadcasts only | the client's read-only mirror of Server Truth (always ~1 RTT stale) |\n| **Predicted** | the player's own actions | the client's read/write copy, rendered ahead of confirmation |\n| **Render** | Mirage, via `Render:OnBlend` | the blended value your VFX/SFX/UI actually draws |\n\nEvery optimistic action gets a per-key sequence id. The client keeps a small pending queue and re-derives Predicted as `Confirmed + pending actions`. When a broadcast acks up to an id, accepted actions drop out of the queue silently and rejected ones fire `OnMispredict`. If an action goes unanswered past a ping-sized grace window, Mirage resyncs that entry from Confirmed rather than trusting a stale guess.\n\n### Blend modes\n\n- **`\"Discrete\"`** — trust Predicted immediately, correct only on rejection. For pickups and other discrete events.\n- **`\"Continuous\"`** — always ease Render toward Confirmed over a few frames. For meters, progress bars, and positions, so small corrections don't visibly pop.\n\n### Key scopes\n\n- **`\"Global\"`** (default) — one shared state table streamed to every client (a coin grid, a world boss's health).\n- **`\"PerPlayer\"`** — an independent state table per player, streamed only to its owner (inventory, quest progress, personal currency). Other clients never receive another player's copy.\n\n## Documentation\n\nFull API reference and guides live on the [Moonwave docs site](https://aslyumm.github.io/Mirage/). Start with the intro, then the **Server**, **Client**, **PredictedStore**, **ConfirmedStore**, and **Blend** pages.\n\n## Contributing\n\nMirage is built with [Rojo](https://rojo.space/) and its tests run against a real Roblox engine. The local development toolchain (Rojo, Wally, Selene, StyLua, run-in-roblox) is pinned in `aftman.toml` and installed with `aftman install` — these are tools for working *on* Mirage, not dependencies of the package. Source lives under `src/` (`Server/`, `Client/`, `Shared/`), with specs in `tests/` — run them with `./scripts/test.ps1` (requires Roblox Studio). Lint and formatting (`selene`, `stylua --check`) run in CI on every push.\n\n## License\n\nMIT © aslyumm. See [LICENSE](https://github.com/aslyumm/Mirage/blob/master/LICENSE).\n","readmeTruncated":false}