diff --git a/.changeset/quiet-pugs-rule.md b/.changeset/quiet-pugs-rule.md
new file mode 100644
index 0000000000..4399bc1c39
--- /dev/null
+++ b/.changeset/quiet-pugs-rule.md
@@ -0,0 +1,22 @@
+---
+"@latticexyz/world": patch
+---
+
+Added `deploy` config options to systems in the MUD config:
+
+- `disabled` to toggle deploying the system (defaults to `false`)
+- `registerWorldFunctions` to toggle registering namespace-prefixed system functions on the world (defaults to `true`)
+
+```ts
+import { defineWorld } from "@latticexyz/world";
+
+export default defineWorld({
+ systems: {
+ HiddenSystem: {
+ deploy: {
+ registerWorldFunctions: false,
+ },
+ },
+ },
+});
+```
diff --git a/docs/pages/config/reference.mdx b/docs/pages/config/reference.mdx
index 8c2bb986b0..17c45833d8 100644
--- a/docs/pages/config/reference.mdx
+++ b/docs/pages/config/reference.mdx
@@ -65,6 +65,15 @@ The MUD config has two modes: single namespace and multiple namespaces. By defau
Whether or not any address can call this system. Defaults to `true`.
A list of contract addresses or system labels that can call this system, used with `openAccess: false`.
+
+ Customize how to deploy this system.
+
+
+ Disable deployment of this system. Defaults to `false`.
+ Whether this system's functions should be registered on the world, prefixed with the system namespace. Defaults to `true`.
+
+
+
diff --git a/e2e/packages/contracts/mud.config.ts b/e2e/packages/contracts/mud.config.ts
index 14c972dd50..db69f40144 100644
--- a/e2e/packages/contracts/mud.config.ts
+++ b/e2e/packages/contracts/mud.config.ts
@@ -58,6 +58,13 @@ export default defineWorld({
key: [],
},
},
+ systems: {
+ HiddenSystem: {
+ deploy: {
+ registerWorldFunctions: false,
+ },
+ },
+ },
modules: [
{
artifactPath:
diff --git a/e2e/packages/contracts/src/systems/HiddenSystem.sol b/e2e/packages/contracts/src/systems/HiddenSystem.sol
new file mode 100644
index 0000000000..5ad4b56a11
--- /dev/null
+++ b/e2e/packages/contracts/src/systems/HiddenSystem.sol
@@ -0,0 +1,10 @@
+// SPDX-License-Identifier: MIT
+pragma solidity >=0.8.24;
+
+import { System } from "@latticexyz/world/src/System.sol";
+
+contract HiddenSystem is System {
+ function hidden() public {
+ // this system is only callable with system ID, not via world
+ }
+}
diff --git a/packages/cli/src/deploy/common.ts b/packages/cli/src/deploy/common.ts
index 4714d7a934..8116043f2b 100644
--- a/packages/cli/src/deploy/common.ts
+++ b/packages/cli/src/deploy/common.ts
@@ -89,7 +89,7 @@ export type System = DeterministicContract & {
readonly allowAll: boolean;
readonly allowedAddresses: readonly Hex[];
readonly allowedSystemIds: readonly Hex[];
- readonly functions: readonly WorldFunction[];
+ readonly worldFunctions: readonly WorldFunction[];
};
export type DeployedSystem = Omit & {
diff --git a/packages/cli/src/deploy/deploy.ts b/packages/cli/src/deploy/deploy.ts
index ee148df050..76384c6de3 100644
--- a/packages/cli/src/deploy/deploy.ts
+++ b/packages/cli/src/deploy/deploy.ts
@@ -115,7 +115,7 @@ export async function deploy({
const functionTxs = await ensureFunctions({
client,
worldDeploy,
- functions: systems.flatMap((system) => system.functions),
+ functions: systems.flatMap((system) => system.worldFunctions),
});
const moduleTxs = await ensureModules({
client,
diff --git a/packages/cli/src/deploy/getSystems.ts b/packages/cli/src/deploy/getSystems.ts
index 07b14974a7..70b88132d2 100644
--- a/packages/cli/src/deploy/getSystems.ts
+++ b/packages/cli/src/deploy/getSystems.ts
@@ -31,7 +31,7 @@ export async function getSystems({
table: worldConfig.namespaces.world.tables.Systems,
key: { systemId: system.resourceId },
});
- const systemFunctions = functions.filter((func) => func.systemId === system.resourceId);
+ const worldFunctions = functions.filter((func) => func.systemId === system.resourceId);
return {
address,
namespace: system.namespace,
@@ -41,7 +41,7 @@ export async function getSystems({
allowedAddresses: resourceAccess
.filter(({ resourceId }) => resourceId === system.resourceId)
.map(({ address }) => address),
- functions: systemFunctions,
+ worldFunctions,
};
}),
);
diff --git a/packages/cli/src/deploy/resolveConfig.ts b/packages/cli/src/deploy/resolveConfig.ts
index e22e8daffe..9209456f2f 100644
--- a/packages/cli/src/deploy/resolveConfig.ts
+++ b/packages/cli/src/deploy/resolveConfig.ts
@@ -41,45 +41,49 @@ export async function resolveConfig({
const configSystems = await resolveSystems({ rootDir, config });
- const systems = configSystems.map((system): System => {
- const contractData = getContractData(`${system.label}.sol`, system.label, forgeOutDir);
+ const systems = configSystems
+ .filter((system) => !system.deploy.disabled)
+ .map((system): System => {
+ const contractData = getContractData(`${system.label}.sol`, system.label, forgeOutDir);
- const systemFunctions = contractData.abi
- .filter((item): item is typeof item & { type: "function" } => item.type === "function")
- .map(toFunctionSignature)
- .filter((sig) => !baseSystemFunctions.includes(sig))
- .map((sig): WorldFunction => {
- // TODO: figure out how to not duplicate contract behavior (https://github.com/latticexyz/mud/issues/1708)
- const worldSignature = system.namespace === "" ? sig : `${system.namespace}__${sig}`;
- return {
- signature: worldSignature,
- selector: toFunctionSelector(worldSignature),
- systemId: system.systemId,
- systemFunctionSignature: sig,
- systemFunctionSelector: toFunctionSelector(sig),
- };
- });
+ const worldFunctions = system.deploy.registerWorldFunctions
+ ? contractData.abi
+ .filter((item): item is typeof item & { type: "function" } => item.type === "function")
+ .map(toFunctionSignature)
+ .filter((sig) => !baseSystemFunctions.includes(sig))
+ .map((sig): WorldFunction => {
+ // TODO: figure out how to not duplicate contract behavior (https://github.com/latticexyz/mud/issues/1708)
+ const worldSignature = system.namespace === "" ? sig : `${system.namespace}__${sig}`;
+ return {
+ signature: worldSignature,
+ selector: toFunctionSelector(worldSignature),
+ systemId: system.systemId,
+ systemFunctionSignature: sig,
+ systemFunctionSelector: toFunctionSelector(sig),
+ };
+ })
+ : [];
- // TODO: move to resolveSystems?
- const allowedAddresses = system.accessList.filter((target): target is Hex => isHex(target));
- const allowedSystemIds = system.accessList
- .filter((target) => !isHex(target))
- .map((label) => {
- const system = configSystems.find((s) => s.label === label)!;
- return system.systemId;
- });
+ // TODO: move to resolveSystems?
+ const allowedAddresses = system.accessList.filter((target): target is Hex => isHex(target));
+ const allowedSystemIds = system.accessList
+ .filter((target) => !isHex(target))
+ .map((label) => {
+ const system = configSystems.find((s) => s.label === label)!;
+ return system.systemId;
+ });
- return {
- ...system,
- allowAll: system.openAccess,
- allowedAddresses,
- allowedSystemIds,
- prepareDeploy: createPrepareDeploy(contractData.bytecode, contractData.placeholders),
- deployedBytecodeSize: contractData.deployedBytecodeSize,
- abi: contractData.abi,
- functions: systemFunctions,
- };
- });
+ return {
+ ...system,
+ allowAll: system.openAccess,
+ allowedAddresses,
+ allowedSystemIds,
+ prepareDeploy: createPrepareDeploy(contractData.bytecode, contractData.placeholders),
+ deployedBytecodeSize: contractData.deployedBytecodeSize,
+ abi: contractData.abi,
+ worldFunctions,
+ };
+ });
// Check for overlapping system IDs (since names get truncated when turning into IDs)
// TODO: move this into the world config resolve step once it resolves system IDs
diff --git a/packages/world/ts/config/v2/defaults.ts b/packages/world/ts/config/v2/defaults.ts
index e6e7c571b8..13f77b09fb 100644
--- a/packages/world/ts/config/v2/defaults.ts
+++ b/packages/world/ts/config/v2/defaults.ts
@@ -1,10 +1,17 @@
-import { CodegenInput, DeployInput, ModuleInput, SystemInput, WorldInput } from "./input";
+import { CodegenInput, DeployInput, ModuleInput, SystemDeployInput, SystemInput, WorldInput } from "./input";
+
+export const SYSTEM_DEPLOY_DEFAULTS = {
+ disabled: false,
+ registerWorldFunctions: true,
+} as const satisfies Required;
+
+export type SYSTEM_DEPLOY_DEFAULTS = typeof SYSTEM_DEPLOY_DEFAULTS;
export const SYSTEM_DEFAULTS = {
namespace: "",
openAccess: true,
accessList: [],
-} as const satisfies Omit, "label" | "name">;
+} as const satisfies Omit, "label" | "name" | "deploy">;
export type SYSTEM_DEFAULTS = typeof SYSTEM_DEFAULTS;
diff --git a/packages/world/ts/config/v2/input.ts b/packages/world/ts/config/v2/input.ts
index 48b7799019..9af5b30884 100644
--- a/packages/world/ts/config/v2/input.ts
+++ b/packages/world/ts/config/v2/input.ts
@@ -1,6 +1,8 @@
import { StoreInput, NamespaceInput as StoreNamespaceInput } from "@latticexyz/store/config/v2";
import { DynamicResolution, ValueWithType } from "./dynamicResolution";
-import { Codegen } from "./output";
+import { Codegen, SystemDeploy } from "./output";
+
+export type SystemDeployInput = Partial;
export type SystemInput = {
/**
@@ -22,6 +24,7 @@ export type SystemInput = {
readonly openAccess?: boolean;
/** An array of addresses or system names that can access the system */
readonly accessList?: readonly string[];
+ readonly deploy?: SystemDeployInput;
};
export type SystemsInput = {
diff --git a/packages/world/ts/config/v2/output.ts b/packages/world/ts/config/v2/output.ts
index 2f70ffd759..00e78c83b5 100644
--- a/packages/world/ts/config/v2/output.ts
+++ b/packages/world/ts/config/v2/output.ts
@@ -25,6 +25,20 @@ export type Module = {
readonly artifactPath: string | undefined;
};
+export type SystemDeploy = {
+ /**
+ * Whether or not to deploy the system.
+ * Defaults to `false`.
+ */
+ readonly disabled: boolean;
+ /**
+ * Whether or not to register system functions on the world.
+ * System functions are prefixed with the system namespace when registering on the world, so system function names must be unique within their namespace.
+ * Defaults to `true`.
+ */
+ readonly registerWorldFunctions: boolean;
+};
+
export type System = {
/**
* Human-readable system label. Used as config keys, interface names, and filenames.
@@ -47,6 +61,7 @@ export type System = {
readonly openAccess: boolean;
/** An array of addresses or system names that can access the system */
readonly accessList: readonly string[];
+ readonly deploy: SystemDeploy;
};
export type Systems = {
diff --git a/packages/world/ts/config/v2/system.test.ts b/packages/world/ts/config/v2/system.test.ts
index 8664100d46..a1a11b08fe 100644
--- a/packages/world/ts/config/v2/system.test.ts
+++ b/packages/world/ts/config/v2/system.test.ts
@@ -16,6 +16,10 @@ describe("resolveSystem", () => {
namespace: "",
name: "ExampleSystem" as string,
systemId: resourceToHex({ type: "system", namespace: "", name: "ExampleSystem" }),
+ deploy: {
+ disabled: false,
+ registerWorldFunctions: true,
+ },
} as const;
attest(system).equals(expected);
@@ -33,6 +37,10 @@ describe("resolveSystem", () => {
namespace: "",
name: "ExampleSystem" as string,
systemId: resourceToHex({ type: "system", namespace: "", name: "ExampleSystem" }),
+ deploy: {
+ disabled: false,
+ registerWorldFunctions: true,
+ },
} as const;
attest(system).equals(expected);
@@ -51,6 +59,10 @@ describe("resolveSystem", () => {
name: "ExampleSystem" as string,
systemId: resourceToHex({ type: "system", namespace: "", name: "ExampleSystem" }),
openAccess: false,
+ deploy: {
+ disabled: false,
+ registerWorldFunctions: true,
+ },
} as const;
attest(system).equals(expected);
diff --git a/packages/world/ts/config/v2/system.ts b/packages/world/ts/config/v2/system.ts
index c756699eb1..cc201078a4 100644
--- a/packages/world/ts/config/v2/system.ts
+++ b/packages/world/ts/config/v2/system.ts
@@ -1,7 +1,7 @@
-import { SYSTEM_DEFAULTS } from "./defaults";
+import { SYSTEM_DEFAULTS, SYSTEM_DEPLOY_DEFAULTS } from "./defaults";
import { SystemInput } from "./input";
import { hasOwnKey, mergeIfUndefined } from "@latticexyz/store/config/v2";
-import { ErrorMessage, narrow, requiredKeyOf } from "@ark/util";
+import { ErrorMessage, narrow, requiredKeyOf, show } from "@ark/util";
import { Hex } from "viem";
import { resourceToHex } from "@latticexyz/common";
@@ -52,6 +52,9 @@ export type resolveSystem = input extends SystemInput
readonly systemId: Hex;
readonly openAccess: undefined extends input["openAccess"] ? SYSTEM_DEFAULTS["openAccess"] : input["openAccess"];
readonly accessList: undefined extends input["accessList"] ? SYSTEM_DEFAULTS["accessList"] : input["accessList"];
+ readonly deploy: show<
+ mergeIfUndefined
+ >;
}
: never;
@@ -68,6 +71,7 @@ export function resolveSystem(input: input): resolveS
namespace,
name,
systemId,
+ deploy: mergeIfUndefined(input.deploy ?? {}, SYSTEM_DEPLOY_DEFAULTS),
},
SYSTEM_DEFAULTS,
) as never;
diff --git a/packages/world/ts/config/v2/world.test.ts b/packages/world/ts/config/v2/world.test.ts
index 971840e36a..90c6a2aefd 100644
--- a/packages/world/ts/config/v2/world.test.ts
+++ b/packages/world/ts/config/v2/world.test.ts
@@ -581,6 +581,7 @@ describe("defineWorld", () => {
namespace: "app",
name: "Example",
systemId: "0x737961707000000000000000000000004578616d706c65000000000000000000",
+ deploy: { disabled: false, registerWorldFunctions: true },
openAccess: true,
accessList: [],
},
@@ -592,6 +593,10 @@ describe("defineWorld", () => {
readonly systemId: \`0x\${string}\`
readonly openAccess: true
readonly accessList: readonly []
+ readonly deploy: {
+ readonly disabled: false
+ readonly registerWorldFunctions: true
+ }
}
}`);
});
diff --git a/packages/world/ts/node/render-solidity/worldgen.ts b/packages/world/ts/node/render-solidity/worldgen.ts
index 76b0ec8944..3305b8b27b 100644
--- a/packages/world/ts/node/render-solidity/worldgen.ts
+++ b/packages/world/ts/node/render-solidity/worldgen.ts
@@ -28,14 +28,17 @@ export async function worldgen({
const outputPath = path.join(outDir, config.codegen.worldInterfaceName + ".sol");
- const systems = (await resolveSystems({ rootDir, config })).map((system) => {
- const interfaceName = `I${system.label}`;
- return {
- ...system,
- interfaceName,
- interfacePath: path.join(path.dirname(outputPath), `${interfaceName}.sol`),
- };
- });
+ const systems = (await resolveSystems({ rootDir, config }))
+ // TODO: move to codegen option or generate "system manifest" and codegen from that
+ .filter((system) => system.deploy.registerWorldFunctions)
+ .map((system) => {
+ const interfaceName = `I${system.label}`;
+ return {
+ ...system,
+ interfaceName,
+ interfacePath: path.join(path.dirname(outputPath), `${interfaceName}.sol`),
+ };
+ });
const worldImports = systems.map(
(system): ImportDatum => ({