/*
ecsjs is an entity component system library for JavaScript
Copyright (C) 2014 Peter Flannery
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see .
*/
import type { ComponentMap } from './component-map.js';
import {
ComponentClassesMapKey,
type ComponentClass,
type Iterator,
} from './types.js';
import { hashString } from './utils.js';
/**
* Component class map for storing registered component maps
* @category Maps
*/
export class ComponentClassesMap {
private hashIdToName: Map = new Map()
private hashIdToIndex = new Map();
private indexToHashId: number[] = [];
private maps: ComponentMap[] = [];
/**
* Creates a new component classes map
* @param entries - Optional initial data for the map
*/
constructor(entries?: readonly (readonly [number, ComponentMap])[] | null) {
if (entries) {
for (let i = 0; i < entries.length; i++) {
const entry = entries[i];
this.setByHashId(entry[0], entry[1]);
}
}
}
/**
* Returns the number of registered component maps
*/
get size() {
return this.maps.length;
}
/**
* Clears all maps and internal indices while preserving array references
*/
clear(): void {
this.maps.length = 0;
this.indexToHashId.length = 0;
this.hashIdToIndex.clear();
}
/**
* Retrieves a component map using the component class
* @param componentClass - The component class to get the map for
* @returns The {@link ComponentMap} for the specified class, or undefined if not found
*/
get(componentClass: ComponentClass): ComponentMap | undefined {
const index = this.hashIdToIndex.get(componentClass.hashId!);
return index !== undefined ? this.maps[index] : undefined;
}
/**
* Checks if a component class map is already registered
* @param componentClass - The component class to check
* @returns True if the map is registered, otherwise false
*/
has(componentClass: ComponentClass): boolean {
if (componentClass.hashId === undefined) return false;
return this.hashIdToIndex.has(componentClass.hashId);
}
/**
* Registers a component map. Automatically generates a hashId from the class name if missing
* @param componentClass - The component class to register
* @param map - The {@link ComponentMap} instance to store
* @returns The {@link ComponentClassesMap} instance for chaining
*/
set(componentClass: ComponentClass, map: ComponentMap): this {
if (componentClass.hashId === undefined) {
componentClass.hashId = hashString(componentClass.name);
}
this.hashIdToName.set(componentClass.hashId, componentClass.name);
return this.setByHashId(componentClass.hashId!, map);
}
/**
* Maps a specific numeric hash id to a component map instance.
* @param hash - The numeric hash id
* @param map - The {@link ComponentMap} instance to store
* @returns The {@link ComponentClassesMap} instance for chaining
*/
setByHashId(hash: number, map: ComponentMap): this {
const existingIndex = this.hashIdToIndex.get(hash);
if (existingIndex !== undefined) {
this.maps[existingIndex] = map;
} else {
this.hashIdToIndex.set(hash, this.maps.length);
this.indexToHashId.push(hash);
this.maps.push(map);
}
return this;
}
/**
* Removes a component map and re-orders internal storage to maintain density
* @param componentClass - The component class to remove the map for
* @returns True if the map was removed, otherwise false
*/
delete(componentClass: ComponentClass): boolean {
const hashId = componentClass.hashId!;
const index = this.hashIdToIndex.get(hashId);
if (index === undefined) return false;
const lastIndex = this.maps.length - 1;
const lastHash = this.indexToHashId[lastIndex];
// Swap
this.maps[index] = this.maps[lastIndex];
this.indexToHashId[index] = lastHash;
// Update map pointer
this.hashIdToIndex.set(lastHash, index);
// Pop
this.maps.pop();
this.indexToHashId.pop();
this.hashIdToIndex.delete(hashId);
return true;
}
/**
* Executes a callback for every map, providing the instance and its registered name
* @param callback - The callback to execute
*/
forEach(callback: (value: ComponentMap, name: string) => void) {
for (let i = 0; i < this.maps.length; i++) {
callback(this.maps[i], this.hashIdToName.get(this.indexToHashId[i])!);
}
}
/**
* Returns an iterator of all component map instances
* @returns An {@link ArrayIterator} of {@link ComponentMap} instances
*/
values(): ArrayIterator> { return this.maps.values() }
/**
* Returns an iterator of [hashId, componentMap] pairs
* @returns An {@link Iterator} instance
*/
entries(): Iterator<[number, ComponentMap]> {
let index = 0;
const instances = this.maps;
const hashes = this.indexToHashId;
return {
next(): IteratorResult<[number, ComponentMap]> {
if (index < instances.length) {
const value: [number, ComponentMap] = [hashes[index], instances[index]];
index++;
return { value, done: false };
}
return { value: undefined as any, done: true };
},
reset() { index = 0; },
[Symbol.iterator]() { return this; }
};
}
/**
* Called when using JSON.stringify
* @returns An object representation for serialization
*/
toJSON() {
return { [ComponentClassesMapKey]: 1, iterable: [...this.entries()] };
}
}