Index: ps/trunk/binaries/data/mods/public/globalscripts/FSM.js =================================================================== --- ps/trunk/binaries/data/mods/public/globalscripts/FSM.js +++ ps/trunk/binaries/data/mods/public/globalscripts/FSM.js @@ -0,0 +1,376 @@ +// Hierarchical finite state machine implementation. +// +// FSMs are specified as a JS data structure; +// see e.g. UnitAI.js for an example of the syntax. +// +// FSMs are implicitly linked with an external object. +// That object stores all FSM-related state. +// (This means we can serialise FSM-based components as +// plain old JS objects, with no need to serialise the complex +// FSM structure itself or to add custom serialisation code.) + +/** + +FSM API: + +Users define the FSM behaviour like: + +var FsmSpec = { + + // Define some default message handlers: + + "MessageName1": function(msg) { + // This function will be called in response to calls to + // Fsm.ProcessMessage(this, { "type": "MessageName1", "data": msg }); + // + // In this function, 'this' is the component object passed into + // ProcessMessage, so you can access 'this.propertyName' + // and 'this.methodName()' etc. + }, + + "MessageName2": function(msg) { + // Another message handler. + }, + + // Define the behaviour for the 'STATENAME' state: + // Names of states may only contain the characters A-Z + "STATENAME": { + + "MessageName1": function(msg) { + // This overrides the previous MessageName1 that was + // defined earlier, and will be called instead of it + // in response to ProcessMessage. + }, + + // We don't override MessageName2, so the default one + // will be called instead. + + // Define the 'STATENAME.SUBSTATENAME' state: + // (we support arbitrarily-nested hierarchies of states) + "SUBSTATENAME": { + + "MessageName2": function(msg) { + // Override the default MessageName2. + // But we don't override MessageName1, so the one from + // STATENAME will be used instead. + }, + + "enter": function() { + // This is a special function called when transitioning + // into this state, or into a substate of this state. + // + // If it returns true, the transition will be aborted: + // do this if you've called SetNextState inside this enter + // handler, because otherwise the new state transition + // will get mixed up with the previous ongoing one. + // In normal cases, you can return false or nothing. + }, + + "leave": function() { + // Called when transitioning out of this state. + }, + }, + + // Define a new state which is an exact copy of another + // state that is defined elsewhere in this FSM: + "OTHERSUBSTATENAME": "STATENAME.SUBSTATENAME", + } + +} + + +Objects can then make themselves act as an instance of the FSM by running + FsmSpec.Init(this, "STATENAME"); +which will define a few properties on 'this' (with names prefixed "fsm"), +and then they can call the FSM functions on the object like + FsmSpec.SetNextState(this, "STATENAME.SUBSTATENAME"); + +These objects must also define a function property that can be called as + this.FsmStateNameChanged(name); + +(This design aims to avoid storing any per-instance state that cannot be +easily serialized - it only stores state-name strings.) + + */ + +function FSM(spec) +{ + // The (relatively) human-readable FSM specification needs to get + // compiled into a more-efficient-to-execute version. + // + // In particular, message handling should require minimal + // property lookups in the common case (even when the FSM has + // a deeply nested hierarchy), and there should never be any + // string manipulation at run-time. + + this.decompose = { "": [] }; + /* 'decompose' will store: + { + "": [], + "A": ["A"], + "A.B": ["A", "A.B"], + "A.B.C": ["A", "A.B", "A.B.C"], + "A.B.D": ["A", "A.B", "A.B.D"], + ... + }; + This is used when switching between states in different branches + of the hierarchy, to determine the list of sub-states to leave/enter + */ + + this.states = { }; + /* 'states' will store: + { + ... + "A": { + "_name": "A", + "_parent": "", + "_refs": { // local -> global name lookups (for SetNextState) + "B": "A.B", + "B.C": "A.B.C", + "B.D": "A.B.D", + }, + }, + "A.B": { + "_name": "A.B", + "_parent": "A", + "_refs": { + "C": "A.B.C", + "D": "A.B.D", + }, + "MessageType": function(msg) { ... }, + }, + "A.B.C": { + "_name": "A.B.C", + "_parent": "A.B", + "_refs": {}, + "enter": function() { ... }, + "MessageType": function(msg) { ... }, + }, + "A.B.D": { + "_name": "A.B.D", + "_parent": "A.B", + "_refs": {}, + "enter": function() { ... }, + "leave": function() { ... }, + "MessageType": function(msg) { ... }, + }, + ... + } + */ + + function process(fsm, node, path, handlers) + { + // Handle string references to nodes defined elsewhere in the FSM spec + if (typeof node === "string") + { + var refpath = node.split("."); + var refd = spec; + for (var p of refpath) + { + refd = refd[p]; + if (!refd) + { + error("FSM node "+path.join(".")+" referred to non-defined node "+node); + return {}; + } + } + node = refd; + } + + var state = {}; + fsm.states[path.join(".")] = state; + + var newhandlers = {}; + for (var e in handlers) + newhandlers[e] = handlers[e]; + + state._name = path.join("."); + state._parent = path.slice(0, -1).join("."); + state._refs = {}; + + for (var key in node) + { + if (key === "enter" || key === "leave") + { + state[key] = node[key]; + } + else if (key.match(/^[A-Z]+$/)) + { + state._refs[key] = (state._name ? state._name + "." : "") + key; + + // (the rest of this will be handled later once we've grabbed + // all the event handlers) + } + else + { + newhandlers[key] = node[key]; + } + } + + for (var e in newhandlers) + state[e] = newhandlers[e]; + + for (var key in node) + { + if (key.match(/^[A-Z]+$/)) + { + var newpath = path.concat([key]); + + var decomposed = [newpath[0]]; + for (var i = 1; i < newpath.length; ++i) + decomposed.push(decomposed[i-1] + "." + newpath[i]); + fsm.decompose[newpath.join(".")] = decomposed; + + var childstate = process(fsm, node[key], newpath, newhandlers); + + for (var r in childstate._refs) + { + var cname = key + "." + r; + state._refs[cname] = childstate._refs[r]; + } + } + } + + return state; + } + + process(this, spec, [], {}); +} + +FSM.prototype.Init = function(obj, initialState) +{ + this.deferFromState = undefined; + + obj.fsmStateName = ""; + obj.fsmNextState = undefined; + this.SwitchToNextState(obj, initialState); +}; + +FSM.prototype.SetNextState = function(obj, state) +{ + obj.fsmNextState = state; +}; + +FSM.prototype.ProcessMessage = function(obj, msg) +{ +// warn("ProcessMessage(obj, "+uneval(msg)+")"); + + var func = this.states[obj.fsmStateName][msg.type]; + if (!func) + { + error("Tried to process unhandled event '" + msg.type + "' in state '" + obj.fsmStateName + "'"); + return undefined; + } + + var ret = func.apply(obj, [msg]); + + // If func called SetNextState then switch into the new state, + // and continue switching if the new state's 'enter' called SetNextState again + while (obj.fsmNextState) + { + var nextStateName = this.LookupState(obj.fsmStateName, obj.fsmNextState); + obj.fsmNextState = undefined; + + this.SwitchToNextState(obj, nextStateName); + } + + return ret; +}; + +FSM.prototype.DeferMessage = function(obj, msg) +{ + // We need to work out which sub-state we were running the message handler from, + // and then try again in its parent state. + var old = this.deferFromState; + var from; + if (old) // if we're recursively deferring and saved the last used state, use that + from = old; + else // if this is the first defer then we must have last processed the message in the current FSM state + from = obj.fsmStateName; + + // Find and save the parent, for use in recursive defers + this.deferFromState = this.states[from]._parent; + + // Run the function from the parent state + var state = this.states[this.deferFromState]; + var func = state[msg.type]; + if (!func) + error("Failed to defer event '" + msg.type + "' from state '" + obj.fsmStateName + "'"); + func.apply(obj, [msg]); + + // Restore the changes we made + this.deferFromState = old; + + // TODO: if an inherited handler defers, it calls exactly the same handler + // on the parent state, which is probably useless and inefficient + + // NOTE: this will break if two units try to execute AI at the same time; + // as long as AI messages are queue and processed asynchronously it should be fine +}; + +FSM.prototype.LookupState = function(currentStateName, stateName) +{ +// print("LookupState("+currentStateName+", "+stateName+")\n"); + for (var s = currentStateName; s; s = this.states[s]._parent) + if (stateName in this.states[s]._refs) + return this.states[s]._refs[stateName]; + return stateName; +}; + +FSM.prototype.GetCurrentState = function(obj) +{ + return obj.fsmStateName; +}; + +FSM.prototype.SwitchToNextState = function(obj, nextStateName) +{ + var fromState = this.decompose[obj.fsmStateName]; + var toState = this.decompose[nextStateName]; + + if (!toState) + error("Tried to change to non-existent state '" + nextStateName + "'"); + + // Find the set of states in the hierarchy tree to leave then enter, + // to traverse from the old state to the new one. + // If any enter/leave function returns true then abort the process + // (this lets them intercept the transition and start a new transition) + + for (var equalPrefix = 0; fromState[equalPrefix] && fromState[equalPrefix] === toState[equalPrefix]; ++equalPrefix) + { + } + + // If the next-state is the same as the current state, leave/enter up one level so cleanup gets triggered. + if (equalPrefix > 0 && equalPrefix === toState.length) + --equalPrefix; + + for (var i = fromState.length-1; i >= equalPrefix; --i) + { + var leave = this.states[fromState[i]].leave; + if (leave) + { + obj.fsmStateName = fromState[i]; + if (leave.apply(obj)) + { + obj.FsmStateNameChanged(obj.fsmStateName); + return; + } + } + } + + for (var i = equalPrefix; i < toState.length; ++i) + { + var enter = this.states[toState[i]].enter; + if (enter) + { + obj.fsmStateName = toState[i]; + if (enter.apply(obj)) + { + obj.FsmStateNameChanged(obj.fsmStateName); + return; + } + } + } + + obj.fsmStateName = nextStateName; + obj.FsmStateNameChanged(obj.fsmStateName); +}; Index: ps/trunk/binaries/data/mods/public/globalscripts/MultiKeyMap.js =================================================================== --- ps/trunk/binaries/data/mods/public/globalscripts/MultiKeyMap.js +++ ps/trunk/binaries/data/mods/public/globalscripts/MultiKeyMap.js @@ -0,0 +1,223 @@ +// Convenient container abstraction for storing items referenced by a 3-tuple. +// Used by the ModifiersManager to store items by (property Name, entity, item ID). +// Methods starting with an underscore are private to the storage. +// This supports stackable items as it stores count for each 3-tuple. +// It is designed to be as fast as can be for a JS container. +function MultiKeyMap() +{ + this.items = new Map(); + // Keys are referred to as 'primaryKey', 'secondaryKey', 'itemID'. +} + +MultiKeyMap.prototype.Serialize = function() +{ + let ret = []; + for (let primary of this.items.keys()) + { + // Keys of a Map can be arbitrary types whereas objects only support string, so use a list. + let vals = [primary, []]; + ret.push(vals); + for (let secondary of this.items.get(primary).keys()) + vals[1].push([secondary, this.items.get(primary).get(secondary)]); + } + return ret; +}; + +MultiKeyMap.prototype.Deserialize = function(data) +{ + for (let primary in data) + { + this.items.set(data[primary][0], new Map()); + for (let secondary in data[primary][1]) + this.items.get(data[primary][0]).set(data[primary][1][secondary][0], data[primary][1][secondary][1]); + } +}; + +/** + * Add a single item. + * NB: if you add an item with a different value but the same itemID, the original value remains. + * @param item - an object. + * @param itemID - internal ID of this item, for later removal and/or updating + * @param stackable - if stackable, changing the count of items invalides, otherwise not. + * @returns true if the items list changed in such a way that cached values are possibly invalidated. + */ +MultiKeyMap.prototype.AddItem = function(primaryKey, itemID, item, secondaryKey, stackable = false) +{ + if (!this._AddItem(primaryKey, itemID, item, secondaryKey, stackable)) + return false; + + this._OnItemModified(primaryKey, secondaryKey, itemID); + return true; +}; + +/** + * Add items to multiple properties at once (only one item per property) + * @param items - Dictionnary of { primaryKey: item } + * @returns true if the items list changed in such a way that cached values are possibly invalidated. + */ +MultiKeyMap.prototype.AddItems = function(itemID, items, secondaryKey, stackable = false) +{ + let modified = false; + for (let primaryKey in items) + modified = this.AddItem(primaryKey, itemID, items[primaryKey], secondaryKey, stackable) || modified; + return modified; +}; + +/** + * Removes a item on a property. + * @param primaryKey - property to change (e.g. "Health/Max") + * @param itemID - internal ID of the item to remove + * @param secondaryKey - secondaryKey ID + * @returns true if the items list changed in such a way that cached values are possibly invalidated. + */ +MultiKeyMap.prototype.RemoveItem = function(primaryKey, itemID, secondaryKey, stackable = false) +{ + if (!this._RemoveItem(primaryKey, itemID, secondaryKey, stackable)) + return false; + + this._OnItemModified(primaryKey, secondaryKey, itemID); + return true; +}; + +/** + * Removes items with this ID for any property name. + * Naively iterates all property names. + * @returns true if the items list changed in such a way that cached values are possibly invalidated. + */ +MultiKeyMap.prototype.RemoveAllItems = function(itemID, secondaryKey, stackable = false) +{ + let modified = false; + // Map doesn't implement some so use a for-loop here. + for (let primaryKey of this.items.keys()) + modified = this.RemoveItem(primaryKey, itemID, secondaryKey, stackable) || modified; + return modified; +}; + +/** + * @param itemID - internal ID of the item to try and find. + * @returns true if there is at least one item with that itemID + */ +MultiKeyMap.prototype.HasItem = function(primaryKey, itemID, secondaryKey) +{ + // some() returns false for an empty list which is wanted here. + return this._getItems(primaryKey, secondaryKey).some(item => item._ID === itemID); +}; + +/** + * Check if we have a item for any property name. + * Naively iterates all property names. + * @returns true if there is at least one item with that itemID + */ +MultiKeyMap.prototype.HasAnyItem = function(itemID, secondaryKey) +{ + // Map doesn't implement some so use for loops instead. + for (let primaryKey of this.items.keys()) + if (this.HasItem(primaryKey, itemID, secondaryKey)) + return true; + return false; +}; + +/** + * @returns A list of items (references to stored items to avoid copying) + * (these need to be treated as constants to not break the map) + */ +MultiKeyMap.prototype.GetItems = function(primaryKey, secondaryKey) +{ + return this._getItems(primaryKey, secondaryKey); +}; + +/** + * @returns A dictionary of { Property Name: items } for the secondary Key. + * Naively iterates all property names. + */ +MultiKeyMap.prototype.GetAllItems = function(secondaryKey) +{ + let items = {}; + + // Map doesn't implement filter so use a for loop. + for (let primaryKey of this.items.keys()) + { + if (!this.items.get(primaryKey).has(secondaryKey)) + continue; + items[primaryKey] = this.GetItems(primaryKey, secondaryKey); + } + return items; +}; + +/** + * @returns a list of items. + * This does not necessarily return a reference to items' list, use _getItemsOrInit for that. + */ +MultiKeyMap.prototype._getItems = function(primaryKey, secondaryKey) +{ + let cache = this.items.get(primaryKey); + if (cache) + cache = cache.get(secondaryKey); + return cache ? cache : []; +}; + +/** + * @returns a reference to the list of items for that property name and secondaryKey. + */ +MultiKeyMap.prototype._getItemsOrInit = function(primaryKey, secondaryKey) +{ + let cache = this.items.get(primaryKey); + if (!cache) + cache = this.items.set(primaryKey, new Map()).get(primaryKey); + + let cache2 = cache.get(secondaryKey); + if (!cache2) + cache2 = cache.set(secondaryKey, []).get(secondaryKey); + return cache2; +}; + +/** + * @returns true if the items list changed in such a way that cached values are possibly invalidated. + */ +MultiKeyMap.prototype._AddItem = function(primaryKey, itemID, item, secondaryKey, stackable) +{ + let items = this._getItemsOrInit(primaryKey, secondaryKey); + for (let it of items) + if (it._ID == itemID) + { + it._count++; + return stackable; + } + items.push({ "_ID": itemID, "_count": 1, "value": item }); + return true; +}; + +/** + * @returns true if the items list changed in such a way that cached values are possibly invalidated. + */ +MultiKeyMap.prototype._RemoveItem = function(primaryKey, itemID, secondaryKey, stackable) +{ + let items = this._getItems(primaryKey, secondaryKey); + + let existingItem = items.filter(item => { return item._ID == itemID; }); + if (!existingItem.length) + return false; + + if (--existingItem[0]._count > 0) + return stackable; + + let stilValidItems = items.filter(item => item._count > 0); + + // Delete entries from the map if necessary to clean up. + if (!stilValidItems.length) + { + this.items.get(primaryKey).delete(secondaryKey); + if (!this.items.get(primaryKey).size) + this.items.delete(primaryKey); + return true; + } + + this.items.get(primaryKey).set(secondaryKey, stilValidItems); + + return true; +}; + +/** + * Stub method, to overload. + */ +MultiKeyMap.prototype._OnItemModified = function(primaryKey, secondaryKey, itemID) {}; Index: ps/trunk/binaries/data/mods/public/globalscripts/WeightedList.js =================================================================== --- ps/trunk/binaries/data/mods/public/globalscripts/WeightedList.js +++ ps/trunk/binaries/data/mods/public/globalscripts/WeightedList.js @@ -0,0 +1,37 @@ +function WeightedList() +{ + this.elements = new Map(); + this.totalWeight = 0; +}; + +WeightedList.prototype.length = function() +{ + return this.elements.size; +}; + +WeightedList.prototype.push = function(item, weight = 1) +{ + this.elements.set(item, weight); + this.totalWeight += weight; +}; + +WeightedList.prototype.remove = function(item) +{ + const weight = this.elements.get(item); + if (weight) + this.totalWeight -= weight; + this.elements.delete(item); +}; + +WeightedList.prototype.randomItem = function() +{ + const targetWeight = randFloat(0, this.totalWeight); + let cumulativeWeight = 0; + for (let [item, weight] of this.elements) + { + cumulativeWeight += weight; + if (cumulativeWeight >= targetWeight) + return item; + } + return undefined; +}; Index: ps/trunk/binaries/data/mods/public/globalscripts/tests/test_MultiKeyMap.js =================================================================== --- ps/trunk/binaries/data/mods/public/globalscripts/tests/test_MultiKeyMap.js +++ ps/trunk/binaries/data/mods/public/globalscripts/tests/test_MultiKeyMap.js @@ -0,0 +1,122 @@ +function setup_keys(map) +{ + map.AddItem("prim_a", "item_a", null, "sec_a"); + map.AddItem("prim_a", "item_b", null, "sec_a"); + map.AddItem("prim_a", "item_c", null, "sec_a"); + map.AddItem("prim_a", "item_a", null, "sec_b"); + map.AddItem("prim_b", "item_a", null, "sec_a"); + map.AddItem("prim_c", "item_a", null, "sec_a"); + map.AddItem("prim_c", "item_a", null, 5); +} + +// Check that key-related operations are correct. +function test_keys(map) +{ + TS_ASSERT(map.items.has("prim_a")); + TS_ASSERT(map.items.has("prim_b")); + TS_ASSERT(map.items.has("prim_c")); + + TS_ASSERT(map.items.get("prim_a").has("sec_a")); + TS_ASSERT(map.items.get("prim_a").has("sec_b")); + TS_ASSERT(!map.items.get("prim_a").has("sec_c")); + TS_ASSERT(map.items.get("prim_b").has("sec_a")); + TS_ASSERT(map.items.get("prim_c").has("sec_a")); + TS_ASSERT(map.items.get("prim_c").has(5)); + + TS_ASSERT(map.items.get("prim_a").get("sec_a").length == 3); + TS_ASSERT(map.items.get("prim_a").get("sec_b").length == 1); + TS_ASSERT(map.items.get("prim_b").get("sec_a").length == 1); + TS_ASSERT(map.items.get("prim_c").get("sec_a").length == 1); + TS_ASSERT(map.items.get("prim_c").get(5).length == 1); + + TS_ASSERT(map.GetItems("prim_a", "sec_a").length == 3); + TS_ASSERT(map.GetItems("prim_a", "sec_b").length == 1); + TS_ASSERT(map.GetItems("prim_b", "sec_a").length == 1); + TS_ASSERT(map.GetItems("prim_c", "sec_a").length == 1); + TS_ASSERT(map.GetItems("prim_c", 5).length == 1); + + TS_ASSERT(map.HasItem("prim_a", "item_a", "sec_a")); + TS_ASSERT(map.HasItem("prim_a", "item_b", "sec_a")); + TS_ASSERT(map.HasItem("prim_a", "item_c", "sec_a")); + TS_ASSERT(!map.HasItem("prim_a", "item_d", "sec_a")); + TS_ASSERT(map.HasItem("prim_a", "item_a", "sec_b")); + TS_ASSERT(!map.HasItem("prim_a", "item_b", "sec_b")); + TS_ASSERT(!map.HasItem("prim_a", "item_c", "sec_b")); + TS_ASSERT(map.HasItem("prim_b", "item_a", "sec_a")); + TS_ASSERT(map.HasItem("prim_c", "item_a", "sec_a")); + TS_ASSERT(map.HasAnyItem("item_a", "sec_b")); + TS_ASSERT(map.HasAnyItem("item_b", "sec_a")); + TS_ASSERT(!map.HasAnyItem("item_d", "sec_a")); + TS_ASSERT(!map.HasAnyItem("item_b", "sec_b")); + + // Adding the same item increases its count. + map.AddItem("prim_a", "item_b", 0, "sec_a"); + TS_ASSERT_EQUALS(map.items.get("prim_a").get("sec_a").length, 3); + TS_ASSERT_EQUALS(map.items.get("prim_a").get("sec_a").filter(item => item._ID == "item_b")[0]._count, 2); + TS_ASSERT_EQUALS(map.GetItems("prim_a", "sec_a").length, 3); + + // Adding without stackable doesn't invalidate caches, adding with does. + TS_ASSERT(!map.AddItem("prim_a", "item_b", 0, "sec_a")); + TS_ASSERT(map.AddItem("prim_a", "item_b", 0, "sec_a", true)); + + TS_ASSERT(map.items.get("prim_a").get("sec_a").filter(item => item._ID == "item_b")[0]._count == 4); + + // Likewise removing, unless we now reach 0 + TS_ASSERT(!map.RemoveItem("prim_a", "item_b", "sec_a")); + TS_ASSERT(map.RemoveItem("prim_a", "item_b", "sec_a", true)); + TS_ASSERT(!map.RemoveItem("prim_a", "item_b", "sec_a")); + TS_ASSERT(map.RemoveItem("prim_a", "item_b", "sec_a")); + + // Check that cleanup is done + TS_ASSERT(map.items.get("prim_a").get("sec_a").length == 2); + TS_ASSERT(map.RemoveItem("prim_a", "item_a", "sec_a")); + TS_ASSERT(map.RemoveItem("prim_a", "item_c", "sec_a")); + TS_ASSERT(!map.items.get("prim_a").has("sec_a")); + TS_ASSERT(map.items.get("prim_a").has("sec_b")); + TS_ASSERT(map.RemoveItem("prim_a", "item_a", "sec_b")); + TS_ASSERT(!map.items.has("prim_a")); +} + +function setup_items(map) +{ + map.AddItem("prim_a", "item_a", { "value": 1 }, "sec_a"); + map.AddItem("prim_a", "item_b", { "value": 2 }, "sec_a"); + map.AddItem("prim_a", "item_c", { "value": 3 }, "sec_a"); + map.AddItem("prim_a", "item_c", { "value": 1000 }, "sec_a"); + map.AddItem("prim_a", "item_a", { "value": 5 }, "sec_b"); + map.AddItem("prim_b", "item_a", { "value": 6 }, "sec_a"); + map.AddItem("prim_c", "item_a", { "value": 7 }, "sec_a"); +} + +// Check that items returned are correct. +function test_items(map) +{ + let items = map.GetAllItems("sec_a"); + TS_ASSERT("prim_a" in items); + TS_ASSERT("prim_b" in items); + TS_ASSERT("prim_c" in items); + let sum = 0; + for (let key in items) + items[key].forEach(item => { sum += item.value.value * item._count; }); + TS_ASSERT(sum == 22); +} + +// Test items, and test that deserialised versions still pass test (i.e. test serialisation). +let map = new MultiKeyMap(); +setup_keys(map); +test_keys(map); + +map = new MultiKeyMap(); +let map2 = new MultiKeyMap(); +setup_keys(map); +map2.Deserialize(map.Serialize()); +test_keys(map2); + +map = new MultiKeyMap(); +setup_items(map); +test_items(map); +map = new MultiKeyMap(); +map2 = new MultiKeyMap(); +setup_items(map); +map2.Deserialize(map.Serialize()); +test_items(map2); Index: ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Auras.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Auras.js +++ ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Auras.js @@ -1,4 +1,3 @@ -Engine.LoadHelperScript("MultiKeyMap.js"); Engine.LoadHelperScript("Player.js"); Engine.LoadHelperScript("ValueModification.js"); Engine.LoadComponentScript("interfaces/Auras.js"); Index: ps/trunk/binaries/data/mods/public/simulation/components/tests/test_ModifiersManager.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/tests/test_ModifiersManager.js +++ ps/trunk/binaries/data/mods/public/simulation/components/tests/test_ModifiersManager.js @@ -1,6 +1,5 @@ Engine.LoadComponentScript("interfaces/ModifiersManager.js"); Engine.LoadComponentScript("ModifiersManager.js"); -Engine.LoadHelperScript("MultiKeyMap.js"); Engine.LoadHelperScript("Player.js"); Engine.LoadHelperScript("ValueModification.js"); Index: ps/trunk/binaries/data/mods/public/simulation/components/tests/test_StatusEffectsReceiver.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/tests/test_StatusEffectsReceiver.js +++ ps/trunk/binaries/data/mods/public/simulation/components/tests/test_StatusEffectsReceiver.js @@ -1,4 +1,3 @@ -Engine.LoadHelperScript("MultiKeyMap.js"); Engine.LoadHelperScript("Player.js"); Engine.LoadHelperScript("ValueModification.js"); Engine.LoadComponentScript("interfaces/Health.js"); Index: ps/trunk/binaries/data/mods/public/simulation/components/tests/test_UnitAI.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/tests/test_UnitAI.js +++ ps/trunk/binaries/data/mods/public/simulation/components/tests/test_UnitAI.js @@ -1,4 +1,3 @@ -Engine.LoadHelperScript("FSM.js"); Engine.LoadHelperScript("Player.js"); Engine.LoadHelperScript("Position.js"); Engine.LoadHelperScript("Sound.js"); Index: ps/trunk/binaries/data/mods/public/simulation/helpers/FSM.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/helpers/FSM.js +++ ps/trunk/binaries/data/mods/public/simulation/helpers/FSM.js @@ -1,378 +0,0 @@ -// Hierarchical finite state machine implementation. -// -// FSMs are specified as a JS data structure; -// see e.g. UnitAI.js for an example of the syntax. -// -// FSMs are implicitly linked with an external object. -// That object stores all FSM-related state. -// (This means we can serialise FSM-based components as -// plain old JS objects, with no need to serialise the complex -// FSM structure itself or to add custom serialisation code.) - -/** - -FSM API: - -Users define the FSM behaviour like: - -var FsmSpec = { - - // Define some default message handlers: - - "MessageName1": function(msg) { - // This function will be called in response to calls to - // Fsm.ProcessMessage(this, { "type": "MessageName1", "data": msg }); - // - // In this function, 'this' is the component object passed into - // ProcessMessage, so you can access 'this.propertyName' - // and 'this.methodName()' etc. - }, - - "MessageName2": function(msg) { - // Another message handler. - }, - - // Define the behaviour for the 'STATENAME' state: - // Names of states may only contain the characters A-Z - "STATENAME": { - - "MessageName1": function(msg) { - // This overrides the previous MessageName1 that was - // defined earlier, and will be called instead of it - // in response to ProcessMessage. - }, - - // We don't override MessageName2, so the default one - // will be called instead. - - // Define the 'STATENAME.SUBSTATENAME' state: - // (we support arbitrarily-nested hierarchies of states) - "SUBSTATENAME": { - - "MessageName2": function(msg) { - // Override the default MessageName2. - // But we don't override MessageName1, so the one from - // STATENAME will be used instead. - }, - - "enter": function() { - // This is a special function called when transitioning - // into this state, or into a substate of this state. - // - // If it returns true, the transition will be aborted: - // do this if you've called SetNextState inside this enter - // handler, because otherwise the new state transition - // will get mixed up with the previous ongoing one. - // In normal cases, you can return false or nothing. - }, - - "leave": function() { - // Called when transitioning out of this state. - }, - }, - - // Define a new state which is an exact copy of another - // state that is defined elsewhere in this FSM: - "OTHERSUBSTATENAME": "STATENAME.SUBSTATENAME", - } - -} - - -Objects can then make themselves act as an instance of the FSM by running - FsmSpec.Init(this, "STATENAME"); -which will define a few properties on 'this' (with names prefixed "fsm"), -and then they can call the FSM functions on the object like - FsmSpec.SetNextState(this, "STATENAME.SUBSTATENAME"); - -These objects must also define a function property that can be called as - this.FsmStateNameChanged(name); - -(This design aims to avoid storing any per-instance state that cannot be -easily serialized - it only stores state-name strings.) - - */ - -function FSM(spec) -{ - // The (relatively) human-readable FSM specification needs to get - // compiled into a more-efficient-to-execute version. - // - // In particular, message handling should require minimal - // property lookups in the common case (even when the FSM has - // a deeply nested hierarchy), and there should never be any - // string manipulation at run-time. - - this.decompose = { "": [] }; - /* 'decompose' will store: - { - "": [], - "A": ["A"], - "A.B": ["A", "A.B"], - "A.B.C": ["A", "A.B", "A.B.C"], - "A.B.D": ["A", "A.B", "A.B.D"], - ... - }; - This is used when switching between states in different branches - of the hierarchy, to determine the list of sub-states to leave/enter - */ - - this.states = { }; - /* 'states' will store: - { - ... - "A": { - "_name": "A", - "_parent": "", - "_refs": { // local -> global name lookups (for SetNextState) - "B": "A.B", - "B.C": "A.B.C", - "B.D": "A.B.D", - }, - }, - "A.B": { - "_name": "A.B", - "_parent": "A", - "_refs": { - "C": "A.B.C", - "D": "A.B.D", - }, - "MessageType": function(msg) { ... }, - }, - "A.B.C": { - "_name": "A.B.C", - "_parent": "A.B", - "_refs": {}, - "enter": function() { ... }, - "MessageType": function(msg) { ... }, - }, - "A.B.D": { - "_name": "A.B.D", - "_parent": "A.B", - "_refs": {}, - "enter": function() { ... }, - "leave": function() { ... }, - "MessageType": function(msg) { ... }, - }, - ... - } - */ - - function process(fsm, node, path, handlers) - { - // Handle string references to nodes defined elsewhere in the FSM spec - if (typeof node === "string") - { - var refpath = node.split("."); - var refd = spec; - for (var p of refpath) - { - refd = refd[p]; - if (!refd) - { - error("FSM node "+path.join(".")+" referred to non-defined node "+node); - return {}; - } - } - node = refd; - } - - var state = {}; - fsm.states[path.join(".")] = state; - - var newhandlers = {}; - for (var e in handlers) - newhandlers[e] = handlers[e]; - - state._name = path.join("."); - state._parent = path.slice(0, -1).join("."); - state._refs = {}; - - for (var key in node) - { - if (key === "enter" || key === "leave") - { - state[key] = node[key]; - } - else if (key.match(/^[A-Z]+$/)) - { - state._refs[key] = (state._name ? state._name + "." : "") + key; - - // (the rest of this will be handled later once we've grabbed - // all the event handlers) - } - else - { - newhandlers[key] = node[key]; - } - } - - for (var e in newhandlers) - state[e] = newhandlers[e]; - - for (var key in node) - { - if (key.match(/^[A-Z]+$/)) - { - var newpath = path.concat([key]); - - var decomposed = [newpath[0]]; - for (var i = 1; i < newpath.length; ++i) - decomposed.push(decomposed[i-1] + "." + newpath[i]); - fsm.decompose[newpath.join(".")] = decomposed; - - var childstate = process(fsm, node[key], newpath, newhandlers); - - for (var r in childstate._refs) - { - var cname = key + "." + r; - state._refs[cname] = childstate._refs[r]; - } - } - } - - return state; - } - - process(this, spec, [], {}); -} - -FSM.prototype.Init = function(obj, initialState) -{ - this.deferFromState = undefined; - - obj.fsmStateName = ""; - obj.fsmNextState = undefined; - this.SwitchToNextState(obj, initialState); -}; - -FSM.prototype.SetNextState = function(obj, state) -{ - obj.fsmNextState = state; -}; - -FSM.prototype.ProcessMessage = function(obj, msg) -{ -// warn("ProcessMessage(obj, "+uneval(msg)+")"); - - var func = this.states[obj.fsmStateName][msg.type]; - if (!func) - { - error("Tried to process unhandled event '" + msg.type + "' in state '" + obj.fsmStateName + "'"); - return undefined; - } - - var ret = func.apply(obj, [msg]); - - // If func called SetNextState then switch into the new state, - // and continue switching if the new state's 'enter' called SetNextState again - while (obj.fsmNextState) - { - var nextStateName = this.LookupState(obj.fsmStateName, obj.fsmNextState); - obj.fsmNextState = undefined; - - this.SwitchToNextState(obj, nextStateName); - } - - return ret; -}; - -FSM.prototype.DeferMessage = function(obj, msg) -{ - // We need to work out which sub-state we were running the message handler from, - // and then try again in its parent state. - var old = this.deferFromState; - var from; - if (old) // if we're recursively deferring and saved the last used state, use that - from = old; - else // if this is the first defer then we must have last processed the message in the current FSM state - from = obj.fsmStateName; - - // Find and save the parent, for use in recursive defers - this.deferFromState = this.states[from]._parent; - - // Run the function from the parent state - var state = this.states[this.deferFromState]; - var func = state[msg.type]; - if (!func) - error("Failed to defer event '" + msg.type + "' from state '" + obj.fsmStateName + "'"); - func.apply(obj, [msg]); - - // Restore the changes we made - this.deferFromState = old; - - // TODO: if an inherited handler defers, it calls exactly the same handler - // on the parent state, which is probably useless and inefficient - - // NOTE: this will break if two units try to execute AI at the same time; - // as long as AI messages are queue and processed asynchronously it should be fine -}; - -FSM.prototype.LookupState = function(currentStateName, stateName) -{ -// print("LookupState("+currentStateName+", "+stateName+")\n"); - for (var s = currentStateName; s; s = this.states[s]._parent) - if (stateName in this.states[s]._refs) - return this.states[s]._refs[stateName]; - return stateName; -}; - -FSM.prototype.GetCurrentState = function(obj) -{ - return obj.fsmStateName; -}; - -FSM.prototype.SwitchToNextState = function(obj, nextStateName) -{ - var fromState = this.decompose[obj.fsmStateName]; - var toState = this.decompose[nextStateName]; - - if (!toState) - error("Tried to change to non-existent state '" + nextStateName + "'"); - - // Find the set of states in the hierarchy tree to leave then enter, - // to traverse from the old state to the new one. - // If any enter/leave function returns true then abort the process - // (this lets them intercept the transition and start a new transition) - - for (var equalPrefix = 0; fromState[equalPrefix] && fromState[equalPrefix] === toState[equalPrefix]; ++equalPrefix) - { - } - - // If the next-state is the same as the current state, leave/enter up one level so cleanup gets triggered. - if (equalPrefix > 0 && equalPrefix === toState.length) - --equalPrefix; - - for (var i = fromState.length-1; i >= equalPrefix; --i) - { - var leave = this.states[fromState[i]].leave; - if (leave) - { - obj.fsmStateName = fromState[i]; - if (leave.apply(obj)) - { - obj.FsmStateNameChanged(obj.fsmStateName); - return; - } - } - } - - for (var i = equalPrefix; i < toState.length; ++i) - { - var enter = this.states[toState[i]].enter; - if (enter) - { - obj.fsmStateName = toState[i]; - if (enter.apply(obj)) - { - obj.FsmStateNameChanged(obj.fsmStateName); - return; - } - } - } - - obj.fsmStateName = nextStateName; - obj.FsmStateNameChanged(obj.fsmStateName); -}; - -Engine.RegisterGlobal("FSM", FSM); Index: ps/trunk/binaries/data/mods/public/simulation/helpers/MultiKeyMap.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/helpers/MultiKeyMap.js +++ ps/trunk/binaries/data/mods/public/simulation/helpers/MultiKeyMap.js @@ -1,225 +0,0 @@ -// Convenient container abstraction for storing items referenced by a 3-tuple. -// Used by the ModifiersManager to store items by (property Name, entity, item ID). -// Methods starting with an underscore are private to the storage. -// This supports stackable items as it stores count for each 3-tuple. -// It is designed to be as fast as can be for a JS container. -function MultiKeyMap() -{ - this.items = new Map(); - // Keys are referred to as 'primaryKey', 'secondaryKey', 'itemID'. -} - -MultiKeyMap.prototype.Serialize = function() -{ - let ret = []; - for (let primary of this.items.keys()) - { - // Keys of a Map can be arbitrary types whereas objects only support string, so use a list. - let vals = [primary, []]; - ret.push(vals); - for (let secondary of this.items.get(primary).keys()) - vals[1].push([secondary, this.items.get(primary).get(secondary)]); - } - return ret; -}; - -MultiKeyMap.prototype.Deserialize = function(data) -{ - for (let primary in data) - { - this.items.set(data[primary][0], new Map()); - for (let secondary in data[primary][1]) - this.items.get(data[primary][0]).set(data[primary][1][secondary][0], data[primary][1][secondary][1]); - } -}; - -/** - * Add a single item. - * NB: if you add an item with a different value but the same itemID, the original value remains. - * @param item - an object. - * @param itemID - internal ID of this item, for later removal and/or updating - * @param stackable - if stackable, changing the count of items invalides, otherwise not. - * @returns true if the items list changed in such a way that cached values are possibly invalidated. - */ -MultiKeyMap.prototype.AddItem = function(primaryKey, itemID, item, secondaryKey, stackable = false) -{ - if (!this._AddItem(primaryKey, itemID, item, secondaryKey, stackable)) - return false; - - this._OnItemModified(primaryKey, secondaryKey, itemID); - return true; -}; - -/** - * Add items to multiple properties at once (only one item per property) - * @param items - Dictionnary of { primaryKey: item } - * @returns true if the items list changed in such a way that cached values are possibly invalidated. - */ -MultiKeyMap.prototype.AddItems = function(itemID, items, secondaryKey, stackable = false) -{ - let modified = false; - for (let primaryKey in items) - modified = this.AddItem(primaryKey, itemID, items[primaryKey], secondaryKey, stackable) || modified; - return modified; -}; - -/** - * Removes a item on a property. - * @param primaryKey - property to change (e.g. "Health/Max") - * @param itemID - internal ID of the item to remove - * @param secondaryKey - secondaryKey ID - * @returns true if the items list changed in such a way that cached values are possibly invalidated. - */ -MultiKeyMap.prototype.RemoveItem = function(primaryKey, itemID, secondaryKey, stackable = false) -{ - if (!this._RemoveItem(primaryKey, itemID, secondaryKey, stackable)) - return false; - - this._OnItemModified(primaryKey, secondaryKey, itemID); - return true; -}; - -/** - * Removes items with this ID for any property name. - * Naively iterates all property names. - * @returns true if the items list changed in such a way that cached values are possibly invalidated. - */ -MultiKeyMap.prototype.RemoveAllItems = function(itemID, secondaryKey, stackable = false) -{ - let modified = false; - // Map doesn't implement some so use a for-loop here. - for (let primaryKey of this.items.keys()) - modified = this.RemoveItem(primaryKey, itemID, secondaryKey, stackable) || modified; - return modified; -}; - -/** - * @param itemID - internal ID of the item to try and find. - * @returns true if there is at least one item with that itemID - */ -MultiKeyMap.prototype.HasItem = function(primaryKey, itemID, secondaryKey) -{ - // some() returns false for an empty list which is wanted here. - return this._getItems(primaryKey, secondaryKey).some(item => item._ID === itemID); -}; - -/** - * Check if we have a item for any property name. - * Naively iterates all property names. - * @returns true if there is at least one item with that itemID - */ -MultiKeyMap.prototype.HasAnyItem = function(itemID, secondaryKey) -{ - // Map doesn't implement some so use for loops instead. - for (let primaryKey of this.items.keys()) - if (this.HasItem(primaryKey, itemID, secondaryKey)) - return true; - return false; -}; - -/** - * @returns A list of items (references to stored items to avoid copying) - * (these need to be treated as constants to not break the map) - */ -MultiKeyMap.prototype.GetItems = function(primaryKey, secondaryKey) -{ - return this._getItems(primaryKey, secondaryKey); -}; - -/** - * @returns A dictionary of { Property Name: items } for the secondary Key. - * Naively iterates all property names. - */ -MultiKeyMap.prototype.GetAllItems = function(secondaryKey) -{ - let items = {}; - - // Map doesn't implement filter so use a for loop. - for (let primaryKey of this.items.keys()) - { - if (!this.items.get(primaryKey).has(secondaryKey)) - continue; - items[primaryKey] = this.GetItems(primaryKey, secondaryKey); - } - return items; -}; - -/** - * @returns a list of items. - * This does not necessarily return a reference to items' list, use _getItemsOrInit for that. - */ -MultiKeyMap.prototype._getItems = function(primaryKey, secondaryKey) -{ - let cache = this.items.get(primaryKey); - if (cache) - cache = cache.get(secondaryKey); - return cache ? cache : []; -}; - -/** - * @returns a reference to the list of items for that property name and secondaryKey. - */ -MultiKeyMap.prototype._getItemsOrInit = function(primaryKey, secondaryKey) -{ - let cache = this.items.get(primaryKey); - if (!cache) - cache = this.items.set(primaryKey, new Map()).get(primaryKey); - - let cache2 = cache.get(secondaryKey); - if (!cache2) - cache2 = cache.set(secondaryKey, []).get(secondaryKey); - return cache2; -}; - -/** - * @returns true if the items list changed in such a way that cached values are possibly invalidated. - */ -MultiKeyMap.prototype._AddItem = function(primaryKey, itemID, item, secondaryKey, stackable) -{ - let items = this._getItemsOrInit(primaryKey, secondaryKey); - for (let it of items) - if (it._ID == itemID) - { - it._count++; - return stackable; - } - items.push({ "_ID": itemID, "_count": 1, "value": item }); - return true; -}; - -/** - * @returns true if the items list changed in such a way that cached values are possibly invalidated. - */ -MultiKeyMap.prototype._RemoveItem = function(primaryKey, itemID, secondaryKey, stackable) -{ - let items = this._getItems(primaryKey, secondaryKey); - - let existingItem = items.filter(item => { return item._ID == itemID; }); - if (!existingItem.length) - return false; - - if (--existingItem[0]._count > 0) - return stackable; - - let stilValidItems = items.filter(item => item._count > 0); - - // Delete entries from the map if necessary to clean up. - if (!stilValidItems.length) - { - this.items.get(primaryKey).delete(secondaryKey); - if (!this.items.get(primaryKey).size) - this.items.delete(primaryKey); - return true; - } - - this.items.get(primaryKey).set(secondaryKey, stilValidItems); - - return true; -}; - -/** - * Stub method, to overload. - */ -MultiKeyMap.prototype._OnItemModified = function(primaryKey, secondaryKey, itemID) {}; - -Engine.RegisterGlobal("MultiKeyMap", MultiKeyMap); Index: ps/trunk/binaries/data/mods/public/simulation/helpers/WeightedList.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/helpers/WeightedList.js +++ ps/trunk/binaries/data/mods/public/simulation/helpers/WeightedList.js @@ -1,39 +0,0 @@ -function WeightedList() -{ - this.elements = new Map(); - this.totalWeight = 0; -}; - -WeightedList.prototype.length = function() -{ - return this.elements.size; -}; - -WeightedList.prototype.push = function(item, weight = 1) -{ - this.elements.set(item, weight); - this.totalWeight += weight; -}; - -WeightedList.prototype.remove = function(item) -{ - const weight = this.elements.get(item); - if (weight) - this.totalWeight -= weight; - this.elements.delete(item); -}; - -WeightedList.prototype.randomItem = function() -{ - const targetWeight = randFloat(0, this.totalWeight); - let cumulativeWeight = 0; - for (let [item, weight] of this.elements) - { - cumulativeWeight += weight; - if (cumulativeWeight >= targetWeight) - return item; - } - return undefined; -}; - -Engine.RegisterGlobal("WeightedList", WeightedList); Index: ps/trunk/binaries/data/mods/public/simulation/helpers/tests/test_MultiKeyMap.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/helpers/tests/test_MultiKeyMap.js +++ ps/trunk/binaries/data/mods/public/simulation/helpers/tests/test_MultiKeyMap.js @@ -1,124 +0,0 @@ -Engine.LoadHelperScript("MultiKeyMap.js"); - -function setup_keys(map) -{ - map.AddItem("prim_a", "item_a", null, "sec_a"); - map.AddItem("prim_a", "item_b", null, "sec_a"); - map.AddItem("prim_a", "item_c", null, "sec_a"); - map.AddItem("prim_a", "item_a", null, "sec_b"); - map.AddItem("prim_b", "item_a", null, "sec_a"); - map.AddItem("prim_c", "item_a", null, "sec_a"); - map.AddItem("prim_c", "item_a", null, 5); -} - -// Check that key-related operations are correct. -function test_keys(map) -{ - TS_ASSERT(map.items.has("prim_a")); - TS_ASSERT(map.items.has("prim_b")); - TS_ASSERT(map.items.has("prim_c")); - - TS_ASSERT(map.items.get("prim_a").has("sec_a")); - TS_ASSERT(map.items.get("prim_a").has("sec_b")); - TS_ASSERT(!map.items.get("prim_a").has("sec_c")); - TS_ASSERT(map.items.get("prim_b").has("sec_a")); - TS_ASSERT(map.items.get("prim_c").has("sec_a")); - TS_ASSERT(map.items.get("prim_c").has(5)); - - TS_ASSERT(map.items.get("prim_a").get("sec_a").length == 3); - TS_ASSERT(map.items.get("prim_a").get("sec_b").length == 1); - TS_ASSERT(map.items.get("prim_b").get("sec_a").length == 1); - TS_ASSERT(map.items.get("prim_c").get("sec_a").length == 1); - TS_ASSERT(map.items.get("prim_c").get(5).length == 1); - - TS_ASSERT(map.GetItems("prim_a", "sec_a").length == 3); - TS_ASSERT(map.GetItems("prim_a", "sec_b").length == 1); - TS_ASSERT(map.GetItems("prim_b", "sec_a").length == 1); - TS_ASSERT(map.GetItems("prim_c", "sec_a").length == 1); - TS_ASSERT(map.GetItems("prim_c", 5).length == 1); - - TS_ASSERT(map.HasItem("prim_a", "item_a", "sec_a")); - TS_ASSERT(map.HasItem("prim_a", "item_b", "sec_a")); - TS_ASSERT(map.HasItem("prim_a", "item_c", "sec_a")); - TS_ASSERT(!map.HasItem("prim_a", "item_d", "sec_a")); - TS_ASSERT(map.HasItem("prim_a", "item_a", "sec_b")); - TS_ASSERT(!map.HasItem("prim_a", "item_b", "sec_b")); - TS_ASSERT(!map.HasItem("prim_a", "item_c", "sec_b")); - TS_ASSERT(map.HasItem("prim_b", "item_a", "sec_a")); - TS_ASSERT(map.HasItem("prim_c", "item_a", "sec_a")); - TS_ASSERT(map.HasAnyItem("item_a", "sec_b")); - TS_ASSERT(map.HasAnyItem("item_b", "sec_a")); - TS_ASSERT(!map.HasAnyItem("item_d", "sec_a")); - TS_ASSERT(!map.HasAnyItem("item_b", "sec_b")); - - // Adding the same item increases its count. - map.AddItem("prim_a", "item_b", 0, "sec_a"); - TS_ASSERT_EQUALS(map.items.get("prim_a").get("sec_a").length, 3); - TS_ASSERT_EQUALS(map.items.get("prim_a").get("sec_a").filter(item => item._ID == "item_b")[0]._count, 2); - TS_ASSERT_EQUALS(map.GetItems("prim_a", "sec_a").length, 3); - - // Adding without stackable doesn't invalidate caches, adding with does. - TS_ASSERT(!map.AddItem("prim_a", "item_b", 0, "sec_a")); - TS_ASSERT(map.AddItem("prim_a", "item_b", 0, "sec_a", true)); - - TS_ASSERT(map.items.get("prim_a").get("sec_a").filter(item => item._ID == "item_b")[0]._count == 4); - - // Likewise removing, unless we now reach 0 - TS_ASSERT(!map.RemoveItem("prim_a", "item_b", "sec_a")); - TS_ASSERT(map.RemoveItem("prim_a", "item_b", "sec_a", true)); - TS_ASSERT(!map.RemoveItem("prim_a", "item_b", "sec_a")); - TS_ASSERT(map.RemoveItem("prim_a", "item_b", "sec_a")); - - // Check that cleanup is done - TS_ASSERT(map.items.get("prim_a").get("sec_a").length == 2); - TS_ASSERT(map.RemoveItem("prim_a", "item_a", "sec_a")); - TS_ASSERT(map.RemoveItem("prim_a", "item_c", "sec_a")); - TS_ASSERT(!map.items.get("prim_a").has("sec_a")); - TS_ASSERT(map.items.get("prim_a").has("sec_b")); - TS_ASSERT(map.RemoveItem("prim_a", "item_a", "sec_b")); - TS_ASSERT(!map.items.has("prim_a")); -} - -function setup_items(map) -{ - map.AddItem("prim_a", "item_a", { "value": 1 }, "sec_a"); - map.AddItem("prim_a", "item_b", { "value": 2 }, "sec_a"); - map.AddItem("prim_a", "item_c", { "value": 3 }, "sec_a"); - map.AddItem("prim_a", "item_c", { "value": 1000 }, "sec_a"); - map.AddItem("prim_a", "item_a", { "value": 5 }, "sec_b"); - map.AddItem("prim_b", "item_a", { "value": 6 }, "sec_a"); - map.AddItem("prim_c", "item_a", { "value": 7 }, "sec_a"); -} - -// Check that items returned are correct. -function test_items(map) -{ - let items = map.GetAllItems("sec_a"); - TS_ASSERT("prim_a" in items); - TS_ASSERT("prim_b" in items); - TS_ASSERT("prim_c" in items); - let sum = 0; - for (let key in items) - items[key].forEach(item => { sum += item.value.value * item._count; }); - TS_ASSERT(sum == 22); -} - -// Test items, and test that deserialised versions still pass test (i.e. test serialisation). -let map = new MultiKeyMap(); -setup_keys(map); -test_keys(map); - -map = new MultiKeyMap(); -let map2 = new MultiKeyMap(); -setup_keys(map); -map2.Deserialize(map.Serialize()); -test_keys(map2); - -map = new MultiKeyMap(); -setup_items(map); -test_items(map); -map = new MultiKeyMap(); -map2 = new MultiKeyMap(); -setup_items(map); -map2.Deserialize(map.Serialize()); -test_items(map2);