diff --git a/BepInEx/plugins/DrakiaXYZ-GildedKeyStorage.dll b/BepInEx/plugins/DrakiaXYZ-GildedKeyStorage.dll new file mode 100644 index 0000000..bc5b943 Binary files /dev/null and b/BepInEx/plugins/DrakiaXYZ-GildedKeyStorage.dll differ diff --git a/user/mods/Jehree-GildedKeyStorage/LICENSE.txt b/user/mods/Jehree-GildedKeyStorage/LICENSE.txt new file mode 100644 index 0000000..9b4b7cf --- /dev/null +++ b/user/mods/Jehree-GildedKeyStorage/LICENSE.txt @@ -0,0 +1,7 @@ +Gilded Key Storage © 2023 by Jehree is licensed under Attribution-NonCommercial-NoDerivatives 4.0 International. +To view a copy of this license, visit http://creativecommons.org/licenses/by-nc-nd/4.0/ + +PERMISSION TO USE THIS CONTENT: +If you want to iterate on this project in some way shoot me a message on the +SPT-AKI website and there is a pretty high chance I will be fine with it. +My profile: https://hub.sp-tarkov.com/user/32691-jehree/ \ No newline at end of file diff --git a/user/mods/Jehree-GildedKeyStorage/bundles.json b/user/mods/Jehree-GildedKeyStorage/bundles.json new file mode 100644 index 0000000..070e475 --- /dev/null +++ b/user/mods/Jehree-GildedKeyStorage/bundles.json @@ -0,0 +1,44 @@ +{ + "manifest": [ + { + "key": "CaseBundles/golden_keychain1.bundle", + "dependencyKeys": [ + "assets/commonassets/physics/physicsmaterials.bundle", + "shaders", + "cubemaps" + ] + }, + { + "key": "CaseBundles/golden_keychain2.bundle", + "dependencyKeys": [ + "assets/commonassets/physics/physicsmaterials.bundle", + "shaders", + "cubemaps" + ] + }, + { + "key": "CaseBundles/golden_keychain3.bundle", + "dependencyKeys": [ + "assets/commonassets/physics/physicsmaterials.bundle", + "shaders", + "cubemaps" + ] + }, + { + "key": "CaseBundles/golden_keycard_case.bundle", + "dependencyKeys": [ + "shaders", + "assets/commonassets/physics/physicsmaterials.bundle", + "cubemaps" + ] + }, + { + "key": "CaseBundles/golden_key_pouch.bundle", + "dependencyKeys": [ + "shaders", + "cubemaps", + "assets/commonassets/physics/physicsmaterials.bundle" + ] + } + ] +} \ No newline at end of file diff --git a/user/mods/Jehree-GildedKeyStorage/bundles/CaseBundles/golden_key_pouch.bundle b/user/mods/Jehree-GildedKeyStorage/bundles/CaseBundles/golden_key_pouch.bundle new file mode 100644 index 0000000..506a9c5 Binary files /dev/null and b/user/mods/Jehree-GildedKeyStorage/bundles/CaseBundles/golden_key_pouch.bundle differ diff --git a/user/mods/Jehree-GildedKeyStorage/bundles/CaseBundles/golden_keycard_case.bundle b/user/mods/Jehree-GildedKeyStorage/bundles/CaseBundles/golden_keycard_case.bundle new file mode 100644 index 0000000..f2e4e4d Binary files /dev/null and b/user/mods/Jehree-GildedKeyStorage/bundles/CaseBundles/golden_keycard_case.bundle differ diff --git a/user/mods/Jehree-GildedKeyStorage/bundles/CaseBundles/golden_keychain1.bundle b/user/mods/Jehree-GildedKeyStorage/bundles/CaseBundles/golden_keychain1.bundle new file mode 100644 index 0000000..2344277 Binary files /dev/null and b/user/mods/Jehree-GildedKeyStorage/bundles/CaseBundles/golden_keychain1.bundle differ diff --git a/user/mods/Jehree-GildedKeyStorage/bundles/CaseBundles/golden_keychain2.bundle b/user/mods/Jehree-GildedKeyStorage/bundles/CaseBundles/golden_keychain2.bundle new file mode 100644 index 0000000..f8ae758 Binary files /dev/null and b/user/mods/Jehree-GildedKeyStorage/bundles/CaseBundles/golden_keychain2.bundle differ diff --git a/user/mods/Jehree-GildedKeyStorage/bundles/CaseBundles/golden_keychain3.bundle b/user/mods/Jehree-GildedKeyStorage/bundles/CaseBundles/golden_keychain3.bundle new file mode 100644 index 0000000..b8b2abf Binary files /dev/null and b/user/mods/Jehree-GildedKeyStorage/bundles/CaseBundles/golden_keychain3.bundle differ diff --git a/user/mods/Jehree-GildedKeyStorage/config/barters.json b/user/mods/Jehree-GildedKeyStorage/config/barters.json new file mode 100644 index 0000000..6a5dc67 --- /dev/null +++ b/user/mods/Jehree-GildedKeyStorage/config/barters.json @@ -0,0 +1,73 @@ +{ + "Keytool Barter for Keychain": { + "id": "62a09d3bcf4a99369e262447", + "trader": "mechanic", + "trader_loyalty_level": 1, + "unlimited_stock": true, + "stock_amount": 3, + + "barter": [ + { + "count": 1, + "_tpl": "59fafd4b86f7745ca07e1232" + } + ] + }, + + "Prokill Barter for Keychain": { + "id": "62a09d3bcf4a99369e262447", + "trader": "jaeger", + "trader_loyalty_level": 1, + "unlimited_stock": true, + "stock_amount": 3, + + "barter": [ + { + "count": 1, + "_tpl": "5c1267ee86f77416ec610f72" + } + ] + }, + + "Chains Barter for Keychain": { + "id": "62a09d3bcf4a99369e262447", + "trader": "ragman", + "trader_loyalty_level": 1, + "unlimited_stock": true, + "stock_amount": 3, + + "barter": [ + { + "count": 3, + "_tpl": "573474f924597738002c6174" + }, + { + "count": 1, + "_tpl": "5734758f24597738025ee253" + } + ] + }, + + "Barter for Cottage Key": { + "id": "5a0eb6ac86f7743124037a28", + "trader": "prapor", + "trader_loyalty_level": 2, + "unlimited_stock": true, + "stock_amount": 1, + + "barter": [ + { + "count": 1, + "_tpl": "5c12613b86f7743bbe2c3f76" + }, + { + "count": 1, + "_tpl": "590c37d286f77443be3d7827" + }, + { + "count": 1, + "_tpl": "590c621186f774138d11ea29" + } + ] + } +} \ No newline at end of file diff --git a/user/mods/Jehree-GildedKeyStorage/config/cases.json b/user/mods/Jehree-GildedKeyStorage/config/cases.json new file mode 100644 index 0000000..28c21a4 --- /dev/null +++ b/user/mods/Jehree-GildedKeyStorage/config/cases.json @@ -0,0 +1,405 @@ +{ + "Golden Key Pouch": { + "case_type": "container", + + "id": "Golden_Key_Pouch", + "item_name": "Secure Key Box", + "item_short_name": "Key Box", + "item_description": "Secure and compact storage for your Golden Keychains and Golden Keycard Holder, as well as some extra space for loose keys.", + "flea_price": 3000000, + + "trader": "therapist", + "trader_loyalty_level": 2, + "unlimited_stock": true, + "stock_amount": 1, + + "barter": [ + { + "count": 1, + "_tpl": "5c052e6986f7746b207bc3c9" + }, + { + "count": 1, + "_tpl": "5bc9bc53d4351e00367fbcee" + }, + { + "count": 1, + "_tpl": "59fafd4b86f7745ca07e1232" + }, + { + "count": 300000, + "_tpl": "5449016a4bdc2d6f028b456f" + } + ], + + "ExternalSize": { + "width": 1, + "height": 1 + }, + + "Grids": [ + { + "width": 1, + "height": 4, + "included_filter": [ + "Golden_Keychain1", + "Golden_Keychain2", + "Golden_Keychain3", + "Golden_Keycard_Case" + ] + }, + { + "width": 5, + "height": 5, + "included_filter": ["543be5e94bdc2df1348b4568"] + } + ] + }, + + "Golden Keychain Mk. I": { + "case_type": "slots", + + "id": "Golden_Keychain1", + "item_name": "Golden Keychain Mk. I", + "item_short_name": "Mk. I", + "item_description": "One of 3 gold keychains. This one contains all Ground Zero, Factory, Woods, Customs, Interchange, and Misc keys.", + "flea_price": 1000000, + "sound": "keys", + + "trader": "therapist", + "trader_loyalty_level": 1, + "unlimited_stock": true, + "stock_amount": 1, + + "barter": [ + { + "count": 1, + "_tpl": "62a09d3bcf4a99369e262447" + }, + { + "count": 3, + "_tpl": "5734758f24597738025ee253" + }, + { + "count": 1, + "_tpl": "5448ba0b4bdc2d02308b456c" + } + ], + + "ExternalSize": { + "width": 1, + "height": 1 + }, + + "slot_ids": [ + "658199aa38c79576a2569e13", + "6581998038c79576a2569e11", + "658199972dc4e60f6d556a2f", + + "5448ba0b4bdc2d02308b456c", + "57a349b2245977762b199ec7", + "593858c486f774253a24cb52", + + "591afe0186f77431bd616a11", + "591ae8f986f77406f854be45", + "5d08d21286f774736e7c94c3", + "664d3db6db5dea2bad286955", + "6761a6ccd9bbb27ad703c48a", + + "5938994586f774523a425196", + "5914578086f774123569ffa4", + "5672c92d4bdc2d180f8b4567", + "5938504186f7740991483f30", + "59148c8a86f774197930e983", + "5780cf942459777df90dcb72", + "5780cf9e2459777df90dcb73", + "5780cfa52459777dfb276eb1", + "593aa4be86f77457f56379f8", + "5780cda02459777b272ede61", + "5780cf722459777a5108b9a1", + "5780cf7f2459777de4559322", + "5780cf692459777de4559321", + "59136a4486f774447a1ed172", + "591383f186f7744a4c5edcf3", + "591382d986f774465a6413a7", + "59136e1e86f774432f15d133", + "59387a4986f77401cc236e62", + "5938603e86f77435642354f4", + "5780d0652459777df90dcb74", + "5913877a86f774432f15d444", + "5780d07a2459777de4559324", + "5913611c86f77479e0084092", + "5938144586f77473c2087145", + "593962ca86f774068014d9af", + "5937ee6486f77408994ba448", + "5780d0532459777a5108b9a2", + "5913915886f774123603c392", + "5da743f586f7744014504f72", + "664d4b0103ef2c61246afb56", + "6761a6f90575f25e020816a4", + + "5ad5d49886f77455f9731921", + "5ad5db3786f7743568421cce", + "5e42c71586f7747f245e1343", + "5ad5d64486f774079b080af8", + "5ad5ccd186f774446d5706e9", + "5ad5cfbd86f7742c825d6104", + "5ad5d20586f77449be26d877", + "5ad7217186f7746744498875", + "5ad7242b86f7740a6a3abd43", + "5ad7247386f7747487619dc3", + "5ad5d7d286f77450166e0a89", + "5addaffe86f77470b455f900", + "664d3dd590294949fe2d81b7", + + "5913651986f774432f15d132", + "59136f6f86f774447a1ed173", + "59148f8286f7741b951ea113", + "5a0f075686f7745bcc42ee12", + "5d80cb8786f774405611c7d9", + "62a09ec84f842e1bd12da3f2", + "6391fcf5744e45201147080f" + ] + }, + + "Golden Keychain Mk. II": { + "case_type": "slots", + + "id": "Golden_Keychain2", + "item_name": "Golden Keychain Mk. II", + "item_short_name": "Mk. II", + "item_description": "One of 3 gold keychains. This one contains all Lighthouse and Shoreline keys.", + "flea_price": 1000000, + "sound": "keys", + + "trader": "ragman", + "trader_loyalty_level": 1, + "unlimited_stock": true, + "stock_amount": 1, + + "barter": [ + { + "count": 1, + "_tpl": "62a09d3bcf4a99369e262447" + }, + { + "count": 3, + "_tpl": "5734758f24597738025ee253" + }, + { + "count": 1, + "_tpl": "5a0eb6ac86f7743124037a28" + } + ], + + "ExternalSize": { + "width": 1, + "height": 1 + }, + + "slot_ids": [ + "61a64428a8c6aa1b795f0ba1", + "61a6444b8c141d68246e2d2f", + "61a64492ba05ef10d62adcc1", + "61aa5aed32a4743c3453d319", + "61aa5b518f5e7a39b41416e2", + "61aa5b7db225ac1ead7957c1", + "61aa5ba8018e9821b7368da9", + "61aa81fcb225ac1ead7957c3", + "62987c658081af308d7558c6", + "62987cb98081af308d7558c8", + "62987da96188c076bc0d8c51", + "62987dfc402c7f69bf010923", + "62987e26a77ec735f90a2995", + "62a9cb937377a65d7b070cef", + "664d3de85f2355673b09aed5", + "66265d7be65f224b2e17c6aa", + "5a0eb38b86f774153b320eb0", + "5a0eb6ac86f7743124037a28", + "5a0f068686f7745b0d4ea242", + "5d8e15b686f774445103b190", + "5a0dc45586f7742f6b0b73e3", + "5a0dc95c86f77452440fc675", + "5a144dfd86f77445cb5a0982", + "5a0ec6d286f7742c0b518fb5", + "5a0ee30786f774023b6ee08f", + "5a13eebd86f7746fd639aa93", + "5a13ef0686f7746e5a411744", + "5a0ee34586f774023b6ee092", + "5a0ee37f86f774023657a86f", + "5a1452ee86f7746f33111763", + "5a13ef7e86f7741290491063", + "5a13f46386f7741dd7384b04", + "5a0eff2986f7741fd654e684", + "5a0ea64786f7741707720468", + "5eff09cd30a7dc22fd1ddfed", + "5a144bdb86f7741d374bbde0", + "5a0ee4b586f7743698200d22", + "5a13f24186f77410e57c5626", + "5a13f35286f77413ef1436b0", + "5a145d4786f7744cbb6f4a12", + "5a145d7b86f7744cbb6f4a13", + "5a0eec9686f77402ac5c39f2", + "5a0eecf686f7740350630097", + "5a0eed4386f77405112912aa", + "5a145ebb86f77458f1796f05", + "5a0eee1486f77402aa773226", + "5a0ea79b86f7741d4a35298e", + "5a0f08bc86f77478f33b84c2", + "5a0f0f5886f7741c4e32a472", + "5a0ea69f86f7741cd5406619", + "5a0ec70e86f7742c0b518fba", + "5a0ee62286f774369454a7ac", + "5a0ee72c86f77436955d3435", + "5a0ee76686f7743698200d5c", + "5a0eeb1a86f774688b70aa5c", + "5a0eeb8e86f77461257ed71a", + "5a0eebed86f77461230ddb3d", + "5a0eedb386f77403506300be", + "5a0f006986f7741ffd2fe484", + "5a0f045e86f7745b0f0d0e42", + "5a13ee1986f774794d4c14cd", + "664d3ddfdda2e85aca370d75" + ] + }, + + "Golden Keychain Mk. III": { + "case_type": "slots", + + "id": "Golden_Keychain3", + "item_name": "Golden Keychain Mk. III", + "item_short_name": "Mk. III", + "item_description": "One of 3 gold keychains. This one contains all Streets, Reserve, and Labs keys.", + "flea_price": 1000000, + "sound": "keys", + + "trader": "skier", + "trader_loyalty_level": 1, + "unlimited_stock": true, + "stock_amount": 1, + + "barter": [ + { + "count": 1, + "_tpl": "62a09d3bcf4a99369e262447" + }, + { + "count": 3, + "_tpl": "5734758f24597738025ee253" + }, + { + "count": 1, + "_tpl": "5780d0532459777a5108b9a2" + } + ], + + "ExternalSize": { + "width": 1, + "height": 1 + }, + + "slot_ids": [ + "6398fd8ad3de3849057f5128", "63a39667c9b3aa4b61683e98", + "63a397d3af870e651d58e65b", "63a399193901f439517cafb6", + "63a39c69af870e651d58e6aa", "63a39c7964283b5e9c56b280", + "63a39cb1c9b3aa4b61683ee2", "63a39ce4cd6db0635c1975fa", + "63a39df18a56922e82001f25", "63a39dfe3901f439517cafba", + "63a39e1d234195315d4020bd", "63a39e49cd6db0635c1975fc", + "63a39f08cd6db0635c197600", "63a39f18c2d53c2c6839c1d3", + "63a39f6e64283b5e9c56b289", "63a39fc0af870e651d58e6ae", + "63a39fd1c9b3aa4b61683efb", "63a39fdf1e21260da44a0256", + "63a3a93f8a56922e82001f5d", "63a71e781031ac76fe773c7d", + "64ccc1ec1779ad6ba200a137", + "64ccc1d4a0f13c24561edf27", "64ccc1f4ff54fb38131acf27", + "63a71e922b25f7513905ca20", + "63a71e86b7f4570d3a293169", + "63a71eb5b7f4570d3a29316b", "63a71ed21031ac76fe773c7f", + "64ccc1fe088064307e14a6f7", "64ccc206793ca11c8f450a38", + "64ccc2111779ad6ba200a139", "64ccc246ff54fb38131acf29", + "64ccc24de61ea448b507d34d", "64d4b23dc1b37504b41ac2b6", + "64ccc268c41e91416064ebc7", "64ce572331dd890873175115", + "64ccc25f95763a1ae376e447", + + "6582dbf0b8d7830efc45016f", "6582dbe43a2e5248357dbe9a", + "6582dc5740562727a654ebb1", "6582dc4b6ba9e979af6b79f4", + + "5d80c60f86f77440373c4ece", "5d80c62a86f7744036212b3f", + "5d80c66d86f774405611c7d6", "5d80c6c586f77440351beef1", + "5d80c6fc86f774403a401e3c", "5d80c78786f774403a401e3e", + "5d80c88d86f77440556dbf07", "5d80c8f586f77440373c4ed0", + "5d80c93086f7744036212b41", "5d80c95986f77440351beef3", + "5d80ca9086f774403a401e40", "5d80cab086f77440535be201", + "5d80cb3886f77440556dbf09", "5d80cb5686f77440545d1286", + "5d80cbd886f77470855c26c2", "5d80ccac86f77470841ff452", + "5d80ccdd86f77474f7575e02", "5d80cd1a86f77402aa362f42", + "5d8e0db586f7744450412a42", "5d8e0e0e86f774321140eb56", + "5d8e3ecc86f774414c78d05e", "5d947d3886f774447b415893", + "5d947d4e86f774447b415895", "5d95d6be86f77424444eb3a7", + "5d95d6fa86f77424484aa5e9", "5d9f1fa686f774726974a992", + "5da46e3886f774653b7a83fe", "5da5cdcd86f774529238fb9b", + "5ede7a8229445733cb4c18e2", "5ede7b0c6d23e5473e6e8c66", + + "5c1e2a1e86f77431ea0ea84c", + "5c1f79a086f7746ed066fb8f", "5c1e2d1f86f77431e9280bee" + ] + }, + + "Golden Keycard Case": { + "case_type": "slots", + + "id": "Golden_Keycard_Case", + "item_name": "Golden Keycard Holder", + "item_short_name": "Keycards", + "item_description": "A shiny keycard case that can hold every keycard in the game, including a few Labs Access cards.", + "flea_price": 1000000, + "sound": "container_plastic", + + "trader": "peacekeeper", + "trader_loyalty_level": 1, + "unlimited_stock": true, + "stock_amount": 1, + + "barter": [ + { + "count": 1, + "_tpl": "619cbf9e0a7c3a1a2731940a" + }, + { + "count": 3, + "_tpl": "5734758f24597738025ee253" + }, + { + "count": 1, + "_tpl": "5c94bbff86f7747ee735c08f" + } + ], + + "ExternalSize": { + "width": 1, + "height": 1 + }, + + "slot_ids": [ + "5c1d0f4986f7744bb01837fa", + "5c1d0c5f86f7744bb2683cf0", + "5c1d0dc586f7744baf2e7b79", + "5c1d0d6d86f7744bb2683e1f", + "5efde6b4f5448336730dbd61", + "5c1e495a86f7743109743dfb", + "5e42c81886f7742a01529f57", + "5e42c83786f7742a021fdf3c", + "5c1d0efb86f7744baf2e7b7b", + "66acd6702b17692df20144c0", + "6711039f9e648049e50b3307", + "5c94bbff86f7747ee735c08f", + "5c94bbff86f7747ee735c08f", + "5c94bbff86f7747ee735c08f", + "5c94bbff86f7747ee735c08f", + "5c94bbff86f7747ee735c08f", + "5c94bbff86f7747ee735c08f", + "5c94bbff86f7747ee735c08f", + "5c94bbff86f7747ee735c08f", + "5c94bbff86f7747ee735c08f" + ] + } +} \ No newline at end of file diff --git a/user/mods/Jehree-GildedKeyStorage/config/config.default.json5 b/user/mods/Jehree-GildedKeyStorage/config/config.default.json5 new file mode 100644 index 0000000..9d717e6 --- /dev/null +++ b/user/mods/Jehree-GildedKeyStorage/config/config.default.json5 @@ -0,0 +1,59 @@ +{ + "key_insurance_enabled": false, + "cases_insurance_enabled": false, + "cases_flea_banned": true, + "weightless_keys": true, + "no_key_use_limit": false, + "keys_are_discardable": true, + "all_keys_in_secure": true, + "allow_cases_in_special": false, + "allow_cases_in_secure": true, + "allow_cases_in_backpacks": true, + "cases_allowed_in": [], + "cases_disallowed_in": [], + "use_finite_keys_list": false, + "finite_keys_list":[ + "5780cf7f2459777de4559322", // Dorm room 314 marked key, Uses: 10 + "5937ee6486f77408994ba448", // Machinery key, Uses: 1 + "593962ca86f774068014d9af", // Unknown key, Uses: 1 + "5c94bbff86f7747ee735c08f", // TerraGroup Labs access keycard, Uses: 1 + "5d08d21286f774736e7c94c3", // Shturman's stash key, Uses: 1 + "5d80c60f86f77440373c4ece", // RB-BK marked key, Uses: 10 + "5d80c62a86f7744036212b3f", // RB-VO marked key, Uses: 10 + "5ede7a8229445733cb4c18e2", // RB-PKPM marked key, Uses: 10 + "5efde6b4f5448336730dbd61", // Keycard with a blue marking, Uses: 1 + "62987dfc402c7f69bf010923", // Shared bedroom marked key, Uses: 10 + "6391fcf5744e45201147080f", // Primorsky Ave apartment key, Uses: 1 + "6398fd8ad3de3849057f5128", // Backup hideout key, Uses: 1 + "63a397d3af870e651d58e65b", // Car dealership closed section key, Uses: 1 + "63a39e1d234195315d4020bd", // Primorsky 46-48 skybridge key, Uses: 1 + "63a3a93f8a56922e82001f5d", // Abandoned factory marked key, Uses: 10 + "64ccc25f95763a1ae376e447", // Mysterious room marked key, Uses: 10 + "64ce572331dd890873175115", // Aspect company office key, Uses: 1 + "64d4b23dc1b37504b41ac2b6", // Rusted bloody key, Uses: 1 + "658199aa38c79576a2569e13", // TerraGroup science office key, Uses: 1 + "6582dbf0b8d7830efc45016f", // Relaxation room key, Uses: 2 + "664d3db6db5dea2bad286955", // Shatun's hideout key, Uses: 5 + "664d3dd590294949fe2d81b7", // Grumpy's hideout key, Uses: 5 + "664d3ddfdda2e85aca370d75", // Voron's hideout key, Uses: 5 + "664d3de85f2355673b09aed5", // Leon's hideout key, Uses: 5 + "664d4b0103ef2c61246afb56", // Dorm overseer key, Uses: 2 + "66acd6702b17692df20144c0", // TerraGroup storage room keycard, Uses: 10 + "6711039f9e648049e50b3307", // TerraGroup Labs residential unit keycard , Uses: 2 + //"5e42c81886f7742a01529f57", // Object #11SR keycard, Uses: 10 + //"5e42c83786f7742a021fdf3c", // Object #21WS keycard, Uses: 10 + //"5c1d0c5f86f7744bb2683cf0", // TerraGroup Labs keycard (Blue), Uses: 10 + //"5c1d0d6d86f7744bb2683e1f", // TerraGroup Labs keycard (Yellow), Uses: 10 + //"5c1d0dc586f7744baf2e7b79", // TerraGroup Labs keycard (Green), Uses: 10 + //"5c1d0efb86f7744baf2e7b7b", // TerraGroup Labs keycard (Red), Uses: 10 + //"5c1d0f4986f7744bb01837fa", // TerraGroup Labs keycard (Black), Uses: 10 + //"5c1e495a86f7743109743dfb", // TerraGroup Labs keycard (Violet), Uses: 10 + ], + + "debug": { + "log_missing_keys": false, + "log_rare_keys": false, + "give_profile_all_keys": false, + "force_remove_debug_items_on_start": false + } +} \ No newline at end of file diff --git a/user/mods/Jehree-GildedKeyStorage/package.json b/user/mods/Jehree-GildedKeyStorage/package.json new file mode 100644 index 0000000..de41edb --- /dev/null +++ b/user/mods/Jehree-GildedKeyStorage/package.json @@ -0,0 +1,27 @@ +{ + "name": "Gilded Key Storage", + "version": "1.6.2", + "main": "src/mod.js", + "license": "CC BY-NC-ND 4.0", + "author": "Jehree", + "sptVersion": "~3.11.0", + "isBundleMod": true, + "scripts": { + "setup": "npm i", + "build": "node ./build.mjs", + "buildinfo": "node ./build.mjs --verbose" + }, + "devDependencies": { + "@types/node": "22.10.5", + "@typescript-eslint/eslint-plugin": "7.2", + "@typescript-eslint/parser": "7.2", + "archiver": "^6.0", + "eslint": "8.57", + "fs-extra": "11.2", + "ignore": "^5.2", + "json5": "2.2.3", + "tsyringe": "4.8.0", + "typescript": "5.7.3", + "winston": "3.17.0" + } +} diff --git a/user/mods/Jehree-GildedKeyStorage/src/debug.ts b/user/mods/Jehree-GildedKeyStorage/src/debug.ts new file mode 100644 index 0000000..5b97e52 --- /dev/null +++ b/user/mods/Jehree-GildedKeyStorage/src/debug.ts @@ -0,0 +1,213 @@ +/* eslint-disable @typescript-eslint/brace-style */ +import type { ILogger } from "@spt/models/spt/utils/ILogger"; +import { LogTextColor } from "@spt/models/spt/logging/LogTextColor"; +import type { ITemplateItem } from "@spt/models/eft/common/tables/ITemplateItem"; +import { BaseClasses } from "@spt/models/enums/BaseClasses"; +import type { StaticRouterModService } from "@spt/services/mod/staticRouter/StaticRouterModService"; +import type { SaveServer } from "@spt/servers/SaveServer"; +import type { ItemHelper } from "@spt/helpers/ItemHelper"; +import * as cases from "../config/cases.json"; + +const keysInConfig:Array = [ + ...cases["Golden Keycard Case"].slot_ids, + ...cases["Golden Keychain Mk. I"].slot_ids, + ...cases["Golden Keychain Mk. II"].slot_ids, + ...cases["Golden Keychain Mk. III"].slot_ids +] + +export class Debug +{ + debugConfig: any; + + constructor(debugConfig: any) + { + this.debugConfig = debugConfig; + } + + logMissingKeys(logger:ILogger, itemHelper:ItemHelper, dbItems:Record, dbLocales: Record, ignoredKeyList: string[]):void{ + if (!this.debugConfig.log_missing_keys) return + + logger.log("[Gilded Key Storage]: Keys missing from config: ", LogTextColor.MAGENTA) + logger.log("-------------------------------------------", LogTextColor.YELLOW) + + for (const itemID in dbItems){ + const thisItem = dbItems[itemID] + + // Skip if the item is in our ignore list + if (ignoredKeyList.includes(itemID)) continue; + + // Skip items that aren't items + if (thisItem._type !== "Item") continue; + + // Skip non-keys + if (!itemHelper.isOfBaseclass(thisItem._id, BaseClasses.KEY)) continue; + + // Skip quest keys + if (thisItem._props.QuestItem) continue; + + if (this.isKeyMissing(itemID)){ + + logger.log(dbLocales[`${itemID} Name`], LogTextColor.MAGENTA) + logger.log(itemID, LogTextColor.MAGENTA) + logger.log("-------------------------------------------", LogTextColor.YELLOW) + } + } + } + + logRareKeys(logger:ILogger, itemHelper:ItemHelper, dbItems:Record, dbLocales: Record):void{ + if (!this.debugConfig.log_rare_keys) return + + logger.log("[Gilded Key Storage]: Rare key list: ", LogTextColor.CYAN) + logger.log("-------------------------------------------", LogTextColor.YELLOW) + + for (const itemID in dbItems){ + const thisItem = dbItems[itemID] + + // Skip items that aren't items + if (thisItem._type !== "Item") continue; + + // Skip non-keys + if (!itemHelper.isOfBaseclass(thisItem._id, BaseClasses.KEY)) continue; + + // Skip quest keys + if (thisItem._props.QuestItem) continue; + + if (thisItem._props.MaximumNumberOfUsage <= 10){ + logger.log(` "${itemID}", // ${dbLocales[`${itemID} Name`]}, Uses: ${thisItem._props.MaximumNumberOfUsage}`, LogTextColor.CYAN) + } + } + } + + isKeyMissing(keyId:string):boolean{ + if (keysInConfig.includes(keyId)){ + return false + } + + return true + } + + giveProfileAllKeysAndGildedCases(staticRouterModService:StaticRouterModService, saveServer: SaveServer, logger:ILogger):void{ + if (!this.debugConfig.give_profile_all_keys) return + + staticRouterModService.registerStaticRouter( + "On_Game_Start_Gilded_Key_Storage", + [{ + url: "/client/game/start", + action: async (url, info, sessionId, output) => { + + const profile = saveServer.getProfile(sessionId) + const profileInventory = profile.characters?.pmc?.Inventory + + if (!profileInventory){ + logger.log("New profile detected! load to stash, then close and reopen SPT to receive all keys and gilded cases", LogTextColor.RED) + return output + } + + const itemIdsToPush = this.getArrayOfKeysAndCases() + + let xVal = 0 + let yVal = 0 + + for (let i = 0; i < itemIdsToPush.length; i++){ + const thisItemId = itemIdsToPush[i] + + xVal++ + + if (xVal > 9){ + xVal = 0 + yVal += 1 + } + + profileInventory.items.push( + { + _id: `${thisItemId}_gilded_debug_id`, + _tpl: thisItemId, + parentId: profileInventory.stash, + slotId: "hideout", + location: { + x: xVal, + y: yVal, + r: "Horizontal", + isSearched: true + } + } + ) + + profile.characters.pmc.Encyclopedia[thisItemId] = true + } + return output + } + }], + "spt" + ); + } + + removeAllDebugInstanceIdsFromProfile(staticRouterModService:StaticRouterModService, saveServer: SaveServer):void{ + + if (!this.debugConfig.give_profile_all_keys && !this.debugConfig.force_remove_debug_items_on_start) return + + let urlHook = "/client/game/logout" + if (this.debugConfig.force_remove_debug_items_on_start){ + urlHook = "/client/game/start" + } + + staticRouterModService.registerStaticRouter( + "On_Logout_Gilded_Key_Storage", + [{ + url: urlHook, + action: async (url, info, sessionId, output) => { + + const profile = saveServer.getProfile(sessionId) + const profileInventory = profile.characters?.pmc?.Inventory + const profileItems = profileInventory.items + + if (!profileInventory){return output} + + for (let i = profileItems.length; i > 0; i--){ + + const itemKey = i-1 + + if (profileItems[itemKey]._id.includes("_gilded_debug_id")){ + + profileInventory.items.splice(itemKey, 1) + } + } + + return output + } + }], + "spt" + ); + } + + + // biome-ignore lint/suspicious/noExplicitAny: + getArrayOfKeysAndCases():Array{ + const keysAndCases = [ + ...keysInConfig, + cases["Golden Key Pouch"].id, + cases["Golden Keycard Case"].id, + cases["Golden Keychain Mk. I"].id, + cases["Golden Keychain Mk. II"].id, + cases["Golden Keychain Mk. III"].id + ] + + for (let i = keysAndCases.length; i > 0; i--){ + const top = i-1 + + for (let x = keysAndCases.length; x > 0; x--){ + const bottom = x-1 + + if (top !== bottom){ + + if (keysAndCases[top] === keysAndCases[bottom]){ + + keysAndCases.splice(bottom, 1) + } + } + } + } + + return keysAndCases + } +} \ No newline at end of file diff --git a/user/mods/Jehree-GildedKeyStorage/src/mod.ts b/user/mods/Jehree-GildedKeyStorage/src/mod.ts new file mode 100644 index 0000000..950c9bd --- /dev/null +++ b/user/mods/Jehree-GildedKeyStorage/src/mod.ts @@ -0,0 +1,715 @@ +/* eslint-disable @typescript-eslint/naming-convention */ +/* eslint-disable @typescript-eslint/brace-style */ +import type { DependencyContainer } from "tsyringe"; +import type { ProfileHelper } from "@spt/helpers/ProfileHelper"; +import type { StaticRouterModService } from "@spt/services/mod/staticRouter/StaticRouterModService"; +import type { SaveServer } from "@spt/servers/SaveServer"; +import type { IPostDBLoadMod } from "@spt/models/external/IPostDBLoadMod"; +import type { IPreSptLoadMod } from "@spt/models/external/IPreSptLoadMod"; +import type { ItemHelper } from "@spt/helpers/ItemHelper"; +import type { HashUtil } from "@spt/utils/HashUtil"; +import type { DatabaseServer } from "@spt/servers/DatabaseServer"; +import type { ILogger } from "@spt/models/spt/utils/ILogger"; +import { BaseClasses } from "@spt/models/enums/BaseClasses"; +import { ItemTpl } from "@spt/models/enums/ItemTpl"; +import { Traders } from "@spt/models/enums/Traders"; +import { LogTextColor } from "@spt/models/spt/logging/LogTextColor"; +import type { GameController } from "@spt/controllers/GameController"; +import type { IEmptyRequestData } from "@spt/models/eft/common/IEmptyRequestData"; +import type { ITrader } from "@spt/models/eft/common/tables/ITrader"; +import type { ITemplateItem } from "@spt/models/eft/common/tables/ITemplateItem"; +import { FileSystemSync } from "@spt/utils/FileSystemSync"; +import { Debug } from "./debug"; + +import barters from "../config/barters.json"; +import cases from "../config/cases.json"; + +import path from "path"; +import { copyFileSync, existsSync } from "fs"; +import JSON5 from "json5"; + +class Mod implements IPostDBLoadMod, IPreSptLoadMod { + private HANDBOOK_GEARCASES = "5b5f6fa186f77409407a7eb7"; + newIdMap = { + Golden_Key_Pouch: "661cb36922c9e10dc2d9514b", + Golden_Keycard_Case: "661cb36f5441dc730e28bcb0", + Golden_Keychain1: "661cb372e5eb56290da76c3e", + Golden_Keychain2: "661cb3743bf00d3d145518b3", + Golden_Keychain3: "661cb376b16226f648eb0cdc" + }; + + // These are keys that BSG added with no actual use, or drop chance. Ignore them for now + // These should be confirmed every client update to still be unused + private ignoredKeyList = [ + "5671446a4bdc2d97058b4569", + "57518f7724597720a31c09ab", + "57518fd424597720c85dbaaa", + "5751916f24597720a27126df", + "5751961824597720a31c09ac", + "590de4a286f77423d9312a32", + "590de52486f774226a0c24c2", + "61a6446f4b5f8b70f451b166", + "63a39ddda3a2b32b5f6e007a", + "63a39e0f64283b5e9c56b282", + "63a39e5b234195315d4020bf", + "63a39e6acd6db0635c1975fe", + "63a71f1a0aa9fb29da61c537", + "63a71f3b0aa9fb29da61c539", + "658199a0490414548c0fa83b", + "6582dc63cafcd9485374dbc5" + ]; + + logger: ILogger + modName: string + modVersion: string + container: DependencyContainer; + profileHelper: ProfileHelper; + itemHelper: ItemHelper; + fileSystemSync: FileSystemSync; + config: any; + + constructor() { + this.modName = "Gilded Key Storage"; + } + + public preSptLoad(container: DependencyContainer): void { + this.container = container; + + const staticRouterModService = container.resolve("StaticRouterModService") + const saveServer = container.resolve("SaveServer") + const logger = container.resolve("WinstonLogger") + this.profileHelper = container.resolve("ProfileHelper"); + this.itemHelper = container.resolve("ItemHelper"); + this.fileSystemSync = container.resolve("FileSystemSync"); + + // Load our config + this.loadConfig(); + + // On game start, see if we need to fix issues from previous versions + // Note: We do this as a method replacement so we can run _before_ SPT's gameStart + container.afterResolution("GameController", (_, result: GameController) => { + const originalGameStart = result.gameStart; + + result.gameStart = (url: string, info: IEmptyRequestData, sessionID: string, startTimeStampMS: number) => { + // If there's a profile ID passed in, call our fixer method + if (sessionID) + { + this.fixProfile(sessionID); + } + + // Call the original + originalGameStart.apply(result, [url, info, sessionID, startTimeStampMS]); + } + }); + + // Setup debugging if enabled + const debugUtil = new Debug(this.config.debug) + debugUtil.giveProfileAllKeysAndGildedCases(staticRouterModService, saveServer, logger) + debugUtil.removeAllDebugInstanceIdsFromProfile(staticRouterModService, saveServer) + } + + public postDBLoad(container: DependencyContainer): void { + this.logger = container.resolve("WinstonLogger"); + this.logger.log(`[${this.modName}] : Mod loading`, LogTextColor.GREEN); + const debugUtil = new Debug(this.config.debug) + const databaseServer = container.resolve("DatabaseServer"); + const dbTables = databaseServer.getTables(); + const restrInRaid = dbTables.globals.config.RestrictionsInRaid; + const dbTemplates = dbTables.templates + const dbTraders = dbTables.traders + const dbItems = dbTemplates.items + const dbLocales = dbTables.locales.global.en + + debugUtil.logRareKeys(this.logger, this.itemHelper, dbItems, dbLocales); + this.combatibilityThings(dbItems) + + for (const caseName of Object.keys(cases)) + { + this.createCase(container, cases[caseName], dbTables); + } + + this.pushSupportiveBarters(dbTraders) + this.adjustItemProperties(dbItems) + this.setLabsCardInRaidLimit(restrInRaid, 9) + + debugUtil.logMissingKeys(this.logger, this.itemHelper, dbItems, dbLocales, this.ignoredKeyList) + } + + loadConfig(): void { + const userConfigPath = path.resolve(__dirname, "../config/config.json5"); + const defaultConfigPath = path.resolve(__dirname, "../config/config.default.json5"); + + // Copy the default config if the user config doesn't exist yet + if (!existsSync(userConfigPath)) + { + copyFileSync(defaultConfigPath, userConfigPath); + } + + // Create the config as a merge of the default and user configs, so we always + // have the default values available, even if missing in the user config + this.config = { + ...JSON5.parse(this.fileSystemSync.read(defaultConfigPath)), + ...JSON5.parse(this.fileSystemSync.read(userConfigPath)) + }; + } + + pushSupportiveBarters(dbTraders: Record):void{ + for (const barter of Object.keys(barters)){ + this.pushToTrader(barters[barter], barters[barter].id, dbTraders); + } + } + + // biome-ignore lint/suspicious/noExplicitAny: + setLabsCardInRaidLimit(restrInRaid:any, limitAmount:number):void{ + if (restrInRaid === undefined) return + + //restrInRaid type set to any to shut the linter up because the type doesn't include MaxIn... props + //set labs access card limit in raid to 9 so the keycard case can be filled while on pmc + for (const restr in restrInRaid){ + const thisRestriction = restrInRaid[restr] + if (thisRestriction.TemplateId === ItemTpl.KEYCARD_TERRAGROUP_LABS_ACCESS){ + thisRestriction.MaxInLobby = limitAmount; + thisRestriction.MaxInRaid = limitAmount; + } + } + } + + adjustItemProperties(dbItems: Record){ + for (const [_, item] of Object.entries(dbItems)){ + // Skip anything that isn't specifically an Item type item + if (item._type !== "Item") + { + continue; + } + + const itemProps = item._props + + // Adjust key specific properties + if (this.itemHelper.isOfBaseclass(item._id, BaseClasses.KEY)){ + + if (this.config.weightless_keys){ + itemProps.Weight = 0.0; + } + + itemProps.InsuranceDisabled = !this.config.key_insurance_enabled; + + // If keys are to be set to no limit, and we're either not using the finite keys list, or this key doesn't exist + // in it, set the key max usage to 0 (infinite) + if (this.config.no_key_use_limit && + (!this.config.use_finite_keys_list || !this.config.finite_keys_list.includes(item._id))) + { + itemProps.MaximumNumberOfUsage = 0; + } + + if (this.config.keys_are_discardable) { + // BSG uses DiscordLimit == 0 to flag as not insurable, so we need to swap to the flag + if (itemProps.DiscardLimit === 0) + { + itemProps.InsuranceDisabled = true; + } + + itemProps.DiscardLimit = -1; + } + } + + // Remove keys from secure container exclude filter + if (this.config.all_keys_in_secure && this.itemHelper.isOfBaseclass(item._id, BaseClasses.MOB_CONTAINER) && itemProps?.Grids) { + // Theta container has multiple grids, so we need to loop through all grids + for (const grid of itemProps.Grids) { + const filter = grid?._props?.filters[0]; + if (filter) + { + // Exclude items with a base class of KEY. Have to check that it's an "Item" type first because isOfBaseClass only accepts Items + filter.ExcludedFilter = filter.ExcludedFilter.filter( + itemTpl => this.itemHelper.getItem(itemTpl)[1]?._type !== "Item" || !this.itemHelper.isOfBaseclass(itemTpl, BaseClasses.KEY) + ); + } + } + } + } + } + + combatibilityThings(dbItems: Record):void{ + //do a compatibility correction to make this mod work with other mods with destructive code (cough, SVM, cough) + //basically just add the filters element back to backpacks and secure containers if they've been removed by other mods + const compatFiltersElement = [{ Filter: [BaseClasses.ITEM], ExcludedFilter: [] }]; + + for (const [_, item] of Object.entries(dbItems)){ + // Skip non-items + if (item._type !== "Item") continue; + + if ( + item._parent === BaseClasses.BACKPACK || + item._parent === BaseClasses.VEST || + (this.itemHelper.isOfBaseclass(item._id, BaseClasses.MOB_CONTAINER) && item._id !== ItemTpl.SECURE_CONTAINER_BOSS) + ) { + for (const grid of item._props.Grids) + { + if (grid._props.filters[0] === undefined) { + grid._props.filters = structuredClone(compatFiltersElement); + } + } + } + } + } + + createCase(container, caseConfig, tables){ + const handbook = tables.templates.handbook; + const locales = Object.values(tables.locales.global) as Record[]; + const itemID: string = caseConfig.id; + const itemPrefabPath = `CaseBundles/${itemID.toLocaleLowerCase()}.bundle` + const templateId = this.newIdMap[itemID]; + // biome-ignore lint/suspicious/noExplicitAny: + let item: any; + + //clone a case + if (caseConfig.case_type === "container"){ + item = structuredClone(tables.templates.items[ItemTpl.CONTAINER_SICC]); + item._props.IsAlwaysAvailableForInsurance = true; + item._props.DiscardLimit = -1; + } + + if (caseConfig.case_type === "slots"){ + item = structuredClone(tables.templates.items[ItemTpl.MOUNT_STRIKE_INDUSTRIES_KEYMOD_4_INCH_RAIL]); + item._props.IsAlwaysAvailableForInsurance = true; + item._props.DiscardLimit = -1; + item._props.ItemSound = caseConfig.sound; + } + + item._name = caseConfig.item_name; + item._id = templateId; + item._props.Prefab.path = itemPrefabPath; + + //call methods to set the grid or slot cells up + if (caseConfig.case_type === "container"){ + item._props.Grids = this.createGrid(container, templateId, caseConfig); + } + if (caseConfig.case_type === "slots"){ + item._props.Slots = this.createSlot(container, templateId, caseConfig); + } + + //set external size of the container: + item._props.Width = caseConfig.ExternalSize.width; + item._props.Height = caseConfig.ExternalSize.height; + + tables.templates.items[templateId] = item; + + //add locales + for (const locale of locales) { + locale[`${templateId} Name`] = caseConfig.item_name; + locale[`${templateId} ShortName`] = caseConfig.item_short_name; + locale[`${templateId} Description`] = caseConfig.item_description; + } + + item._props.CanSellOnRagfair = !this.config.cases_flea_banned; + item._props.InsuranceDisabled = !this.config.cases_insurance_enabled; + const price = caseConfig.flea_price + + handbook.Items.push( + { + Id: templateId, + ParentId: this.HANDBOOK_GEARCASES, + Price: price + } + ); + + //allow or disallow in secure containers, backpacks, other specific items per the config + this.allowIntoContainers( + templateId, + tables.templates.items + ); + + this.pushToTrader(caseConfig, templateId, tables.traders); + } + + pushToTrader(caseConfig, itemID:string, dbTraders: Record){ + const traderIDs = { + mechanic: Traders.MECHANIC, + skier: Traders.SKIER, + peacekeeper: Traders.PEACEKEEPER, + therapist: Traders.THERAPIST, + prapor: Traders.PRAPOR, + jaeger: Traders.JAEGER, + ragman: Traders.RAGMAN + }; + + //add to config trader's inventory + let traderToPush = caseConfig.trader; + for (const [key, val] of Object.entries(traderIDs)) + { + if (key === caseConfig.trader){ + traderToPush = val; + } + } + const trader = dbTraders[traderToPush]; + + trader.assort.items.push({ + _id: itemID, + _tpl: itemID, + parentId: "hideout", + slotId: "hideout", + upd: + { + UnlimitedCount: caseConfig.unlimited_stock, + StackObjectsCount: caseConfig.stock_amount + } + }); + + // biome-ignore lint/suspicious/noExplicitAny: + const barterTrade: any = []; + const configBarters = caseConfig.barter; + + for (const barter in configBarters){ + barterTrade.push(configBarters[barter]); + } + + trader.assort.barter_scheme[itemID] = [barterTrade]; + trader.assort.loyal_level_items[itemID] = caseConfig.trader_loyalty_level; + } + + allowIntoContainers(itemID, items: Record): void { + for (const [_, item] of Object.entries(items)){ + // Skip non-items + if (item._type !== "Item") continue; + + //disallow in backpacks + if (!this.config.allow_cases_in_backpacks){ + this.allowOrDisallowIntoCaseByParent(itemID, "exclude", item, BaseClasses.BACKPACK); + } + + //allow in secure containers + if (this.config.allow_cases_in_secure){ + this.allowOrDisallowIntoCaseByParent(itemID, "include", item, BaseClasses.MOB_CONTAINER); + } + + //disallow in additional specific items + for (const configItem in this.config.cases_disallowed_in){ + if (this.config.cases_disallowed_in[configItem] === item._id){ + this.allowOrDisallowIntoCaseByID(itemID, "exclude", item); + } + + } + + //allow in additional specific items + for (const configItem in this.config.cases_allowed_in){ + if (this.config.cases_allowed_in[configItem] === item._id){ + this.allowOrDisallowIntoCaseByID(itemID, "include", item); + } + } + + // Allow in special slots + if (this.config.allow_cases_in_special && (item._id === ItemTpl.POCKETS_1X4_SPECIAL || item._id === ItemTpl.POCKETS_1X4_TUE)){ + this.allowInSpecialSlots(itemID, item); + this.allowInSpecialSlots(itemID, item); + } + } + } + + allowOrDisallowIntoCaseByParent(customItemID, includeOrExclude, currentItem, caseParent): void { + // Skip if the parent isn't our case parent + if (currentItem._parent !== caseParent || currentItem._id === ItemTpl.SECURE_CONTAINER_BOSS) + { + return; + } + + if (includeOrExclude === "exclude") { + for (const grid of currentItem._props.Grids) { + if (grid._props.filters[0].ExcludedFilter === undefined) { + grid._props.filters[0].ExcludedFilter = [customItemID]; + } else { + grid._props.filters[0].ExcludedFilter.push(customItemID) + } + } + } + + if (includeOrExclude === "include") { + for (const grid of currentItem._props.Grids) { + if (grid._props.filters[0].Filter === undefined) { + grid._props.filters[0].Filter = [customItemID]; + } else { + grid._props.filters[0].Filter.push(customItemID) + } + } + } + } + + allowOrDisallowIntoCaseByID(customItemID, includeOrExclude, currentItem): void { + + //exclude custom case in specific item of caseToApplyTo id + if (includeOrExclude === "exclude"){ + for (const grid of currentItem._props.Grids) { + if (grid._props.filters[0].ExcludedFilter === undefined){ + grid._props.filters[0].ExcludedFilter = [customItemID]; + } else { + grid._props.filters[0].ExcludedFilter.push(customItemID) + } + } + } + + //include custom case in specific item of caseToApplyTo id + if (includeOrExclude === "include"){ + for (const grid of currentItem._props.Grids) { + if (grid._props.filters[0].Filter === undefined){ + grid._props.filters[0].Filter = [customItemID]; + } else { + grid._props.filters[0].Filter.push(customItemID) + } + } + } + } + + allowInSpecialSlots(customItemID, currentItem): void { + for (const slot of currentItem._props.Slots) { + slot._props.filters[0]?.Filter.push(customItemID); + } + } + + createGrid(container, itemID, config) { + const grids = []; + + // Loop over all grids in the config + for (let i = 0; i < config.Grids.length; i++) { + const grid = config.Grids[i]; + const inFilt = this.replaceOldIdWithNewId(grid.included_filter ?? []); + const exFilt = this.replaceOldIdWithNewId(grid.excluded_filter ?? []); + const cellWidth = grid.width; + const cellHeight = grid.height; + + // If there's no include filter, add all items + if (inFilt.length === 0) { + inFilt.push(BaseClasses.ITEM); + } + + grids.push(this.generateGridColumn(container, itemID, `column${i}`, cellWidth, cellHeight, inFilt, exFilt)); + } + + return grids; + } + + replaceOldIdWithNewId(entries) + { + const newIdKeys = Object.keys(this.newIdMap); + for (let i = 0; i < entries.length; i++) + { + if (newIdKeys.includes(entries[i])) + { + entries[i] = this.newIdMap[entries[i]]; + } + } + + return entries; + } + + createSlot(container, itemID, config) { + const slots = []; + const configSlots = config.slot_ids; + + for (let i = 0; i < configSlots.length; i++){ + slots.push(this.generateSlotColumn(container, itemID, `mod_mount_${i}`, configSlots[i])); + } + return slots; + } + + generateGridColumn(container: DependencyContainer, itemID, name, cellH, cellV, inFilt, exFilt) { + const hashUtil = container.resolve("HashUtil") + return { + _name: name, + _id: hashUtil.generate(), + _parent: itemID, + _props: { + filters: [ + { + Filter: [...inFilt], + ExcludedFilter: [...exFilt] + } + ], + cellsH: cellH, + cellsV: cellV, + minCount: 0, + maxCount: 0, + maxWeight: 0, + isSortingTable: false + } + }; + } + + generateSlotColumn(container: DependencyContainer, itemID, name, configSlot) { + const hashUtil = container.resolve("HashUtil") + return { + _name: name, + _id: hashUtil.generate(), + _parent: itemID, + _props: { + filters: [ + { + Filter: [configSlot], + ExcludedFilter: [] + } + ], + _required: false, + _mergeSlotWithChildren: false + } + }; + } + + // Handle updating the user profile between versions: + // - Update the container IDs to the new MongoID format + // - Look for any key cases in the user's inventory, and properly update the child key locations if we've moved them + fixProfile(sessionId: string) { + const databaseServer = this.container.resolve("DatabaseServer"); + const dbTables = databaseServer.getTables(); + const dbItems = dbTables.templates.items; + + const pmcProfile = this.profileHelper.getFullProfile(sessionId)?.characters?.pmc; + + // Do nothing if the profile isn't initialized + if (!pmcProfile?.Inventory?.items) return; + + // Update the container IDs to the new MongoID format + for (const item of pmcProfile.Inventory.items) + { + if (this.newIdMap[item._tpl]) + { + item._tpl = this.newIdMap[item._tpl]; + } + } + + // Backup the PMC inventory + const pmcInventory = structuredClone(pmcProfile.Inventory.items); + + // Look for any key cases in the user's inventory, and properly update the child key locations if we've moved them + for (const caseName of Object.keys(cases)) + { + const caseConfig = cases[caseName]; + + if (caseConfig.case_type === "slots" && !this.fixSlotCase(caseConfig, dbItems, pmcProfile)) { + pmcProfile.Inventory.items = pmcInventory; + return; + } + + if (caseConfig.case_type === "container" && !this.fixContainerCase(caseConfig, dbItems, pmcProfile)) { + pmcProfile.Inventory.items = pmcInventory; + return; + } + } + } + + fixSlotCase(caseConfig, dbItems, pmcProfile) { + const templateId = this.newIdMap[caseConfig.id]; + + // Get the template for the case + const caseTemplate = dbItems[templateId]; + + // Try to find the case in the user's profile + const inventoryCases = pmcProfile.Inventory.items.filter(x => x._tpl === templateId); + + for (const inventoryCase of inventoryCases) + { + const caseChildren = pmcProfile.Inventory.items.filter(x => x.parentId === inventoryCase._id); + + for (const child of caseChildren) + { + // Skip if the current slot filter can hold the given item, and there aren't multiple items in it + const currentSlot = caseTemplate._props?.Slots?.find(x => x._name === child.slotId); + if (currentSlot._props?.filters[0]?.Filter[0] === child._tpl && + // A release of GKS went out that may have stacked keycards, so check for any stacked items in one slot + caseChildren.filter(x => x.slotId === currentSlot._name).length === 1 + ) + { + continue; + } + + // Find a new slot, if this is a labs access item, find the first empty compatible slot + const newSlot = caseTemplate._props?.Slots?.find(x => + x._props?.filters[0]?.Filter[0] === child._tpl && + // A release of GKS went out that may have stacked keycards, try to fix that + ( + child._tpl !== ItemTpl.KEYCARD_TERRAGROUP_LABS_ACCESS || + !caseChildren.find(y => y.slotId === x._name) + ) + ); + + // If we couldn't find a new slot for this key, something has gone horribly wrong, restore the inventory and exit + if (!newSlot) + { + this.logger.error(`[${this.modName}] : ERROR: Unable to find new slot for ${child._tpl}. Restoring inventory and exiting`); + return false; + } + + if (newSlot._name !== child.slotId) + { + this.logger.debug(`[${this.modName}] : Need to move ${child.slotId} to ${newSlot._name}`); + child.slotId = newSlot._name; + } + } + } + + return true; + } + + fixContainerCase(caseConfig, dbItems, pmcProfile) { + const templateId = this.newIdMap[caseConfig.id]; + + // Get the template for the case + const caseTemplate = dbItems[templateId]; + + // Try to find the case in the user's profile + const inventoryCases = pmcProfile.Inventory.items.filter(x => x._tpl === templateId); + + for (const inventoryCase of inventoryCases) + { + const caseChildren = pmcProfile.Inventory.items.filter(x => x.parentId === inventoryCase._id); + + for (const child of caseChildren) + { + // Skip if the item already has a location property + if (child.location) { + continue; + } + + // Find which grid the item should be in + const newGrid = caseTemplate._props?.Grids?.find(x => + x._props?.filters[0]?.Filter?.includes(child._tpl) + ); + + if (!newGrid) { + this.logger.error(`[${this.modName}] : ERROR: Unable to find new grid for ${child._tpl}. Restoring inventory and exiting`); + return false; + } + + // Find the first free slot in that grid, assume everything is a 1x1 item + let newX = -1; + let newY = -1; + for (let y = 0; y < newGrid._props.cellsV && newY < 0; y++) + { + for (let x = 0; x < newGrid._props.cellsH && newX < 0; x++) + { + if (!caseChildren.find(item => item.location?.x == x && item.location?.y == y)) { + newX = x; + newY = y; + } + } + } + + if (newX == -1 || newY == -1) { + this.logger.error(`[${this.modName}] : ERROR: Unable to find new location for ${child._tpl}. Restoring inventory and exiting`); + return false; + } + + this.logger.debug(`[${this.modName}] : Need to move ${child.slotId} to ${newGrid._name} X: ${newX} Y: ${newY}`); + + // Update the child item to the new location + child.location = { + "x": newX, + "y": newY, + "r": "Horizontal" + }; + child.slotId = newGrid._name; + } + } + + return true; + } +} + +module.exports = { mod: new Mod() }