Skip to content

Commit

Permalink
feat(@mud/solecs): add @mud/solecs
Browse files Browse the repository at this point in the history
  • Loading branch information
alvrs committed May 13, 2022
1 parent 04ef190 commit 84f05f0
Show file tree
Hide file tree
Showing 59 changed files with 11,924 additions and 36 deletions.
1 change: 0 additions & 1 deletion .yarnrc

This file was deleted.

14 changes: 10 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,14 @@
"repository": "https://github.com/latticexyz/mud.git",
"license": "UNLICENSED",
"private": true,
"workspaces": [
"packages/*"
],
"workspaces": {
"packages": [
"packages/*"
],
"nohoist": [
"@mud/solecs/**"
]
},
"devDependencies": {
"@commitlint/cli": "^16.2.4",
"@commitlint/config-conventional": "^16.2.4",
Expand Down Expand Up @@ -36,5 +41,6 @@
"lint-staged": {
"*.ts": "eslint --cache --fix",
"*.{ts,css,md}": "prettier --write"
}
},
"dependencies": {}
}
2 changes: 2 additions & 0 deletions packages/solecs/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
cache
out
1 change: 1 addition & 0 deletions packages/solecs/.nvmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
17
7 changes: 7 additions & 0 deletions packages/solecs/.prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"printWidth": 120,
"semi": true,
"tabWidth": 2,
"useTabs": false,
"bracketSpacing": true
}
7 changes: 7 additions & 0 deletions packages/solecs/.solhint.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"extends": "solhint:recommended",
"rules": {
"compiler-version": ["error", ">=0.8.0"],
"avoid-low-level-calls": "off"
}
}
3 changes: 3 additions & 0 deletions packages/solecs/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# @mud/solecs

Solidity Entity Component System
9 changes: 9 additions & 0 deletions packages/solecs/foundry.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
[default]
ffi = false
fuzz_runs = 256
optimizer = true
optimizer_runs = 1000000
verbosity = 1
libs = ["node_modules"]
src = "src"
out = "out"
5 changes: 5 additions & 0 deletions packages/solecs/git-install.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#! usr/bin/bash
giturl=https://github.com/$1.git
head=$(git ls-remote $giturl HEAD | head -n1 | awk '{print $1;}')
yarn add $giturl#$head
echo "Installed $giturl#$head"
32 changes: 32 additions & 0 deletions packages/solecs/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
{
"name": "@mud/solecs",
"license": "MIT",
"version": "0.0.1",
"description": "Solidity Entity Component System",
"types": "./types/ethers-contracts/",
"scripts": {
"prepare": "chmod u+x git-install.sh",
"git:install": "bash git-install.sh",
"test": "forge test",
"build": "rimraf out && forge build && yarn dist:abi && yarn types",
"dist:abi": "rimraf abi && mkdir abi && cp out/**/*.json abi/",
"types": "rimraf types && typechain --target=ethers-v5 abi/*.json",
"prettier": "prettier 'src/**/*.sol'",
"solhint": "solhint --config ./.solhint.json 'src/**/*.sol'",
"lint": "yarn prettier && yarn solhint"
},
"devDependencies": {
"@typechain/ethers-v5": "^9.0.0",
"prettier-plugin-solidity": "^1.0.0-beta.19",
"rimraf": "^3.0.2",
"solhint": "^3.3.7",
"typechain": "^8.0.0"
},
"dependencies": {
"@rari-capital/solmate": "https://github.com/Rari-Capital/solmate.git#b6ae78e6ff490f8fec7695c7b65d06e5614f1b65",
"ds-test": "https://github.com/dapphub/ds-test.git#c7a36fb236f298e04edf28e2fee385b80f53945f",
"forge-std": "https://github.com/foundry-rs/forge-std.git#37a3fe48c3a4d8239cda93445f0b5e76b1507436",
"memmove": "https://github.com/brockelmore/memmove.git#d577ecd1bc43656f4032edf4daa9797f756a8ad2",
"openzeppelin-solidity": "https://github.com/OpenZeppelin/openzeppelin-contracts.git#cd2da98d4d21f070f56476b40042279f5500fc59"
}
}
5 changes: 5 additions & 0 deletions packages/solecs/remappings.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
ds-test/=node_modules/ds-test/src/
forge-std/=node_modules/forge-std/src/
@openzeppelin/=node_modules/openzeppelin-solidity/
memmove/=node_modules/memmove/src/
solmate=node_modules/@rari-capital/solmate/src/
109 changes: 109 additions & 0 deletions packages/solecs/src/Component.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
// SPDX-License-Identifier: Unlicense
pragma solidity >=0.8.0;

import '@openzeppelin/contracts/utils/introspection/ERC165Checker.sol';

import { IEntityIndexer } from './interfaces/IEntityIndexer.sol';

import { Set } from './Set.sol';
import { World } from './World.sol';

