Index: binaries/data/mods/public/simulation/components/PlayerManager.js =================================================================== --- binaries/data/mods/public/simulation/components/PlayerManager.js +++ binaries/data/mods/public/simulation/components/PlayerManager.js @@ -25,6 +25,12 @@ newDiplo[id] = 1; cmpPlayer.SetDiplomacy(newDiplo); + Engine.BroadcastMessage(MT_PlayerEntityChanged, { + "player": id, + "from": -1, + "to": ent + }); + return id; }; @@ -37,33 +43,39 @@ { var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); var entities = cmpRangeManager.GetEntitiesByPlayer(id); - for (var e of entities) + for (let e of entities) { - var cmpOwnership = Engine.QueryInterface(e, IID_Ownership); + let cmpOwnership = Engine.QueryInterface(e, IID_Ownership); if (cmpOwnership) cmpOwnership.SetOwner(INVALID_PLAYER); } var oldent = this.playerEntities[id]; - var cmpPlayer = Engine.QueryInterface(oldent, IID_Player); - var diplo = cmpPlayer.GetDiplomacy(); - var color = cmpPlayer.GetColor(); + var oldCmpPlayer = Engine.QueryInterface(oldent, IID_Player); + var diplo = oldCmpPlayer.GetDiplomacy(); + var color = oldCmpPlayer.GetColor(); - var cmpPlayer = Engine.QueryInterface(ent, IID_Player); - cmpPlayer.SetPlayerID(id); + var newCmpPlayer = Engine.QueryInterface(ent, IID_Player); + newCmpPlayer.SetPlayerID(id); this.playerEntities[id] = ent; - cmpPlayer.SetColor(color); - cmpPlayer.SetDiplomacy(diplo); + newCmpPlayer.SetColor(color); + newCmpPlayer.SetDiplomacy(diplo); Engine.DestroyEntity(oldent); Engine.FlushDestroyedEntities(); - for (var e of entities) + for (let e of entities) { - var cmpOwnership = Engine.QueryInterface(e, IID_Ownership); + let cmpOwnership = Engine.QueryInterface(e, IID_Ownership); if (cmpOwnership) cmpOwnership.SetOwner(id); } + + Engine.BroadcastMessage(MT_PlayerEntityChanged, { + "player": id, + "from": oldent, + "to": ent + }); }; /** @@ -126,8 +138,15 @@ PlayerManager.prototype.RemoveAllPlayers = function() { // Destroy existing player entities - for (var id of this.playerEntities) - Engine.DestroyEntity(id); + for (let player in this.playerEntities) + { + Engine.BroadcastMessage(MT_PlayerEntityChanged, { + "player": player, + "from": this.playerEntities[player], + "to": -1 + }); + Engine.DestroyEntity(this.playerEntities[player]); + } this.playerEntities = []; }; @@ -138,6 +157,11 @@ return; var lastId = this.playerEntities.pop(); + Engine.BroadcastMessage(MT_PlayerEntityChanged, { + "player": this.playerEntities.length + 1, + "from": lastId, + "to": -1 + }); Engine.DestroyEntity(lastId); }; Index: binaries/data/mods/public/simulation/components/interfaces/PlayerManager.js =================================================================== --- /dev/null +++ binaries/data/mods/public/simulation/components/interfaces/PlayerManager.js @@ -0,0 +1,6 @@ +/** + * Message of the form { player": number, "from": number, "to": number } + * sent from PlayerManager component to warn other components when a player changed entities. + * This is also sent when the player gets created or destroyed. + */ +Engine.RegisterMessageType("PlayerEntityChanged"); Index: binaries/data/mods/public/simulation/helpers/MultiKeyMap.js =================================================================== --- /dev/null +++ binaries/data/mods/public/simulation/helpers/MultiKeyMap.js @@ -0,0 +1,242 @@ +// Convenient container abstraction for storing items referenced by a 3-tuple. +// Used by the itemsManager 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. +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 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 with storage metadata removed. + */ +MultiKeyMap.prototype.GetItems = function(primaryKey, secondaryKey, stackable = false) +{ + let items = []; + + if (stackable) + this._getItems(primaryKey, secondaryKey).forEach(item => items = items.concat(Array(item.count).fill(item.value))); + else + this._getItems(primaryKey, secondaryKey).forEach(item => items.push(item.value)); + + return items; +}; + +/** + * @returns A dictionary of { Property Name: items } for the secondary Key. + * Naively iterates all property names. + */ +MultiKeyMap.prototype.GetAllItems = function(secondaryKey, stackable = false) +{ + 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, stackable); + } + 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) +{ + if (!this._exists(primaryKey, secondaryKey)) + return []; + return this.items.get(primaryKey).get(secondaryKey); +} + +/** + * @returns a reference to the list of items for that property name and secondaryKey. + */ +MultiKeyMap.prototype._getItemsOrInit = function(primaryKey, secondaryKey) +{ + if (!this._exists(primaryKey, secondaryKey)) + this._initItemsIfNeeded(primaryKey, secondaryKey); + return this.items.get(primaryKey).get(secondaryKey); +} + +MultiKeyMap.prototype._exists = function(primaryKey, secondaryKey) +{ + if (!this.items.has(primaryKey)) + return false; + if (!this.items.get(primaryKey).has(secondaryKey)) + return false; + return true; +} + +MultiKeyMap.prototype._initItemsIfNeeded = function(primaryKey, secondaryKey) +{ + if (!this.items.get(primaryKey)) + this.items.set(primaryKey, new Map()); + if (!this.items.get(primaryKey).get(secondaryKey)) + this.items.get(primaryKey).set(secondaryKey, []); +} + +/** + * @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); + + let existingItems = items.filter(item => { return item.ID == itemID; }); + if (existingItems.length) + { + existingItems[0].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: binaries/data/mods/public/simulation/helpers/tests/test_MultiKeyMap.js =================================================================== --- /dev/null +++ binaries/data/mods/public/simulation/helpers/tests/test_MultiKeyMap.js @@ -0,0 +1,132 @@ +Engine.LoadHelperScript("MultiKeyMap.js"); + +function setup_keys(map) +{ + map.AddItem("prim_a", "item_a", 0, "sec_a"); + map.AddItem("prim_a", "item_b", 0, "sec_a"); + map.AddItem("prim_a", "item_c", 0, "sec_a"); + map.AddItem("prim_a", "item_a", 0, "sec_b"); + map.AddItem("prim_b", "item_a", 0, "sec_a"); + map.AddItem("prim_c", "item_a", 0, "sec_a"); + map.AddItem("prim_c", "item_a", 0, 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(map.items.get("prim_a").get("sec_a").length == 3) + TS_ASSERT(map.items.get("prim_a").get("sec_a").filter(item => item.ID == "item_b")[0].count == 2) + TS_ASSERT(map.GetItems("prim_a", "sec_a").length == 3) + TS_ASSERT(map.GetItems("prim_a", "sec_a", true).length == 4) + + // 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", 1, "sec_a"); + map.AddItem("prim_a", "item_b", 2, "sec_a"); + map.AddItem("prim_a", "item_c", 3, "sec_a"); + map.AddItem("prim_a", "item_c", 1000, "sec_a"); + map.AddItem("prim_a", "item_a", 5, "sec_b"); + map.AddItem("prim_b", "item_a", 6, "sec_a"); + map.AddItem("prim_c", "item_a", 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); + TS_ASSERT(sum == 19) + + items = map.GetAllItems("sec_a", true); + sum = 0; + for (let key in items) + items[key].forEach(item => sum += item); + // We're adding more of the first item_c, the value wasn't replaced. + 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: source/lib/file/vfs/tests/test_vfs_util.h =================================================================== --- /dev/null +++ source/lib/file/vfs/tests/test_vfs_util.h @@ -0,0 +1,95 @@ +/* Copyright (C) 2019 Wildfire Games. + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY + * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, + * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +#include "lib/self_test.h" + +#include "lib/file/vfs/vfs_util.h" +#include "lib/file/file_system.h" +#include "lib/os_path.h" + +static OsPath MOD_PATH(DataDir()/"mods"/"_test.vfs"); +static OsPath CACHE_PATH(DataDir()/"_testcache"); + +extern PIVFS g_VFS; + +class TestVfsUtil : public CxxTest::TestSuite +{ + void initVfs() + { + // Initialise VFS: + + // Make sure the required directories doesn't exist when we start, + // in case the previous test aborted and left them full of junk + if(DirectoryExists(MOD_PATH)) + DeleteDirectory(MOD_PATH); + if(DirectoryExists(CACHE_PATH)) + DeleteDirectory(CACHE_PATH); + + g_VFS = CreateVfs(); + + TS_ASSERT_OK(g_VFS->Mount(L"", MOD_PATH)); + + // Mount _testcache onto virtual /cache - don't use the normal cache + // directory because that's full of loads of cached files from the + // proper game and takes a long time to load. + TS_ASSERT_OK(g_VFS->Mount(L"cache/", CACHE_PATH)); + } + + void deinitVfs() + { + g_VFS.reset(); + DeleteDirectory(MOD_PATH); + DeleteDirectory(CACHE_PATH); + } + +public: + void setUp() + { + initVfs(); + } + + void tearDown() + { + deinitVfs(); + } + + void test_getPathnames() + { + std::shared_ptr nodata(new u8); + g_VFS->CreateFile("test_file.txt", nodata, 0); + g_VFS->CreateFile("test_file2.txt", nodata, 0); + g_VFS->CreateFile("test_file3.txt", nodata, 0); + g_VFS->CreateFile("test_file2.not_txt", nodata, 0); + g_VFS->CreateFile("sub_folder_a/sub_test_file1.txt", nodata, 0); + g_VFS->CreateFile("sub_folder_a/sub_test_file2.txt", nodata, 0); + g_VFS->CreateFile("sub_folder_b/sub_test_file1.txt", nodata, 0); + g_VFS->CreateFile("sub_folder_b/another_file.not_txt", nodata, 0); + + VfsPaths pathNames; + vfs::GetPathnames(g_VFS, "", L"*.txt", pathNames); + TS_ASSERT_EQUALS(pathNames.size(), 3); + vfs::GetPathnames(g_VFS, "sub_folder_a/", L"*.txt", pathNames); + TS_ASSERT_EQUALS(pathNames.size(), 5); + vfs::GetPathnames(g_VFS, "sub_folder_b/", L"*.txt", pathNames); + TS_ASSERT_EQUALS(pathNames.size(), 6); + }; +}; Index: source/lib/file/vfs/vfs_util.cpp =================================================================== --- source/lib/file/vfs/vfs_util.cpp +++ source/lib/file/vfs/vfs_util.cpp @@ -1,4 +1,4 @@ -/* Copyright (C) 2015 Wildfire Games. +/* Copyright (C) 2019 Wildfire Games. * * Permission is hereby granted, free of charge, to any person obtaining * a copy of this software and associated documentation files (the @@ -43,7 +43,6 @@ std::vector files; RETURN_STATUS_IF_ERR(fs->GetDirectoryEntries(path, &files, 0)); - pathnames.clear(); pathnames.reserve(files.size()); for(size_t i = 0; i < files.size(); i++) Index: source/simulation2/components/tests/test_scripts.h =================================================================== --- source/simulation2/components/tests/test_scripts.h +++ source/simulation2/components/tests/test_scripts.h @@ -66,6 +66,7 @@ VfsPaths paths; TS_ASSERT_OK(vfs::GetPathnames(g_VFS, L"simulation/components/tests/", L"test_*.js", paths)); + TS_ASSERT_OK(vfs::GetPathnames(g_VFS, L"simulation/helpers/tests/", L"test_*.js", paths)); paths.push_back(VfsPath(L"simulation/components/tests/setup_test.js")); for (const VfsPath& path : paths) {