/* Handle-based map using fixed arrays. By Karl Zylinski (karl@zylinski.se) The Handle_Map maps a handle to an item. A handle consists of an index and a generation. The item can be any type. Such a handle can be stored as a permanent reference, where you'd usually store a pointer. The benefit of handles is that you know if some other system has destroyed the object at that index, since the generation will then differ. This implementation uses fixed arrays and therefore involves no dynamic memory allocations. Example (assumes this package is imported under the alias `hm`): Entity_Handle :: hm.Handle Entity :: struct { // All items must contain a handle handle: Entity_Handle, pos: [2]f32, } // Note: We use `1024`, if you use a bigger number within a proc you may // blow the stack. In those cases: Store the array inside a global variable // or a dynamically allocated struct. entities: hm.Handle_Map(Entity, Entity_Handle, 1024) h1 := hm.add(&entities, Entity { pos = { 5, 7 } }) h2 := hm.add(&entities, Entity { pos = { 10, 5 } }) // Resolve handle -> pointer if h2e := hm.get(&entities, h2); h2e != nil { h2e.pos.y = 123 } // Will remove this entity, leaving an unused slot hm.remove(&entities, h1) // Will reuse the slot h1 used h3 := hm.add(&entities, Entity { pos = { 1, 2 } }) // Iterate. You can also use `for e in hm.items {}` and skip any item where // `e.handle.idx == 0`. The iterator does that automatically. There's also // `skip` procedure in this package that check `e.handle.idx == 0` for you. ent_iter := hm.make_iter(&entities) for e, h in hm.iter(&ent_iter) { e.pos += { 5, 1 } } */ package handle_map_fixed import "base:intrinsics" // Returned from the `add` proc. Store these as permanent references to items in // the handle map. You can resolve the handle to a pointer using the `get` proc. Handle :: struct { // index into `items` array of the `Handle_Map` struct. idx: u32, // When using the `get` proc, this will be matched to the `gen` on the item // in the handle map. The handle is only valid if they match. If they don't // match, then it means that the slot in the handle map has been reused. gen: u32, } Handle_Map :: struct($T: typeid, $HT: typeid, $N: int) { // Each item must have a field `handle` of type `HT`. // // There's always a "dummy element" at index 0. This way, a Handle with // `idx == 0` means "no Handle". This means that you actually have `N - 1` // items available. items: [N]T, // How much of `items` that is in use. num_items: u32, // The index of the first unused element in `items`. At this index in // `unused_items` you'll find the next-next unused index. Used by `add` and // originally set by `remove`. next_unused: u32, // An element in this array that is non-zero means the thing at that index // in `items` is unused. The non-zero number is the index of the next unused // item. This forms a linked series of indices. The series starts with // `next_unused`. unused_items: [N]u32, // Only used for making it possible to quickly calculate the number of valid // elements. num_unused: u32, } // Clears the handle map using `mem_zero`. It doesn't do `m^ = {}` because that // may blow the stack for a handle map with very big `N` clear :: proc(m: ^Handle_Map($T, $HT, $N)) { intrinsics.mem_zero(m, size_of(m^)) } // Add a value of type `T` to the handle map. Returns a handle you can use as a // permanent reference. // // Will reuse the item at `next_unused` if that value is non-zero. // // Second return value is `false` if the handle-based map is full. add :: proc(m: ^Handle_Map($T, $HT, $N), v: T) -> (HT, bool) #optional_ok { v := v if m.next_unused != 0 { idx := m.next_unused item := &m.items[idx] m.next_unused = m.unused_items[idx] m.unused_items[idx] = 0 gen := item.handle.gen item^ = v item.handle.idx = u32(idx) item.handle.gen = gen + 1 m.num_unused -= 1 return item.handle, true } // We always have a "dummy item" at index zero. This is because handle.idx // being zero means "no item", so we can't use that slot for anything. if m.num_items == 0 { m.items[0] = {} m.num_items += 1 } if m.num_items == len(m.items) { return {}, false } item := &m.items[m.num_items] item^ = v item.handle.idx = u32(m.num_items) item.handle.gen = 1 m.num_items += 1 return item.handle, true } // Resolve a handle to a pointer of type `^T`. The pointer is stable since the // handle map uses a fixed array. But you should _not_ store the pointer // permanently. The item may get reused if any part of your program destroys and // reuses that slot. Only store handles permanently and temporarily resolve them // into pointers as needed. get :: proc(m: ^Handle_Map($T, $HT, $N), h: HT) -> ^T { if h.idx <= 0 || h.idx >= m.num_items { return nil } if item := &m.items[h.idx]; item.handle == h { return item } return nil } // Remove an item from the handle map. You choose which item by passing a handle // to this proc. The item is not really destroyed, rather its index is just // set on `m.next_unused`. Also, the item's `handle.idx` is set to zero, this // is used by the `iter` proc in order to skip that item when iterating. remove :: proc(m: ^Handle_Map($T, $HT, $N), h: HT) { if h.idx <= 0 || h.idx >= m.num_items { return } if item := &m.items[h.idx]; item.handle == h { m.unused_items[h.idx] = m.next_unused m.next_unused = h.idx m.num_unused += 1 item.handle.idx = 0 } } // Tells you if a handle maps to a valid item. This is done by checking if the // handle on the item is the same as the passed handle. valid :: proc(m: Handle_Map($T, $HT, $N), h: HT) -> bool { return h.idx > 0 && h.idx < m.num_items && m.items[h.idx].handle == h } // Tells you how many valid items there are in the handle map. num_used :: proc(m: Handle_Map($T, $HT, $N)) -> int { return int(m.num_items - m.num_unused) } // The maximum number of items the handle map can contain. cap :: proc(m: Handle_Map($T, $HT, $N)) -> int { return N } // For iterating a handle map. Create using `make_iter`. Handle_Map_Iterator :: struct($T: typeid, $HT: typeid, $N: int) { m: ^Handle_Map(T, HT, N), index: u32, } // Create an iterator. Use with `iter` to do the actual iteration. make_iter :: proc(m: ^Handle_Map($T, $HT, $N)) -> Handle_Map_Iterator(T, HT, N) { return { m = m, index = 1 } } // Iterate over the handle map. Skips unused slots, meaning that it skips slots // with handle.idx == 0. // // Usage: // my_iter := hm.make_iter(&my_handle_map) // for e in hm.iter(&my_iter) {} // // Instead of using an iterator you can also loop over `items` and check if // `item.handle.idx == 0` and in that case skip that item. iter :: proc(it: ^Handle_Map_Iterator($T, $HT, $N)) -> (val: ^T, h: HT, cond: bool) { for _ in it.index.. bool { return e.handle.idx == 0 }