{"id":"demi-dog/duplecs","name":"duplecs","scope":"demi-dog","platform":"roblox","description":"Generalized per-world replication for jecs","version":"1.0.0","latest":"1.0.0","versions":["1.0.0"],"license":"MIT","licenseRating":"safe","licenseCaveats":[],"licenseVerified":true,"dependencies":{"ukendio/jecs":{"version":"^0.11.0","alias":"jecs"}},"integrity":"657713d4cec01b190315fba790d9a129634294658a241abfd44e202558bf61b3","likes":0,"downloads":0,"install":"forest install demi-dog/duplecs","url":"https://forest.dev/p/roblox/demi-dog/duplecs","files":"https://api.forest.dev/ai/package/roblox/demi-dog/duplecs/files","readme":"# duplecs\n\nGeneralized per-world replication for [jecs](https://github.com/Ukendio/jecs).\n\nduplecs listens for changes in the server's jecs world, decides *what* should replicate to *whom*, packs data into one filtered packet per client (changes for existing clients, everything for newly-joined ones), and reconciles that data back into each client's world.\n\nduplecs does not own transport — sending packets to clients is the caller's responsibility, typically using a `RemoteEvent`.\n\n## Highlights\n\n- **You decide what replicates, and to whom** — a component reaches a client only when its definition is marked `Networked`, the entity containing it is marked `Replicated`, and neither the component nor the containing entity is blocked by a `Private` filter.\n- **Visibility filters** — assign a `Private` filter either per-entity or per-component: an empty one to keep state server-only, or a client-keyed one for whitelists and blacklists. A client world always holds exactly what that client is currently allowed to see.\n- **Visibility inheritance** — multiple entities can inherit one entity's filter, so their visibility can be edited from a single place: a vehicle's wheels following its hull, a structure's furniture and props following its interior, or a unit inheriting both its team's fog-of-war filter and its region's proximity filter — visible only to clients that pass both.\n- **Relationship support** — network relations like `jecs.ChildOf` by tagging the relation itself; a pair is never sent to a client who cannot see its target. `Private` works here too: per-component filters also prune pairs using that component as a relation.\n- **Nothing sent twice** — a change ships the frame it happens and is never re-sent on the reliable path; a client joining later gets one packet holding everything currently visible to them. `NetworkedOnce` definitions go further: values ship only when the component is added, for places the client can predict changes on its own.\n- **Mapped entity ids** — the server and client often have different ids for the same entity, so duplecs maps them both ways on the client: replicated entities map themselves, shared components map by name so definition order can differ between sides with no manual wiring, and duplecs provides methods for reading or assigning these mappings yourself.\n- **Serdes hooks** — give a component's values a byte encoding and they pack into the packet's buffer, for a fraction of the bandwidth; components without hooks ride a plain side array, so day one needs no byte encodings.\n- **Unreliable channel** — every-frame changes (e.g. positions, velocities) can be routed through self-contained chunks sized for unreliable transports, for when you need the lowest latency.\n- **Client-side control** — reconciliation overrides put your code between the wire and the world: id translation, prediction gating, and interpolation buffers.\n\n## At a glance\n\n```lua\n-- shared/components.luau -- one definitions module, shared by both sides\nlocal jecs = require(\"./roblox_packages/jecs\")\nlocal duplecs = require(\"./roblox_packages/duplecs\")\n\nreturn function(world)\n\tlocal net = duplecs.shared(world) -- the duplecs component set; repeated calls return the same set\n\n\tlocal Health = world:component() :: jecs.Entity<number>\n\tworld:set(Health, jecs.Name, \"Health\") -- component ids map by name between server and client\n\tworld:add(Health, net.Networked) -- marks that instances of this component are allowed to replicate\n\n\treturn { Health = Health }\nend\n```\n\n```lua\n-- server\nlocal jecs = require(\"./roblox_packages/jecs\")\nlocal duplecs = require(\"./roblox_packages/duplecs\")\nlocal components = require(\"./shared/components\")\n\nlocal world = jecs.world()\nlocal net = duplecs.server(world) -- the server half; subsequent calls return the same instance\nlocal c = components(world)\n\n-- a client is whatever you pass to add_client; in this case a Player\nPlayers.PlayerAdded:Connect(net.add_client)\nPlayers.PlayerRemoving:Connect(net.remove_client)\n\n-- once per replication step: run your simulation first, then ship what it changed\nRunService.Heartbeat:Connect(function()\n\tfor client, packet in net.generate_packets() do\n\t\tremote:FireClient(client, packet)\n\tend\nend)\n\n-- replicate an entity: tag it, then add its networked components (the reverse also works)\nlocal goblin = world:entity()\nworld:add(goblin, net.Replicated)\nworld:set(goblin, c.Health, 100)\n```\n\n```lua\n-- client\nlocal jecs = require(\"./roblox_packages/jecs\")\nlocal duplecs = require(\"./roblox_packages/duplecs\")\nlocal components = require(\"./shared/components\") -- the same component module\n\nlocal world = jecs.world()\nlocal net = duplecs.client(world) -- the client half; subsequent calls return the same instance\nlocal c = components(world)\n\n-- reconcile packets when received\nremote.OnClientEvent:Connect(net.reconcile_packet)\n\n-- the goblin should show up as an ordinary entity in the client world\nRunService.RenderStepped:Connect(function()\n\tfor entity, health in world:query(c.Health) do\n\t\tprint(`goblin health: {health}`)\n\tend\nend)\n```\n\nThe full wiring can be found in the [getting started guide](docs/guides/001-getting-started.md).\n\n## Installation\n\nduplecs is published to the [pesde](https://pesde.dev) and [Wally](https://wally.run) registries (the scope is `demidog` on pesde but `demi-dog` on Wally — the two registries forbid opposite punctuation characters in names, so the spelling difference is deliberate, not a typo).\n\nWith pesde, run `pesde add demidog/duplecs`, or declare the dependency in `pesde.toml` yourself, alongside your own jecs dependency:\n\n```toml\n[dependencies]\nduplecs = { name = \"demidog/duplecs\", version = \"^1.0.0\" }\njecs = { wally = \"ukendio/jecs\", version = \"^0.11.0\" }\n```\n\nWith Wally, declare it in `wally.toml`:\n\n```toml\n[dependencies]\nduplecs = \"demi-dog/duplecs@^1.0.0\"\njecs = \"ukendio/jecs@^0.11.0\"\n```\n\nduplecs resolves jecs `^0.11.0` transitively (from the Wally registry, `ukendio/jecs`), but your project needs jecs anyway to create the world — keep the versions compatible so both resolve to a single installation.\n\nBefore upgrading duplecs, check the [changelog](CHANGELOG.md): every entry states whether the wire format changed, and both sides of the wire must run the same duplecs build, so a wire change means upgrading the server and its clients together.\n\n## Documentation\n\n- [`docs/guides/`](docs/guides/README.md) — task-oriented guides, each built around working code: a [getting started](docs/guides/001-getting-started.md) setup guide plus per-feature guides readable in any order.\n- [`docs/api.md`](docs/api.md) — the public API listing: every export with a short description.\n- [`docs/spec.md`](docs/spec.md) — the complete specification: exact guarantees, edge cases, and performance notes behind every behavior.\n- [`CHANGELOG.md`](CHANGELOG.md) — per-release change history.\n\n## Contributing\n\nDevelopment runs standalone under [Lune](https://github.com/lune-org/lune) — no Roblox instance involved. [`CONTRIBUTING.md`](CONTRIBUTING.md) covers the setup, the repository layout, the checks a change must keep green, and the changelog and release process.\n\n## Status\n\nduplecs is licensed under the [MIT license](LICENSE) and published on the pesde and Wally registries — see [Installation](#installation) for the package names. Releases are documented in the [changelog](CHANGELOG.md), each entry stating whether the wire format changed.\n","readmeTruncated":false}