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 # same data conversion as add_entity (lists/tuples/scalars, None semantics, extras preserved) 86 fk = world._convert_components_data([component], **fk) 87 world._validate_component(component, strict=True, check_extra=True, **fk) 88 89 components = self._get_entity_components(entity_id) 90 if len(components) == 0: 91 raise ValueError(f"guaranteed to be >0 {entity_id} {components}") 92 has_component = self._entity_has_buffered_component(entity_id, component, existing_components=components) 93 if has_component: 94 raise ValueError(f"Component: {component} either added twice or exists already (id: {entity_id})") 95 command.args = {"component": component, **fk} # commit path materializes the converted data 96 97 elif command.command_type == CommandType.REMOVE_COMPONENT: 98 component = command.args # TODO: use command.args["component"] for consistency 99 if component not in world.component_types: 100 raise ValueError(f"Unknown component: {component} not in world components {world.component_types}") 101 102 components = self._get_entity_components(entity_id) 103 if len(components) == 0: 104 raise ValueError(f"guaranteed to be >0 {entity_id} {components}") 105 has_component = self._entity_has_buffered_component(entity_id, component, existing_components=components) 106 if not has_component: 107 raise ValueError(f"Component: {component} either removed twice or doesn't exist (id: {entity_id})") 108 109 self.data.append(command) 110 111 def __iter__(self): 112 return iter(self.data) 113 114 def __len__(self): 115 return len(self.data) 116 117 def __eq__(self, other: CommandBuffer | list[Command]): 118 if isinstance(other, list): 119 return self.data == other 120 elif isinstance(other, CommandBuffer): 121 return self.data == other.data 122 else: 123 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 # same data conversion as add_entity (lists/tuples/scalars, None semantics, extras preserved) 87 fk = world._convert_components_data([component], **fk) 88 world._validate_component(component, strict=True, check_extra=True, **fk) 89 90 components = self._get_entity_components(entity_id) 91 if len(components) == 0: 92 raise ValueError(f"guaranteed to be >0 {entity_id} {components}") 93 has_component = self._entity_has_buffered_component(entity_id, component, existing_components=components) 94 if has_component: 95 raise ValueError(f"Component: {component} either added twice or exists already (id: {entity_id})") 96 command.args = {"component": component, **fk} # commit path materializes the converted data 97 98 elif command.command_type == CommandType.REMOVE_COMPONENT: 99 component = command.args # TODO: use command.args["component"] for consistency 100 if component not in world.component_types: 101 raise ValueError(f"Unknown component: {component} not in world components {world.component_types}") 102 103 components = self._get_entity_components(entity_id) 104 if len(components) == 0: 105 raise ValueError(f"guaranteed to be >0 {entity_id} {components}") 106 has_component = self._entity_has_buffered_component(entity_id, component, existing_components=components) 107 if not has_component: 108 raise ValueError(f"Component: {component} either removed twice or doesn't exist (id: {entity_id})") 109 110 self.data.append(command) 111 112 def __iter__(self): 113 return iter(self.data) 114 115 def __len__(self): 116 return len(self.data) 117 118 def __eq__(self, other: CommandBuffer | list[Command]): 119 if isinstance(other, list): 120 return self.data == other 121 elif isinstance(other, CommandBuffer): 122 return self.data == other.data 123 else: 124 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 # same data conversion as add_entity (lists/tuples/scalars, None semantics, extras preserved) 87 fk = world._convert_components_data([component], **fk) 88 world._validate_component(component, strict=True, check_extra=True, **fk) 89 90 components = self._get_entity_components(entity_id) 91 if len(components) == 0: 92 raise ValueError(f"guaranteed to be >0 {entity_id} {components}") 93 has_component = self._entity_has_buffered_component(entity_id, component, existing_components=components) 94 if has_component: 95 raise ValueError(f"Component: {component} either added twice or exists already (id: {entity_id})") 96 command.args = {"component": component, **fk} # commit path materializes the converted data 97 98 elif command.command_type == CommandType.REMOVE_COMPONENT: 99 component = command.args # TODO: use command.args["component"] for consistency 100 if component not in world.component_types: 101 raise ValueError(f"Unknown component: {component} not in world components {world.component_types}") 102 103 components = self._get_entity_components(entity_id) 104 if len(components) == 0: 105 raise ValueError(f"guaranteed to be >0 {entity_id} {components}") 106 has_component = self._entity_has_buffered_component(entity_id, component, existing_components=components) 107 if not has_component: 108 raise ValueError(f"Component: {component} either removed twice or doesn't exist (id: {entity_id})") 109 110 self.data.append(command)
Appends a command to the buffer