|
import |
|
tables, macros, strutils, algorithm, typetraits, sequtils, |
|
fowltek/maybe_t |
|
export typetraits.name, tables |
|
|
|
type |
|
TEntDestructor* = proc(entity:PEntity){.nimcall.} |
|
PTypeinfo* = ref object |
|
## an aggregate of components |
|
size: int |
|
offsets: seq[int] |
|
messages: seq[RT_Message] |
|
initializers*, destructors*: seq[TEntDestructor] |
|
errors*: TMaybe[seq[string]] |
|
when false: |
|
components: seq[TMaybe[tuple[offset: int]]] |
|
else: |
|
components: seq[bool] |
|
|
|
TMessageKind* = enum mNope = 0, mUnicast, mMulticast |
|
RT_Message = object |
|
case kind*: TMessageKind |
|
of mNope: |
|
nil |
|
of mUnicast: |
|
f*: pointer |
|
of mMulticast: |
|
fs*: seq[pointer] |
|
|
|
PEntity* = ref TEntity |
|
TEntity* = object of TObject |
|
data*: cstring |
|
ty*: PTypeinfo |
|
id*: int |
|
|
|
proc `$`* (m:RTMessage):string = |
|
result = "(" |
|
result.add($ m.kind) |
|
case m.kind |
|
of mUnicast: |
|
result.add " set=" |
|
result.add($ not m.f.isNil) |
|
of mMulticast: |
|
result.add " len=" |
|
result.add($ m.fs.len) |
|
else: |
|
discard |
|
result.add")" |
|
|
|
|
|
type |
|
# compile time message information |
|
TMessage = tuple |
|
id: int |
|
multicast: bool |
|
params: PNimrodNode |
|
|
|
var |
|
## compiletime message info |
|
messageIDs* {.compileTime.} = initTable[string,TMessage]() |
|
m_id_counter {.compileTime.} = 0 |
|
|
|
## runtime message info |
|
rt_msgs_multicast* : seq[bool] = @[] |
|
|
|
template dump_messages* : stmt {.immediate.} = |
|
# dump message info at compile time (it doesnt exist at runtime) |
|
static: |
|
echo m_id_counter, " messages:" |
|
for name,m in pairs(messageIDs): |
|
echo " #",m.id, " ",name, " ", (if m.multicast: "multicast" else: "unicast"), " ", repr(m.params) |
|
|
|
proc ensureLen* [T] (S:var seq[T]; L:int) = |
|
if s.len < L: |
|
s.setLen L |
|
|
|
proc collect_argnames (formal_params: PNimrodNode): seq[string] {.compiletime.} = |
|
assert formal_params.kind == nnkFormalParams |
|
result = @[] |
|
for i in 1 .. <len(formal_params): |
|
for ii in 0 .. len(formal_params[i])-3: |
|
result.add($ formal_params[i][ii].ident) |
|
|
|
|
|
template voidExpr* (x): expr = not compiles(type(x)) |
|
# hack to determine if an expression is void |
|
|
|
|
|
macro multicast* (func): stmt = |
|
let f = func.body.copyNimTree |
|
f.params.insert(1, newIdentDefs("entity".ident, "PEntity".ident)) |
|
|
|
let name = ($basename(f.name).ident).normalize |
|
if messageIDs.hasKey(name): |
|
echo "entity failure ", name, " is already declared! new params: ($#) old params: ($#)".format( |
|
f.params.repr, messageIDs[name].params.repr) |
|
|
|
let m_ty = "MSG_"&name |
|
let m_id = m_id_counter |
|
m_id_counter += 1 |
|
echo "New multicast message #", $m_id, ": ", name, " ", repr(f.params) |
|
|
|
var msg: TMessage |
|
msg.id = m_id |
|
msg.multicast = true |
|
msg.params = f.params |
|
messageIDs[name] = msg |
|
|
|
let arg_names = collect_argnames(f.params) |
|
echo arg_names |
|
|
|
#result body should be something like |
|
# if entity.ty.messages[m_id].kind == mMulticast: |
|
# for f in entity.ty.messages[m_id].fs.items: |
|
# cast[MSG_name]( f )( arguments... ) |
|
let xp = parseStmt(""" |
|
if entity.ty.messages[$1].kind == mMulticast: |
|
for f in entity.ty.messages[$1].fs.items: |
|
cast[$2](f)($3) |
|
""".format( m_id, m_ty, arg_names.join(",") )) |
|
f.body = newStmtList(xp) |
|
|
|
result = newStmtList() |
|
|
|
result.add newNimNode(nnkTypeSection).add( |
|
newNimNode(nnkTypeDef).add( |
|
ident(m_ty).postfix("*"), |
|
newEmptyNode(), |
|
newNimNode(nnkProcTy).add(f.params, newNimNode(nnkPragma).add(ident"nimcall")))) |
|
result.add f |
|
result.add parseExpr("rt_msgs_multicast.ensureLen("& $(1+m_id) &")") |
|
result.add parseExpr("rt_msgs_multicast["& $m_id & "] = true") |
|
|
|
result.repr.echo |
|
|
|
macro unicast* (func): stmt = |
|
let f = func.body.copyNimTree |
|
f.params.insert(1, newIdentDefs("entity".ident, "PEntity".ident)) |
|
|
|
let name = ($basename(f.name).ident).normalize |
|
if messageIDs.hasKey(name): |
|
echo "entitty failure: ", name, " is being redeclared! new params: (", f.params.repr, ") old params: (", messageIDs[name].params.repr, ")"#, instantiation_info() |
|
quit 1 |
|
|
|
var arg_names : seq[string] = @[] |
|
for idx in 1 .. <len(f.params): |
|
for ii in 0 .. len(f.params[idx])-3: |
|
arg_names.add($ f.params[idx][ii]) |
|
|
|
#let is_void = f.params[0].kind == nnkEmpty or (f.params[0].kind == nnkIdent and ($f.params[0]).tolower == "void") |
|
|
|
let m_id = m_id_counter |
|
m_id_counter += 1 |
|
echo name, ": id=", m_id, " (", repr(f.params),")" |
|
#if messageIDS.isnil: |
|
# echo "ITS NIL" |
|
# quit 1 |
|
|
|
var msg: TMessage |
|
msg.id = m_id |
|
msg.multicast = false |
|
msg.params = f.params |
|
messageIDs[name] = msg |
|
|
|
discard """ if messageParams.len < m_id+1: |
|
messageParams.setLen m_id+1 |
|
messageParams[m_id] = f.params |
|
""" |
|
|
|
let body = newStmtList() |
|
|
|
#the code i need here: |
|
# if entity.ty.messages[m_id].kind == mUnicast: |
|
# cast[MSG_ name](entity.ty.messages[m_id].f)(entity, args...) |
|
|
|
let message_body = """ |
|
if entity.ty.messages[$1].kind == mUnicast: |
|
when true: |
|
## TODO test |
|
let f = cast[$2](entity.ty.messages[$1].f) |
|
when voidExpr(f($3)): f($3) |
|
else: return f($3) |
|
else: |
|
when voidExpr(cast[$2](entity.ty.messages[$1].f)($3)): |
|
cast[$2](entity.ty.messages[$1].f)($3) |
|
else: |
|
return cast[$2](entity.ty.messages[$1].f)($3) |
|
""".format( |
|
m_id, "MSG_"&name, arg_names.join(",") |
|
) |
|
body.add parseStmt(message_body) |
|
|
|
f.body = body |
|
|
|
result = newStmtList() |
|
#type MSG_get_pos = proc(entity:PEntity): TPoint2d {.nimcall.} |
|
result.add newNimNode(nnkTypeSection).add( |
|
newNimNode(nnkTypeDef).add( |
|
ident("MSG_"& name).postfix("*"), |
|
newEmptyNode(), |
|
newNimNode(nnkProcTy).add(f.params, newNimNode(nnkPragma).add(ident"nimcall")))) |
|
result.add f |
|
#some message info is needed at runtime |
|
result.add parseExpr("rt_msgs_multicast.ensureLen("& $(1+m_id) &")") |
|
result.add parseExpr("rt_msgs_multicast["& $m_id & "] = false") |
|
echo repr(result) |
|
|
|
proc msg_is_multicast* (msg:string):bool{.compileTime.} = |
|
messageIDs[msg.normalize].multicast |
|
proc msg_is_unicast* (msg:string): bool {.compileTime.} = |
|
not messageIDs[msg.normalize].multicast |
|
|
|
discard """ proc get_pos : float {.unicast.} |
|
#proc get_pos : string {.unicast.} """ |
|
|
|
#echo messageID("FOO") |
|
|
|
type |
|
TUnicastMsg = tuple[f:pointer, weight:int32] |
|
PComponentInfo* = ref object |
|
id*: int |
|
name*: string |
|
size*: int |
|
initializer*,destructor*: TEntDestructor |
|
unicast_messages: TTable[int, TUnicastMsg] |
|
multicast_messages: TTable[int, pointer] |
|
|
|
requiredComponents,conflictingComponents:seq[int] |
|
|
|
|
|
var |
|
component_id_counter = 0 |
|
all_declared_components*{.global.}: seq[PComponentInfo] = @[] |
|
|
|
proc componentID* (T:typedesc): int |
|
proc componentInfo* (id: int): PComponentInfo = all_declared_components[id] |
|
proc componentInfo* (T:Typedesc): PComponentInfo = componentInfo(componentID(T))#all_declared_components[componentID(T)] |
|
|
|
proc requiresComponent* (a,b: typedesc) = |
|
componentInfo(a).requiredComponents.add(componentID(b)) |
|
proc conflictsWithComponent* (a,b: typedesc) = |
|
componentInfo(a).conflictingComponents.add(componentID(b)) |
|
|
|
proc setInitializer* (comp:typedesc; f:TEntDestructor) = |
|
componentInfo(comp).initializer = f |
|
proc setDestructor* (comp:typedesc; f:TEntDestructor) = |
|
componentInfo(comp).destructor = f |
|
|
|
proc new_component_id (t:typedesc): int = |
|
result = component_id_counter |
|
inc component_id_counter |
|
|
|
let ci = PComponentInfo( |
|
id: result, |
|
name: name(T), |
|
size: sizeof(T), |
|
unicast_messages: initTable[int, TUnicastMsg](), |
|
multicast_messages: initTable[int,pointer](), |
|
required_components: @[], |
|
conflicting_components: @[] |
|
) |
|
|
|
if all_declared_components.len < result+1: |
|
all_declared_components.setLen result+1 |
|
all_declared_components[result] = ci |
|
|
|
echo ci.name, " declared." |
|
|
|
proc componentID* (T:typedesc): int = |
|
let ID {.global.} = new_component_id(T) |
|
return ID |
|
|
|
proc messageID* (m:string): int {.compileTime.} = messageIDs[m.normalize].id |
|
|
|
macro defMsg* : stmt {.immediate.} = |
|
# Implements a message for a component |
|
# |
|
# Arguments: |
|
# component: typedesc, |
|
# message: static[string], |
|
# weight: int = 0, |
|
# function: proc(arg1: TArg1, ..) |
|
# |
|
# Usage: |
|
# def_msg(THealthComponent, "TakeDamage") do (damage: int): |
|
# # `entity: PEntity` is injected in the params |
|
# entity[THealthComponent].hp -= damage |
|
# if entity[THealthComponent].hp < 0: |
|
# entity.die |
|
# |
|
let cs = callsite() |
|
assert cs.kind == nnkCall |
|
var |
|
component: PNimrodNode |
|
msg: int |
|
weight = 0 |
|
function: PNimrodNode |
|
functionIDX = 3 |
|
|
|
component = cs[1] |
|
let msg_name = ($cs[2]).normalize |
|
msg = messageID(msg_name) |
|
|
|
if cs.len > 4: |
|
weight = cs[3].intval.int |
|
functionIDX = 4 |
|
if functionIDX != cs.len-1: |
|
echo treerepr(cs) |
|
quit "Malformed arguments for defMsg "& $functionidx & ", "& $cs.len |
|
if cs[functionIDX].kind == nnkStmtList: |
|
function = newProc(procType = nnkLambda, body = cs[functionIDX]) |
|
else: |
|
let f = cs[functionIDX].copyNimTree |
|
function = newNimNode(nnkLambda) |
|
f.copyChildrenTo function |
|
|
|
# inject self:PEntity |
|
function.params.insert(1, newIdentDefs("entity".ident, "PEntity".ident)) |
|
|
|
let blck_stmt = newStmtList() |
|
blck_stmt.add parseStmt("let cfunc: `MSG $1` = $2".format(msg_name, repr(function))) |
|
if messageIDs[msg_name].multicast: |
|
blck_stmt.add parseStmt("componentInfo($1).multicast_messages[$2] = cast[pointer](cfunc)".format( |
|
component.repr, msg)) |
|
else: |
|
blck_stmt.add parseStmt("componentInfo($1).unicast_messages[$2] = (cast[pointer](cfunc), $3'i32)".format( |
|
component.repr, msg, weight)) |
|
result = newBlockStmt(blck_stmt) |
|
|
|
when defined(debug): |
|
echo "defMsg($1,$2) result:".format(component.repr, msg_name) |
|
result.repr.echo |
|
|
|
template msgImpl*(component, msg_name, func): stmt {.immediate.} = |
|
block: |
|
let cfunc: `MSG msg_name` = func |
|
const msg_id = messageID(astToStr(msg_name)) |
|
echo "Declaring (",msg_id," ",astToStr(msg_name), ") for ", componentInfo(component).name |
|
when msg_is_unicast(astToStr(msg_name)): |
|
componentInfo(component).unicast_messages[msg_id] = (cast[pointer](cfunc),0'i32) |
|
else: |
|
componentInfo(component).multicast_messages[msg_id] = cast[pointer](cfunc) |
|
|
|
template dump_components* : stmt {.immediate.} = |
|
echo all_declared_components.len, " declared components:" |
|
for comp in all_declared_components: |
|
echo " #", comp.id, ": ", comp.name |
|
|
|
|
|
type |
|
TComponentSet* = seq[int] |
|
|
|
proc componentIDs* (components: varargs[int,`componentID`]): TComponentSet = |
|
@components |
|
proc isValid* (ty:PTypeinfo): bool = |
|
not ty.errors.has |
|
|
|
proc newSeq* [T] (size: int; default: T): seq[T] = |
|
newSeq result, size |
|
for i in 0 .. < size: |
|
result[i] = default |
|
|
|
proc newTypeinfo* (components: TComponentSet): PTypeinfo = |
|
# aggregates components so an entity can be instantiated. |
|
# each component has initializers, destructors and responds |
|
# to messages, PTypeinfo just collects them together. |
|
# resulting type may not be valid, check with isValid(ty) |
|
result = PTypeinfo() |
|
|
|
let n_my_comps = components.len |
|
if n_my_comps == 0: |
|
result.errors = just(@["Entity has 0 components"]) |
|
return |
|
|
|
result.initializers.newSeq 0 |
|
result.destructors.newSeq 0 |
|
result.errors.val.newSeq 0 |
|
|
|
let n_comps = all_declared_components.len |
|
let n_msgs = rt_msgs_multicast.len #m_id_counter |
|
echo "n_msgs: ", n_msgs |
|
result.messages.newSeq n_msgs |
|
var unicast_weights = newSeq[int](n_msgs, -1) |
|
result.offsets.newSeq n_comps |
|
result.components.newSeq n_comps |
|
|
|
# calculate offset for component data |
|
var offset = 0 |
|
var requiredComps, conflictComps: seq[int] |
|
requiredComps.newseq 0 |
|
conflictComps.newseq 0 |
|
|
|
for c_id in components: |
|
let c = componentInfo(c_id) |
|
result.offsets[c_id] = offset |
|
result.components[c_id] = true |
|
inc offset, c.size |
|
|
|
# collect initializers and destructors |
|
if not c.initializer.isNil: result.initializers.add c.initializer |
|
if not c.destructor.isNil: result.destructors.add c.destructor |
|
|
|
requiredComps.add c.requiredComponents |
|
conflictComps.add c.conflictingComponents |
|
|
|
for id, f in c.unicast_messages.pairs: |
|
template this_message: expr = result.messages[id] |
|
|
|
if this_message.kind == mNope or f.weight > unicast_weights[id]: |
|
this_message = RT_Message(kind: mUnicast, f: f.f) |
|
unicast_weights[id] = f.weight |
|
|
|
for id, f in c.multicast_messages.pairs: |
|
if result.messages[id].kind == mNope: |
|
result.messages[id] = RT_Message(kind: mMulticast, fs: @[f]) |
|
elif result.messages[id].kind == mMulticast: |
|
result.messages[id].fs.add f |
|
else: |
|
echo "multicast id class ", id, " with ", result.messages[id] |
|
|
|
# offset ends up being the size we need to allocate for the datas |
|
result.size = offset |
|
|
|
requiredComps = distnct(requiredComps) |
|
requiredComps.sort cmp[int] |
|
for c_id in requiredComps: |
|
if not result.components[c_id]: |
|
result.errors.val.add "Component $# is required" % componentInfo(c_id).name |
|
|
|
conflictComps = distnct(conflictComps) |
|
conflictComps.sort cmp[int] |
|
for c_id in conflictComps: |
|
if result.components[c_id]: |
|
result.errors.val.add "Component $# is in conflict" % componentInfo(c_id).name |
|
|
|
result.errors.has = result.errors.val.len > 0 |
|
|
|
proc allocate* (ty:PTypeinfo): cstring = cast[cstring](alloc0(ty.size)) |
|
|
|
proc initialize* (entity:PEntity) = |
|
for f in entity.ty.initializers: f(entity) |
|
proc destroy* (entity:PEntity) = |
|
# run destructors and dealloc entity data |
|
if entity.data.isNil: return |
|
for f in entity.ty.destructors: f(entity) |
|
dealloc(entity.data) |
|
entity.data = nil |
|
|
|
type |
|
RBadEntity* = ref EBadEntity |
|
EBadEntity* = object of EBase |
|
errors*: TMaybe[seq[string]] |
|
|
|
proc instantiate* (ty:PTypeinfo, initialize = true): PEntity = |
|
# allocates and initializes a typeinfo |
|
if ty.errors.has: |
|
var x = newException(EBadEntity, "typeinfo has errors").RBadEntity |
|
x.errors = just(ty.errors.val) |
|
raise x |
|
|
|
let dat = ty.allocate |
|
if dat.isNil: |
|
raise newException(EBadEntity, "failed to allocate memory") |
|
|
|
new(result) do (ent:PEntity): |
|
ent.destroy |
|
result.ty = ty |
|
result.data = dat |
|
if initialize: |
|
result.initialize |
|
|
|
|
|
|
|
|
|
type |
|
PDomain* = ref object |
|
# Domain is a cache of entity types |
|
types*: TTable[TComponentSet, PTypeinfo] |
|
|
|
proc newDomain* : PDomain = |
|
PDomain(types: initTable[TComponentSet,PTypeinfo]()) |
|
|
|
proc getTypeinfo* (dom:PDomain; components:varargs[int, `componentID`]): PTypeinfo = |
|
var components = @components |
|
components.sort cmp[int] |
|
result = dom.types[components] |
|
if result.isNil: |
|
result = newTypeinfo(components) |
|
dom.types[components] = result |
|
|
|
proc newEntity* (dom:PDomain; components:varargs[int,`componentID`], initialize=true): PEntity = |
|
dom.getTypeinfo(components).instantiate(initialize) |
|
|
|
proc get* (entity: PEntity; ty: typedesc): var ty {.inline.} = |
|
# access the data for a component |
|
when true: |
|
cast[var ty](entity.data[entity.ty.offsets[componentID(ty)]].addr) |
|
else: |
|
cast[var ty](entity.data[entity.ty.components[componentID(ty)].val.offset].addr) |
|
|
|
proc `[]`* (ent:PEntity; ty:typedesc):var ty {.inline.}= ent.get(ty) |
|
# shortcut for `get(ent,ty)` |
|
proc `[]=`* (ent:PEntity; ty:typedesc; val:ty) {.inline.} = ent.get(ty) = val |
|
# shortcut for `get(ent,ty) = val` |
|
|
|
|
|
|
|
## all the testing is done in another module |
|
## this all has to work from outside this module |
|
|