abstract contract Component {
address public world;
address public owner;

Set private entities;
mapping(bytes => address) private valueToEntities;
mapping(uint256 => bytes) private entityToValue;
IEntityIndexer[] internal indexers;

constructor(address _world) {
world = _world;
World(_world).registerComponent(address(this), getID());
entities = new Set();
owner = msg.sender;
}

modifier onlyContractOwner() {
require(msg.sender == owner, 'ONLY_CONTRACT_OWNER');
_;
}

function transferOwnership(address newOwner) public onlyContractOwner {
owner = newOwner;
}

function getID() public pure virtual returns (uint256);

function set(uint256 entity, bytes memory value) public onlyContractOwner {
// Store the entity
entities.add(entity);

// Store the entity's value;
entityToValue[entity] = value;

// Store the reverse mapping
if (valueToEntities[value] == address(0)) {
valueToEntities[value] = address(new Set());
}
Set(valueToEntities[value]).add(entity);

for (uint256 i = 0; i < indexers.length; i++) {
indexers[i].update(entity, value);
}

// Emit global event
World(world).registerComponentValueSet(address(this), entity, value);
}

function remove(uint256 entity) public onlyContractOwner {
// if there is no entity with this value, return
if (valueToEntities[entityToValue[entity]] == address(0)) return;

// Remove the entity from the reverse mapping
Set(valueToEntities[entityToValue[entity]]).remove(entity);

// Remove the entity from the entity list
entities.remove(entity);

// Remove the entity from the mapping
delete entityToValue[entity];

for (uint256 i = 0; i < indexers.length; i++) {
indexers[i].remove(entity);
}

// Emit global event
World(world).registerComponentValueRemoved(address(this), entity);
}

function has(uint256 entity) public view returns (bool) {
return entities.has(entity);
}

function getRawValue(uint256 entity) public view returns (bytes memory) {
// Return the entity's component value
return entityToValue[entity];
}

function getEntities() public view returns (uint256[] memory) {
return entities.getItems();
}

function getEntitiesWithValue(bytes memory value) public view returns (uint256[] memory) {
if (valueToEntities[value] == address(0)) {
return new uint256[](0);
}

// Return all entities with this component value
return Set(valueToEntities[value]).getItems();
}

function registerIndexer(address indexer) external onlyContractOwner {
require(
ERC165Checker.supportsInterface(indexer, type(IEntityIndexer).interfaceId),
'Given address is not an indexer.'
);
indexers.push(IEntityIndexer(indexer));
}
}
150 changes: 150 additions & 0 deletions packages/solecs/src/Indexer.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
// SPDX-License-Identifier: Unlicense
pragma solidity >=0.8.0;

import { IERC165 } from './interfaces/IERC165.sol';
import { IEntityContainer } from './interfaces/IEntityContainer.sol';
import { IEntityIndexer } from './interfaces/IEntityIndexer.sol';

import { Set } from './Set.sol';
import { Component } from './Component.sol';

contract Indexer is IEntityIndexer, IERC165 {
Set private entities;
mapping(bytes => address) internal valueToEntities;
mapping(uint256 => bytes) internal entityToValue;
Component[] private components;
bool[] private trackValueForComponent;

constructor(Component[] memory _components, bool[] memory _trackValueForComponent) {
components = _components;
trackValueForComponent = _trackValueForComponent;
entities = new Set();
}

function update(uint256 entity, bytes memory value) external {
Component component = Component(msg.sender);
bytes[] memory values = new bytes[](components.length);
uint256 index;
bool foundSenderComponent;

for (uint256 i; i < components.length; i++) {
if (components[i] == component) {
foundSenderComponent = true;
}
}

require(foundSenderComponent, 'Message Sender is not indexed!');

// If data for entity doesn't exist yet create it
if (entityToValue[entity].length == 0) {
// Get all component values from ECS
for (uint256 i = 0; i < components.length; i++) {
// Get ECS Value (or set new value)
values[i] = components[i] == component ? value : components[i].getRawValue(entity);

// If one of the tracked values does not exist yet we can't index
if (values[i].length == 0) {
return;
}

// Ignore non tracked values
if (!trackValueForComponent[i]) {
values[i] = new bytes(0);
}
}

_set(entity, values);
return;
}

// Get index of current component
for (uint256 i; i < components.length; i++) {
if (components[i] == component) {
index = i;
}
}

values = abi.decode(entityToValue[entity], (bytes[]));
// Set value at index
values[index] = value;

// Remove old value
Set(valueToEntities[entityToValue[entity]]).remove(entity);

// Add new value
bytes memory indexerValue = abi.encode(values);

entityToValue[entity] = indexerValue;

// Store the reverse mapping
if (valueToEntities[indexerValue] == address(0)) {
valueToEntities[indexerValue] = address(new Set());
}
Set(valueToEntities[indexerValue]).add(entity);
}

function remove(uint256 entity) external {
Component component = Component(msg.sender);
bool foundSenderComponent;

for (uint256 i; i < components.length; i++) {
if (components[i] == component) {
foundSenderComponent = true;
}
}

require(foundSenderComponent, 'Message Sender is not indexed!');

// if there is no entity with this value, return
if (!has(entity)) return;

// Remove the entity from the reverse mapping
Set(valueToEntities[entityToValue[entity]]).remove(entity);

// Remove the entity from the entity list
entities.remove(entity);

// Remove the entity from the mapping
delete entityToValue[entity];
}

function supportsInterface(bytes4 interfaceId) external view returns (bool) {
return interfaceId == type(IERC165).interfaceId || interfaceId == type(IEntityIndexer).interfaceId;
}

function getEntities() external view returns (uint256[] memory) {
return entities.getItems();
}

function getEntitiesWithValue(bytes memory value) external view returns (uint256[] memory) {
if (valueToEntities[value] == address(0)) {
return new uint256[](0);
}

// Return all entities with this component value
return Set(valueToEntities[value]).getItems();
}

function has(uint256 entity) public view returns (bool) {
return entities.has(entity);
}

function _set(uint256 entity, bytes[] memory newValues) internal {
require(newValues.length == components.length, 'Need values for all components.');

// Store the entity
entities.add(entity);

bytes memory indexerValue = abi.encode(newValues);

// Store the entity's value;
entityToValue[entity] = indexerValue;

// Store the reverse mapping
if (valueToEntities[indexerValue] == address(0)) {
valueToEntities[indexerValue] = address(new Set());
}

Set(valueToEntities[indexerValue]).add(entity);
}
}
Loading

0 comments on commit 84f05f0

Please sign in to comment.