MusicManiac added a new file:
QuoteDisplay MoreI'll look into updating my mods when website stops fking dying
THIS DOES NOT HAVE ANY CONTENT, IT'S A DEPENDENCY FOR MODDERS TO USE
This tool is similar to Virtual's Custom Quest Loader in regards of functionality, but it has a couple of QOL features in it:
- Automatic ID generation
- Automatic arrays formating
- Equipment and weapons categories to use in quests
- Which in turn makes your quests automatically support modded items
- Quests configuration on user-side via config file (so they don't have to go dig inside quests jsons to change values).
Mods developed for Virtual's Custom Quest Loader will work with this loader, this is why folders structure is preserved. My guenia pig for testing this was Guiding Light (85%), which has well over 50 quests with different conditions, counter, etc.
However, it's one-way compatibility. If you use QOL features in this loader, your quests will NOT load correctly in Virtual's Custom Quest Loader. They will most likely load, you'll get no errors, but they will most definitely be broken in-game.
QnA:
Q: Can I run this alongside Virtual's Custom Quest Loader?
A: Yes.
Q: Do I need to move my custom traders that I installed from Virtual's Custom Quest Loader to this mod?
A: No, unless they get updated and their dependency changes from Virtual's Custom Quest Loader to this.
Q: Can I delete Virtual's Custom Quest Loader and use this instead?
A: Yes but you don't need to + you'll need to dig in mod files. Just use VCQL. All the QOL in this mod is for MODDERS (with exception of ability to painlessly configure quests by users). You only EVER need this if you run a mod that has a dependency on this.Installation is simple, download this mod, put the mod inside SPT/user/mods folder.
This mod has a dependency on it's own, Weapon Categories Fixer because to use some features of this loader, we first gotta fix BSG inconsistent database.
It's included in the download.
Resources for developing quests:
- Resources: Quest Values Reference Tool
- https://db.sp-tarkov.com/search
- Any trader that adds quests
- Vanilla quests that can be found in Aki_Data/server/database/templates/quests.json
Following things are needed to add your quests into the game:
Traders (Optional)
If you want to have trader + quests, this mod can handle quests side of trader. Loading the trader himself you'll have to do on your own. As a good example to look at how you can load a trader (with pretty much changing a couple of values here and there), check out Guiding Light (85%) or Erika. I'm sure there are more mods that do same thing but these are the only 2 that I use, therefore I looked at their code (or in case of Erika, wrote one) and know that it can be easily adjusted to make your own trader. Or use templates from https://dev.sp-tarkov.com/chomp/ModExamples.
Configs (Optional)
You can add config files for your quests so users can adjust quests as they like. Read more on it in next tab.
Quests (Mandatory)
Quest files can have any name but must follow the format in Aki_Data/server/database/templates/quests.json. For more examples of how quests can look you can check Guiding Light (85%) or Erika, for example.
Side specific quests are supported. The "side" value of a quest can either be "Pmc" (for both sides), "Bear" or "Usec".
Locales (Mandatory)
Language files must also follow a particular format. You can check how exisiting locales file look in Aki_Data/server/database/locales/global/en.
Locales files can also have any name.
Every unique locale, located in any of the language files, will be added to every other place where the same key is not present.
Images (Optional)
All images need to be 314 x 177 and MUST be either a .png or .jpg.
To use custom images, place the image into the 'res/quests' directory and reference the name of the image in your quest file. For example, if you place testImg.png in the folder you can reference it in your quests like "image": "/files/quest/icon/testImg.png",
All Tarkov quest images can be found in Aki_Data/server/images/quests, feel free to use them or load your own.
Now this is where we shine.
This section assumes you're capable browsing AKI_DATA/server/database/templates/quests.json to see how vanilla quests look like or you have used VCQL before.
1. Automatic ID generation
CodeDisplay More"QM_45_AP": { "_id": "QM_45_AP", ... "conditions": { "AvailableForFinish": [ { "_parent": "CounterCreator", "_props": { "counter": { "conditions": [ { "_parent": "Kills", "_props": { ... "id": "thisIsRandomizedInCode", ... } }, { "_parent": "Location", "_props": { "id": "thisIsRandomizedInCode", "target": [ "bigmap" ] } } ], "id": "thisIsSetInCode" }, ... "id": "QM_45_AP Kills_0", "type": "Elimination", ... }, "dynamicLocale": false } ], "AvailableForStart": [ { "_parent": "Level", "_props": { ... "id": "thisIsRandomizedInCode", ... }, "dynamicLocale": false }, { "_parent": "Quest", "_props": { ... "id": "thisIsRandomizedInCode", ... }, "dynamicLocale": false } ], ... }, ... }
What can we see in this snippet:
- Counter conditions ID's no longer need to be set. You don't have to generate new ID for any of your Kill Counter conditions. If you want to generate them, you still can, loader will randomize only ones that are set to "thisIsRandomizedInCode". Attention: this randomizes IDs of conditions use ONLY in "type": "Elimination," counters ("_parent": "CounterCreator",). Why? Because those ID's are unimportant from modding perspective.
- "AvailableForStart" level and quest conditions ("_parent": "Level" and "_parent": "Quest") also can be randomized, they are unimportant to modder.
- Counter itself has an ID, and this one is important. To save progress on quest, information is written in save file, and counterID needs to be same between player sessions. That's why you can see it's not "thisIsRandomizedInCode" but it's "id": "thisIsSetInCode". It set's counter id to quest _id + " counterId". As usual, this only works if you don't set counter id yourself. If it's anything else than "id": "thisIsSetInCode", loader will not touch it.
For example, in snippet above, counter id would be set to "QM_45_AP Kills_0 counterId". And it would be set to it every time loader runs, as long as you keep the same "id": "QM_45_AP Kills_0", which you shouldn't be changing anyway.Example How Above snippet looks without skipping lines:
CodeDisplay More"QM_45_AP": { "QuestName": "Getting that .45 ACP AP", "_id": "QM_45_AP", "acceptPlayerMessage": "QM_45_AP description", "canShowNotificationsInGame": true, "changeQuestMessageText": "QM_45_AP changeQuestMessageText", "completePlayerMessage": "QM_45_AP successMessageText", "conditions": { "AvailableForFinish": [ { "_parent": "CounterCreator", "_props": { "counter": { "conditions": [ { "_parent": "Kills", "_props": { "compareMethod": ">=", "id": "thisIsRandomizedInCode", "target": "Any", "value": "1" } }, { "_parent": "Location", "_props": { "id": "thisIsRandomizedInCode", "target": [ "bigmap" ] } } ], "id": "thisIsSetInCode" }, "doNotResetIfCounterCompleted": false, "dynamicLocale": false, "id": "QM_45_AP Kills_0", "index": 0, "oneSessionOnly": false, "parentId": "", "type": "Elimination", "value": "20", "visibilityConditions": [] }, "dynamicLocale": false } ], "AvailableForStart": [ { "_parent": "Level", "_props": { "compareMethod": ">=", "dynamicLocale": false, "id": "thisIsRandomizedInCode", "index": 0, "parentId": "", "value": 40, "visibilityConditions": [] }, "dynamicLocale": false }, { "_parent": "Quest", "_props": { "availableAfter": 0, "dispersion": 0, "dynamicLocale": false, "id": "thisIsRandomizedInCode", "index": 0, "parentId": "", "status": [ 4 ], "target": "GL_4M", "visibilityConditions": [] }, "dynamicLocale": false } ], "Fail": [] }, "description": "QM_45_AP description", "failMessageText": "QM_45_AP failMessageText", "image": "/files/quest/icon/596b453b86f77457c827bf44.jpg", "instantComplete": false, "isKey": false, "location": "any", "name": "QM_45_AP name", "note": "QM_45_AP note", "questStatus": {}, "restartable": false, "rewards": { "Fail": [], "Started": [], "Success": [] } }, "secretQuest": false, "side": "Pmc", "startedMessageText": "QM_45_AP description", "successMessageText": "QM_45_AP successMessageText", "templateId": "QM_45_AP", "traderId": "Erika_Temporal_Id", "type": "Skill" }
If you ever made a quest that excludes or includes weapon parts or equipment you hate yourself, BSG, earth, sky, heaven and life.
If you make a killcounter that has "equipmentInclusive":, "equipmentExclusive", "weaponModsExclusive' or "weaponModsInclusive" you know that each equipment id has to be in a separate array. Loader implements QOL feature that puts the ids into correct format.
Example:
CodeDisplay More"QM_30mm_scopes": { "QuestName": "Getting those 30mm riflescopes", "_id": "QM_30mm_scopes", ... "conditions": { "AvailableForFinish": [ { "_parent": "CounterCreator", "_props": { "counter": { "conditions": [ { "_parent": "Kills", "_props": { ... "weaponModsInclusive": ["asdb234nj24j32324", "324jnbkj213jbkjb", "21kj31kj32jbfkj"], "weaponModsEclusive": ["324jn324j23n4rfe", "fjn3i4023409fj203jj"], "equipmentInclusive": ["5c0558060db834001b735271"], "equipmentExclusive": ["348945978ghn934htg374htr", "3278hf7fh37fh78f4g72fg"], "value": "1" } } ], "id": "thisIsSetInCode" }, ... }, "dynamicLocale": false }
As you can see, "weaponModsInclusive": ["asdb234nj24j32324", "324jnbkj213jbkjb", "21kj31kj32jbfkj"], is an array with depth of 1, not an array of arrays (as BSG does it for some reason). To clarify, usually any of weaponModsInclusive, weaponModsEclusive, equipmentInclusive, equipmentExclusive has to follow format of [["id1"], ["id2"], ["id3"], ...].
This is optional to use, if you put your ids into array of arrays (as BSG quests do), loader will not touch them. So:
- "weaponModsInclusive": ["asdb234nj24j32324", "324jnbkj213jbkjb", "21kj31kj32jbfkj"], - this will work
- "weaponModsInclusive": [["asdb234nj24j32324"], ["324jnbkj213jbkjb"], ["21kj31kj32jbfkj"]], - this will work
- However, do not combine them inside one property. Loader checks if property (such as "weaponModsInclusive" is an array of arrays, and if it's not, it wraps each element into array)
"weaponModsInclusive": [["asdb234nj24j32324"], "324jnbkj213jbkjb", "21kj31kj32jbfkj"], - this will not workFor next QOL feature to work, these properties must be in 1-depth array. It's made this way to provide compatibility with OLD quests written for Virtual's Custom Quest Loader.
Example How Above snippet looks without skipping lines and applying 2 features previously described:
CodeDisplay More"QM_45_AP": { "QuestName": "Getting that .45 ACP AP", "_id": "QM_45_AP", "acceptPlayerMessage": "QM_45_AP description", "canShowNotificationsInGame": true, "changeQuestMessageText": "QM_45_AP changeQuestMessageText", "completePlayerMessage": "QM_45_AP successMessageText", "conditions": { "AvailableForFinish": [ { "_parent": "CounterCreator", "_props": { "counter": { "conditions": [ { "_parent": "Kills", "_props": { "compareMethod": ">=", "id": "thisIsRandomizedInCode", "target": "Any", "weaponModsInclusive": ["asdb234nj24j32324", "324jnbkj213jbkjb", "21kj31kj32jbfkj"], "weaponModsEclusive": ["324jn324j23n4rfe", "fjn3i4023409fj203jj"], "equipmentInclusive": ["5c0558060db834001b735271"], "equipmentExclusive": ["348945978ghn934htg374htr", "3278hf7fh37fh78f4g72fg"], "value": "1" } }, { "_parent": "Location", "_props": { "id": "thisIsRandomizedInCode", "target": [ "bigmap" ] } } ], "id": "thisIsSetInCode" }, "doNotResetIfCounterCompleted": false, "dynamicLocale": false, "id": "QM_45_AP Kills_0", "index": 0, "oneSessionOnly": false, "parentId": "", "type": "Elimination", "value": "20", "visibilityConditions": [] }, "dynamicLocale": false } ], "AvailableForStart": [ { "_parent": "Level", "_props": { "compareMethod": ">=", "dynamicLocale": false, "id": "thisIsRandomizedInCode", "index": 0, "parentId": "", "value": 40, "visibilityConditions": [] }, "dynamicLocale": false }, { "_parent": "Quest", "_props": { "availableAfter": 0, "dispersion": 0, "dynamicLocale": false, "id": "thisIsRandomizedInCode", "index": 0, "parentId": "", "status": [ 4 ], "target": "GL_4M", "visibilityConditions": [] }, "dynamicLocale": false } ], "Fail": [] }, "description": "QM_45_AP description", "failMessageText": "QM_45_AP failMessageText", "image": "/files/quest/icon/596b453b86f77457c827bf44.jpg", "instantComplete": false, "isKey": false, "location": "any", "name": "QM_45_AP name", "note": "QM_45_AP note", "questStatus": {}, "restartable": false, "rewards": { "Fail": [], "Started": [], "Success": [] } }, "secretQuest": false, "side": "Pmc", "startedMessageText": "QM_45_AP description", "successMessageText": "QM_45_AP successMessageText", "templateId": "QM_45_AP", "traderId": "Erika_Temporal_Id", "type": "Skill" }
Ever wanted to make the quest that requires kills with pistols only and realized you have to put every goddamn pistol id in the quest? This loader features loading categories of weapons/equipment/attachments/barter items/etc. Currently implemented categories can be viewed in mod.ts in public configureCategories(itemDB) function. It parses the database before adding quests. This means that these categories support modded items. For example, if you have a mod that adds brand new sniper rifle that shoots 7,62x51mm rounds, it will automatically count for all the quests that have either "sniperRifles762x51" or "sniperRifles" listed in their weapons.
So you could do something like
CodeDisplay More"QM_45_AP": { "_id": "QM_45_AP", ... "conditions": { "AvailableForFinish": [ { "_parent": "HandoverItem", "_props": { ... "target": ["scopes30mm", "23h123ubuidh1u23ghiu1", "sniperRifles"], "value": "5", "visibilityConditions": [] }, "dynamicLocale": false }, { "_parent": "CounterCreator", "_props": { "counter": { "conditions": [ { "_parent": "Kills", "_props": { ... "weapon": ["SMGs45", "pistols45", "23h3jkhb2hj3bhb12h3gb12h"], "weaponModsInclusive": ["scopes30mm", "324jnbkj213jbkjb", "21kj31kj32jbfkj"], "weaponModsEclusive": ["flashlights", "tacticalComboDevices"], "equipmentInclusive": ["5c0558060db834001b735271", "backpacks (not implemented category, just an example)"], "equipmentExclusive": ["348945978ghn934htg374htr", "helmets (not implemented category, just an example)"], ... } } ], "id": "thisIsSetInCode" }, ... "type": "Elimination", ... }, "dynamicLocale": false } ... }
As you can see, instead of listing every damn id inside "weapon" property we can just list some categories. You can mix in normal ids there too. Note that list of categories available to be used is limited and needs to be expanded manually. If you want to have a category created, you will have to ask for it and wait for it. Providing a code snippet that can be added to public parseItemsDatabase(itemDB) function will accelerate the process.
Categories work in:
- In "_parent": "CounterCreator", of "type": "Elimination", inside "_parent": "Kills", conditions properties: "weapon": ,"equipmentInclusive":, "equipmentExclusive", "weaponModsExclusive' or "weaponModsInclusive"
- In "_parent": "HandoverItem", properties "_props": inside "target": property
For this QOL feature to work, these properties must be in 1-depth array. It's made this way to provide compatibility with OLD quests written for Virtual's Custom Quest Loader, as I don't expect people to convert old quests into better format since they will load anyway.
- These will work:
- "weapon": ["SMGs45", "pistols45", "23h3jkhb2hj3bhb12h3gb12h"],
- "weaponModsInclusive": ["scopes30mm", "324jnbkj213jbkjb", "21kj31kj32jbfkj"],
- "weaponModsEclusive": ["flashlights", "tacticalComboDevices"],
- "equipmentInclusive": ["5c0558060db834001b735271", "backpacks"],
- "equipmentExclusive": ["348945978ghn934htg374htr", "helmets"],
- These will NOT work:
- "weaponModsInclusive": [["scopes30mm"], ["324jnbkj213jbkjb"], ["21kj31kj32jbfkj"]],
- "weaponModsEclusive": ["flashlights", "tacticalComboDevices", ["djk234jkb32423h4gk34h"]],
- "equipmentInclusive": [["5c0558060db834001b735271"], ["backpacks"]],
- "equipmentExclusive": ["348945978ghn934htg374htr", ["helmets"]],
Example How Above snippet looks without skipping lines and applying 3 features previously described:
CodeDisplay More"QM_45_AP": { "QuestName": "Getting that .45 ACP AP", "_id": "QM_45_AP", "acceptPlayerMessage": "QM_45_AP description", "canShowNotificationsInGame": true, "changeQuestMessageText": "QM_45_AP changeQuestMessageText", "completePlayerMessage": "QM_45_AP successMessageText", "conditions": { "AvailableForFinish": [ { "_parent": "HandoverItem", "_props": { "countInRaid": false, "dogtagLevel": 0, "dynamicLocale": false, "id": "QM_45_AP HandoverItem_0", "index": 0, "isEncoded": false, "maxDurability": 100, "minDurability": 0, "onlyFoundInRaid": false, "parentId": "", "target": [ "5efb0cabfb3e451d70735af5", "scopes30mm" ], "value": "600", "visibilityConditions": [] }, "dynamicLocale": false }, { "_parent": "CounterCreator", "_props": { "counter": { "conditions": [ { "_parent": "Kills", "_props": { "compareMethod": ">=", "id": "thisIsRandomizedInCode", "target": "Any", "weapon": ["SMGs45", "pistols45"], "weaponModsInclusive": ["scopes30mm", "324jnbkj213jbkjb", "21kj31kj32jbfkj"], "weaponModsEclusive": ["flashlights", "tacticalComboDevices"], "equipmentInclusive": ["5c0558060db834001b735271"], "equipmentExclusive": ["348945978ghn934htg374htr"], "value": "1" } }, { "_parent": "Location", "_props": { "id": "thisIsRandomizedInCode", "target": [ "bigmap" ] } } ], "id": "thisIsSetInCode" }, "doNotResetIfCounterCompleted": false, "dynamicLocale": false, "id": "QM_45_AP Kills_0", "index": 0, "oneSessionOnly": false, "parentId": "", "type": "Elimination", "value": "20", "visibilityConditions": [] }, "dynamicLocale": false } ] "AvailableForStart": [ { "_parent": "Level", "_props": { "compareMethod": ">=", "dynamicLocale": false, "id": "thisIsRandomizedInCode", "index": 0, "parentId": "", "value": 40, "visibilityConditions": [] }, "dynamicLocale": false } ], "Fail": [] }, "description": "QM_45_AP description", "failMessageText": "QM_45_AP failMessageText", "image": "/files/quest/icon/596b453b86f77457c827bf44.jpg", "instantComplete": false, "isKey": false, "location": "any", "name": "QM_45_AP name", "note": "QM_45_AP note", "questStatus": {}, "restartable": false, "rewards": { "Fail": [], "Started": [], "Success": [ { "target": "Erika_Temporal_Id QM_45_AP Trade", "id": "QM_45_AP Erika_Temporal_Id QM_45_AP Trade", "type": "AssortmentUnlock", "index": 3, "loyaltyLevel": 1, "traderId": "Erika_Temporal_Id", "items": [ { "_id": "Erika_Temporal_Id QM_45_AP Trade", "_tpl": "5efb0cabfb3e451d70735af5", "parentId": "hideout", "slotId": "hideout" } ] } ] }, "secretQuest": false, "side": "Pmc", "startedMessageText": "QM_45_AP description", "successMessageText": "QM_45_AP successMessageText", "templateId": "QM_45_AP", "traderId": "Erika_Temporal_Id", "type": "Skill" }
Balancing quests is annoying cuz people always complain about balance anyway? This loader allows configs for your quests. Users no longer have to go browse jsons of quests to find what they want. Simply add config file in configs folder with following structure:
CodeDisplay More{ "questsItemCounterMultipliers" : { "QM_46x30_AP" : 1, }, "questsKillsCounterMultipliers" : { "QM_46x30_AP" : 1, "QM_9x19_AP" : 1, "QM_9x19_PBP" : 2 }, "questsCultistsKillsCounterMultipliers" : { "QM_46x30_AP" : 1 "QM_GPNVG_18_NVGs" : 1 }, "questsLevelRequirements" : { "QM_46x30_AP" : 35 } }
Idea is very simple, you provide quest ID and number. Make sure your quest IDs are readable by user or else this will not help much and will not make it in any way easier for user to configure quests, which is endgoal.
- questsItemCounterMultipliers - multiplier for your HandoverItem subconditions. So user can make quest require to turn in only 50% of items for quest to complete if they set it to 0.5, for example
- questsKillsCounterMultipliers - multiplier for your kill counters.
- questsCultistsKillsCounterMultipliers - multiplier for your kill counters that have sectantWarrior in list of targets. This take precedence over questsKillsCounterMultipliers. Aka if condition has cultists in it, questsCultistsKillsCounterMultipliers will be applied, NOT questsKillsCounterMultipliers.
- questsLevelRequirements - if quest has level requirement, users can change it here.
You don't have to have this config file, it can be half-empty, it can have only half of your quest Ids, it can have only questsLevelRequirements if you wish. It's however important that IF you have config category that it's named either questsItemCounterMultipliers or questsKillsCounterMultipliers or questsLevelRequirements questsCultistsKillsCounterMultipliers
- I used these QOL to develop Erika and it made my life 10 times simpler. 10/10 recommend. If you need help, take a glance at her files.
- DO NOT include this into your mod, use this as dependency. You can structure your mod so it extracts into appropriate places.
- Please use naming convention I used in Erika files so its easier later (assuming people will be interested in using this loader)
- Ping me on discord in mod-dev channel if you have a question and need semi-fast reply