Skip to content

Commit

Permalink
feat(world): support namespaced systems (#2870)
Browse files Browse the repository at this point in the history
  • Loading branch information
yonadaaa authored Jun 5, 2024
1 parent b5bdfb1 commit 745420b
Show file tree
Hide file tree
Showing 24 changed files with 118 additions and 56 deletions.
13 changes: 13 additions & 0 deletions .changeset/eighty-poems-tan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
"@latticexyz/common": patch
---

Added an export for the root namespace string, which can be imported like so:

```
import { ROOT_NAMESPACE } from "@latticexyz/common";
if (namespace === ROOT_NAMESPACE) {
...
}
```
14 changes: 14 additions & 0 deletions .changeset/small-snakes-guess.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
"@latticexyz/world": patch
"@latticexyz/cli": patch
---

Added support for namespaced systems. Namespaced systems are declared like regular systems, but the file and contract name is prefixed with the namespace name and two underscores in the form `{namespace}__{system}.sol`. For example, the contract `app__ChatSystem` will be registered as `"ChatSystem"` in the `"app"` namespace.

```solidity
contract app__ChatSystem is System {
function sendMessage(string memory message) public {
...
}
}
```
4 changes: 0 additions & 4 deletions examples/minimal/packages/contracts/mud.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,6 @@ export default defineWorld({
openAccess: true,
},
},
excludeSystems: [
// Until namespace overrides, this system must be manually deployed in PostDeploy
"ChatNamespacedSystem",
],
tables: {
CounterTable: {
schema: {
Expand Down
13 changes: 5 additions & 8 deletions examples/minimal/packages/contracts/script/PostDeploy.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,11 @@ import { console } from "forge-std/console.sol";
import { StoreSwitch } from "@latticexyz/store/src/StoreSwitch.sol";
import { ResourceId, WorldResourceIdLib, WorldResourceIdInstance } from "@latticexyz/world/src/WorldResourceId.sol";
import { RESOURCE_SYSTEM } from "@latticexyz/world/src/worldResourceTypes.sol";
import { Systems } from "@latticexyz/world/src/codegen/tables/Systems.sol";

import { MessageTable } from "../src/codegen/index.sol";
import { IWorld } from "../src/codegen/world/IWorld.sol";
import { ChatNamespacedSystem } from "../src/systems/ChatNamespacedSystem.sol";
import { namespace__ChatSystem } from "../src/systems/namespace__ChatSystem.sol";

contract PostDeploy is Script {
using WorldResourceIdInstance for ResourceId;
Expand All @@ -24,19 +25,15 @@ contract PostDeploy is Script {
// Start broadcasting transactions from the deployer account
vm.startBroadcast(deployerPrivateKey);

// Manually deploy a system with another namespace
ChatNamespacedSystem chatNamespacedSystem = new ChatNamespacedSystem();
ResourceId systemId = WorldResourceIdLib.encode({
typeId: RESOURCE_SYSTEM,
namespace: "namespace",
name: "ChatNamespaced"
name: "ChatSystem"
});
IWorld(worldAddress).registerNamespace(systemId.getNamespaceId());
IWorld(worldAddress).registerSystem(systemId, chatNamespacedSystem, true);
IWorld(worldAddress).registerFunctionSelector(systemId, "sendMessage(string)");
address chatSystem = Systems.getSystem(systemId);

// Grant this system access to MessageTable
IWorld(worldAddress).grantAccess(MessageTable._tableId, address(chatNamespacedSystem));
IWorld(worldAddress).grantAccess(MessageTable._tableId, chatSystem);

// ------------------ EXAMPLES ------------------

Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ pragma solidity >=0.8.24;
import { System } from "@latticexyz/world/src/System.sol";
import { MessageTable } from "../codegen/index.sol";

// This system is supposed to have a different namespace, but otherwise be identical to ChatSystem
contract ChatNamespacedSystem is System {
// This system has a different namespace, but is otherwise identical to ChatSystem
contract namespace__ChatSystem is System {
function sendMessage(string memory message) public {
MessageTable.set(message);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@ import { IStoreEvents } from "@latticexyz/store/src/IStoreEvents.sol";

import { IWorld } from "../src/codegen/world/IWorld.sol";
import { MessageTable } from "../src/codegen/index.sol";
import { IChatNamespacedSystem } from "../src/interfaces/IChatNamespacedSystem.sol";
import { namespace__IChatSystem } from "../src/codegen/world/namespace__IChatSystem.sol";

contract ChatNamespacedTest is MudTest {
contract ChatTest is MudTest {
function testOffchain() public {
bytes32[] memory keyTuple;
string memory value = "test";
Expand All @@ -22,6 +22,6 @@ contract ChatNamespacedTest is MudTest {
MessageTable.encodeLengths(value),
MessageTable.encodeDynamic(value)
);
IChatNamespacedSystem(worldAddress).namespace__sendMessage(value);
namespace__IChatSystem(worldAddress).namespace__sendMessage(value);
}
}
2 changes: 1 addition & 1 deletion packages/cli/src/deploy/resolveConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ export function resolveConfig<config extends ConfigInput>({
.map(toFunctionSignature);

const systems = Object.entries(resolvedConfig.systems).map(([systemName, system]): System => {
const namespace = config.namespace;
const namespace = system.namespace;
const name = system.name;
const systemId = resourceToHex({ type: "system", namespace, name });
const contractData = getContractData(`${systemName}.sol`, systemName, forgeOutDir);
Expand Down
2 changes: 2 additions & 0 deletions packages/common/src/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,5 @@ export type Resource = {
readonly namespace: string;
readonly name: string;
};

export const ROOT_NAMESPACE = "";
6 changes: 3 additions & 3 deletions packages/common/src/resourceToLabel.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
const rootNamespace = "";
import { ROOT_NAMESPACE } from "./common";

export type ResourceLabel<
namespace extends string = string,
name extends string = string,
> = namespace extends typeof rootNamespace ? name : `${namespace}__${name}`;
> = namespace extends typeof ROOT_NAMESPACE ? name : `${namespace}__${name}`;

export function resourceToLabel<namespace extends string, name extends string>({
namespace,
Expand All @@ -12,5 +12,5 @@ export function resourceToLabel<namespace extends string, name extends string>({
readonly namespace: namespace;
readonly name: name;
}): ResourceLabel<namespace, name> {
return (namespace === rootNamespace ? name : `${namespace}__${name}`) as ResourceLabel<namespace, name>;
return (namespace === ROOT_NAMESPACE ? name : `${namespace}__${name}`) as ResourceLabel<namespace, name>;
}
33 changes: 27 additions & 6 deletions packages/world/ts/config/resolveWorldConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,13 @@ export type ResolvedWorldConfig = ReturnType<typeof resolveWorldConfig>;
* splitting the access list into addresses and system names.
*/
export function resolveWorldConfig(
config: Pick<StoreConfig & WorldConfig, "systems" | "excludeSystems">,
config: Pick<StoreConfig & WorldConfig, "namespace" | "systems" | "excludeSystems">,
existingContracts?: string[],
) {
// Include contract names ending in "System", but not the base "System" contract, and not Interfaces
const defaultSystemNames =
existingContracts?.filter((name) => name.endsWith("System") && name !== "System" && !name.match(/^I[A-Z]/)) ?? [];
existingContracts?.filter((name) => name.endsWith("System") && name !== "System" && !/(^|__)I[A-Z]/.test(name)) ??
[];
const overriddenSystemNames = Object.keys(config.systems);

// Validate every key in systems refers to an existing system contract (and is not called "World")
Expand All @@ -38,7 +39,12 @@ export function resolveWorldConfig(
const resolvedSystems: Record<string, ResolvedSystemConfig> = systemNames.reduce((acc, systemName) => {
return {
...acc,
[systemName]: resolveSystemConfig(systemName, config.systems[systemName], existingContracts),
[systemName]: resolveSystemConfig({
systemName,
configNamespace: config.namespace,
config: config.systems[systemName],
existingContracts,
}),
};
}, {});

Expand All @@ -57,8 +63,23 @@ export function resolveWorldConfig(
* Default value for accessListAddresses is []
* Default value for accessListSystems is []
*/
export function resolveSystemConfig(systemName: string, config?: SystemConfig, existingContracts?: string[]) {
const name = config?.name ?? systemName;
export function resolveSystemConfig({
systemName,
configNamespace,
config,
existingContracts,
}: {
systemName: string;
configNamespace: string;
config?: SystemConfig;
existingContracts?: string[];
}) {
// If the namespace is not set in the system name, default to the config namespace
const parts = systemName.split("__");
const namespaceIsSet = parts.length === 2;
const namespace = namespaceIsSet ? parts[0] : configNamespace;
const name = namespaceIsSet ? parts[1] : systemName;

const registerFunctionSelectors = config?.registerFunctionSelectors ?? true;
const openAccess = config?.openAccess ?? true;
const accessListAddresses: string[] = [];
Expand All @@ -78,5 +99,5 @@ export function resolveSystemConfig(systemName: string, config?: SystemConfig, e
}
}

return { name, registerFunctionSelectors, openAccess, accessListAddresses, accessListSystems };
return { name, namespace, registerFunctionSelectors, openAccess, accessListAddresses, accessListSystems };
}
8 changes: 6 additions & 2 deletions packages/world/ts/node/render-solidity/worldgen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { renderWorldInterface } from "./renderWorldInterface";
import { resolveWorldConfig } from "../../config/resolveWorldConfig";
import { World as WorldConfig } from "../../config/v2/output";
import { worldToV1 } from "../../config/v2/compat";
import { ROOT_NAMESPACE } from "@latticexyz/common";

export async function worldgen(
configV2: WorldConfig,
Expand Down Expand Up @@ -42,10 +43,13 @@ export async function worldgen(
};
}
});
const systemInterfaceName = `I${system.basename}`;

const { namespace, name } = resolvedConfig.systems[system.basename];

const systemInterfaceName = namespace === ROOT_NAMESPACE ? `I${name}` : `${namespace}__I${name}`;
const output = renderSystemInterface({
name: systemInterfaceName,
functionPrefix: config.namespace === "" ? "" : `${config.namespace}__`,
functionPrefix: namespace === ROOT_NAMESPACE ? "" : `${namespace}__`,
functions,
errors,
imports,
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit 745420b

Please sign in to comment.