microecs.command_buffer
command_buffer.py - data structure that eagerly manages the staging area (add/rm entity/cmpt) before world.upate()
1# pylint: disable=protected-access 2"""command_buffer.py - data structure that eagerly manages the staging area (add/rm entity/cmpt) before world.upate()""" 3from __future__ import annotations 4from typing import Any 5from dataclasses import dataclass 6from enum import StrEnum 7 8from .utils import EntityId 9from .component import ComponentType 10 11class CommandType(StrEnum): 12 """The types of commands in the command pattern below""" 13 ADD_ENTITY = "add_entity" 14 REMOVE_ENTITY = "remove_entity" 15 ADD_COMPONENT = "add_component" 16 REMOVE_COMPONENT = "remove_component" 17 18@dataclass 19class Command: 20 """A command that can happen between two world.updates(), e.g. add/rm entity or components""" 21 command_type: CommandType 22 entity_id: EntityId 23 args: Any | None = None 24 25class CommandBuffer: 26 """A data structure that holds all the uncommited commands between two world updates. Support eager exceptions 27 on things like adding the same component twice on the same entity""" 28 def __init__(self, world: "World"): # noqa 29 self.data: list[Command] = [] 30 self.world = world 31 self.removed_this_tick: set[EntityId] = set() 32 33 def clear(self): 34 """Clears the buffer""" 35 self.data.clear() 36 self.removed_this_tick.clear() 37 38 def _get_entity_components(self, entity_id: EntityId) -> list[ComponentType]: # noqa 39 # This is the case for uncommited entities 40 if entity_id not in self.world._eid_to_pool_ix: 41 # uncommitted spawn: base = the components it was spawned with 42 for cmd in self.data: 43 if cmd.entity_id == entity_id and cmd.command_type == CommandType.ADD_ENTITY: 44 return cmd.args["components"] 45 return [] # Entity should exist so this shouldn't be reached technically. We have an assert at call site. 46 pool, _ = self.world._eid_to_pool_ix[entity_id] 47 return self.world.pool_to_components[pool] 48 49 def _entity_has_buffered_component(self, entity_id: EntityId, component: ComponentType, 50 existing_components: list[ComponentType]) -> bool: 51 # Look for the latest state of this entity w.r.t this component given the unstaged command buffer. 52 # We look in the buffer from right to left and return True if the component was added, False otherwise. 53 # If the component is not in the buffer at all, we check if it is already in the entity. Returns a bool. 54 for old_command in reversed(self.data): 55 if old_command.entity_id != entity_id: 56 continue 57 if old_command.command_type == CommandType.ADD_COMPONENT: 58 old_component = old_command.args["component"] 59 if component == old_component: 60 return True 61 if old_command.command_type == CommandType.REMOVE_COMPONENT: 62 old_component = old_command.args 63 if component == old_component: 64 return False 65 return component in existing_components 66 67 def append(self, command: Command): 68 """Appends a command to the buffer""" 69 world = self.world 70 entity_id = command.entity_id 71 if entity_id not in world.live_entities: 72 raise ValueError(f"Entity: {entity_id} not in live entities ({command})") 73 74 if command.command_type == CommandType.ADD_ENTITY: 75 # nothing to do here: world.add_entity already ensures validated args (& defaults) come here. 76 pass 77 78 elif command.command_type == CommandType.REMOVE_ENTITY: 79 # needed so we can fast check in world.remove_enitity if this is a no-op (same tick) or error (stale eid). 80 self.removed_this_tick.add(command.entity_id) 81 82 elif command.command_type == CommandType.ADD_COMPONENT: 83 component = command.args["component"] 84 fk = {k: v for k, v in command.args.items() if k != "component"} 85 world._validate_component(component, strict=True, check_extra=True, **fk) 86 87 components = self._get_entity_components(entity_id) 88 assert len(components) > 0, f"guaranteed to be >0 {entity_id} {components}" 89 has_component = self._entity_has_buffered_component(entity_id, component, existing_components=components) 90 if has_component: 91 raise ValueError(f"Component: {component} either added twice or exists already (id: {entity_id})") 92 93 elif command.command_type == CommandType.REMOVE_COMPONENT: 94 component = command.args # TODO: use command.args["component"] for consistency 95 if component not in world.component_types: 96 raise ValueError(f"Unknown component: {component} not in world components {world.component_types}") 97 98 components = self._get_entity_components(entity_id) 99 assert len(components) > 0, f"guaranteed to be >0 {entity_id} {components}" 100 has_component = self._entity_has_buffered_component(entity_id, component, existing_components=components) 101 if not has_component: 102 raise ValueError(f"Component: {component} either removed twice or doesn't exist (id: {entity_id})") 103 104 self.data.append(command) 105 106 def __iter__(self): 107 return iter(self.data) 108 109 def __len__(self): 110 return len(self.data) 111 112 def __eq__(self, other: CommandBuffer | list[Command]): 113 if isinstance(other, list): 114 return self.data == other 115 elif isinstance(other, CommandBuffer): 116 return self.data == other.data 117 else: 118 return NotImplemented
class
CommandType(enum.StrEnum):
12class CommandType(StrEnum): 13 """The types of commands in the command pattern below""" 14 ADD_ENTITY = "add_entity" 15 REMOVE_ENTITY = "remove_entity" 16 ADD_COMPONENT = "add_component" 17 REMOVE_COMPONENT = "remove_component"
The types of commands in the command pattern below
ADD_ENTITY =
<CommandType.ADD_ENTITY: 'add_entity'>
REMOVE_ENTITY =
<CommandType.REMOVE_ENTITY: 'remove_entity'>
ADD_COMPONENT =
<CommandType.ADD_COMPONENT: 'add_component'>
REMOVE_COMPONENT =
<CommandType.REMOVE_COMPONENT: 'remove_component'>
@dataclass
class
Command:
19@dataclass 20class Command: 21 """A command that can happen between two world.updates(), e.g. add/rm entity or components""" 22 command_type: CommandType 23 entity_id: EntityId 24 args: Any | None = None
A command that can happen between two world.updates(), e.g. add/rm entity or components
Command( command_type: CommandType, entity_id: int, args: typing.Any | None = None)
command_type: CommandType
class
CommandBuffer:
26class CommandBuffer: 27 """A data structure that holds all the uncommited commands between two world updates. Support eager exceptions 28 on things like adding the same component twice on the same entity""" 29 def __init__(self, world: "World"): # noqa 30 self.data: list[Command] = [] 31 self.world = world 32 self.removed_this_tick: set[EntityId] = set() 33 34 def clear(self): 35 """Clears the buffer""" 36 self.data.clear() 37 self.removed_this_tick.clear() 38 39 def _get_entity_components(self, entity_id: EntityId) -> list[ComponentType]: # noqa 40 # This is the case for uncommited entities 41 if entity_id not in self.world._eid_to_pool_ix: 42 # uncommitted spawn: base = the components it was spawned with 43 for cmd in self.data: 44 if cmd.entity_id == entity_id and cmd.command_type == CommandType.ADD_ENTITY: 45 return cmd.args["components"] 46 return [] # Entity should exist so this shouldn't be reached technically. We have an assert at call site. 47 pool, _ = self.world._eid_to_pool_ix[entity_id] 48 return self.world.pool_to_components[pool] 49 50 def _entity_has_buffered_component(self, entity_id: EntityId, component: ComponentType, 51 existing_components: list[ComponentType]) -> bool: 52 # Look for the latest state of this entity w.r.t this component given the unstaged command buffer. 53 # We look in the buffer from right to left and return True if the component was added, False otherwise. 54 # If the component is not in the buffer at all, we check if it is already in the entity. Returns a bool. 55 for old_command in reversed(self.data): 56 if old_command.entity_id != entity_id: 57 continue 58 if old_command.command_type == CommandType.ADD_COMPONENT: 59 old_component = old_command.args["component"] 60 if component == old_component: 61 return True 62 if old_command.command_type == CommandType.REMOVE_COMPONENT: 63 old_component = old_command.args 64 if component == old_component: 65 return False 66 return component in existing_components 67 68 def append(self, command: Command): 69 """Appends a command to the buffer""" 70 world = self.world 71 entity_id = command.entity_id 72 if entity_id not in world.live_entities: 73 raise ValueError(f"Entity: {entity_id} not in live entities ({command})") 74 75 if command.command_type == CommandType.ADD_ENTITY: 76 # nothing to do here: world.add_entity already ensures validated args (& defaults) come here. 77 pass 78 79 elif command.command_type == CommandType.REMOVE_ENTITY: 80 # needed so we can fast check in world.remove_enitity if this is a no-op (same tick) or error (stale eid). 81 self.removed_this_tick.add(command.entity_id) 82 83 elif command.command_type == CommandType.ADD_COMPONENT: 84 component = command.args["component"] 85 fk = {k: v for k, v in command.args.items() if k != "component"} 86 world._validate_component(component, strict=True, check_extra=True, **fk) 87 88 components = self._get_entity_components(entity_id) 89 assert len(components) > 0, f"guaranteed to be >0 {entity_id} {components}" 90 has_component = self._entity_has_buffered_component(entity_id, component, existing_components=components) 91 if has_component: 92 raise ValueError(f"Component: {component} either added twice or exists already (id: {entity_id})") 93 94 elif command.command_type == CommandType.REMOVE_COMPONENT: 95 component = command.args # TODO: use command.args["component"] for consistency 96 if component not in world.component_types: 97 raise ValueError(f"Unknown component: {component} not in world components {world.component_types}") 98 99 components = self._get_entity_components(entity_id) 100 assert len(components) > 0, f"guaranteed to be >0 {entity_id} {components}" 101 has_component = self._entity_has_buffered_component(entity_id, component, existing_components=components) 102 if not has_component: 103 raise ValueError(f"Component: {component} either removed twice or doesn't exist (id: {entity_id})") 104 105 self.data.append(command) 106 107 def __iter__(self): 108 return iter(self.data) 109 110 def __len__(self): 111 return len(self.data) 112 113 def __eq__(self, other: CommandBuffer | list[Command]): 114 if isinstance(other, list): 115 return self.data == other 116 elif isinstance(other, CommandBuffer): 117 return self.data == other.data 118 else: 119 return NotImplemented
A data structure that holds all the uncommited commands between two world updates. Support eager exceptions on things like adding the same component twice on the same entity
data: list[Command]
def
clear(self):
34 def clear(self): 35 """Clears the buffer""" 36 self.data.clear() 37 self.removed_this_tick.clear()
Clears the buffer
68 def append(self, command: Command): 69 """Appends a command to the buffer""" 70 world = self.world 71 entity_id = command.entity_id 72 if entity_id not in world.live_entities: 73 raise ValueError(f"Entity: {entity_id} not in live entities ({command})") 74 75 if command.command_type == CommandType.ADD_ENTITY: 76 # nothing to do here: world.add_entity already ensures validated args (& defaults) come here. 77 pass 78 79 elif command.command_type == CommandType.REMOVE_ENTITY: 80 # needed so we can fast check in world.remove_enitity if this is a no-op (same tick) or error (stale eid). 81 self.removed_this_tick.add(command.entity_id) 82 83 elif command.command_type == CommandType.ADD_COMPONENT: 84 component = command.args["component"] 85 fk = {k: v for k, v in command.args.items() if k != "component"} 86 world._validate_component(component, strict=True, check_extra=True, **fk) 87 88 components = self._get_entity_components(entity_id) 89 assert len(components) > 0, f"guaranteed to be >0 {entity_id} {components}" 90 has_component = self._entity_has_buffered_component(entity_id, component, existing_components=components) 91 if has_component: 92 raise ValueError(f"Component: {component} either added twice or exists already (id: {entity_id})") 93 94 elif command.command_type == CommandType.REMOVE_COMPONENT: 95 component = command.args # TODO: use command.args["component"] for consistency 96 if component not in world.component_types: 97 raise ValueError(f"Unknown component: {component} not in world components {world.component_types}") 98 99 components = self._get_entity_components(entity_id) 100 assert len(components) > 0, f"guaranteed to be >0 {entity_id} {components}" 101 has_component = self._entity_has_buffered_component(entity_id, component, existing_components=components) 102 if not has_component: 103 raise ValueError(f"Component: {component} either removed twice or doesn't exist (id: {entity_id})") 104 105 self.data.append(command)
Appends a command to the buffer