2023-05-04 09:57:42 +08:00
const path = require ( "path" ) ;
const fs = require ( "fs" ) ;
const questData3 _2 = "QuestExcelConfigData (3.2).json" ;
if ( ! fs . existsSync ( questData3 _2 ) ) {
2023-05-27 14:18:32 +08:00
console . log (
"Place a copy of QuestExcelConfigData for game version 3.2 in the same directory as this script."
) ;
console . log ( ` Name the file ${ questData3 _2 } . ` ) ;
return ;
2023-05-04 09:57:42 +08:00
}
2023-05-26 08:07:57 +08:00
const questPatchesDir = "Patches/Quest" ;
if ( ! fs . existsSync ( questPatchesDir ) ) {
2023-05-27 14:18:32 +08:00
console . log (
"Place a copy of the patches directory from the custom resources repository in the same directory as this script."
) ;
console . log ( "Ensure the custom resources has a 'Quest' directory." ) ;
return ;
2023-05-26 08:07:57 +08:00
}
// btw use `npx prettier --write .` in folder scene after patch
2023-05-04 09:57:42 +08:00
// Define constants.
const unknownCondition = {
2023-05-27 14:18:32 +08:00
type : "QUEST_COND_UNKNOWN" ,
param : [ 0 , 0 ] ,
2023-05-04 09:57:42 +08:00
} ;
const questBlacklist = [
2023-05-27 14:18:32 +08:00
"acceptCond" ,
"finishCond" ,
"failCond" ,
"beginExec" ,
"finishExec" ,
"failExec" ,
2023-05-04 09:57:42 +08:00
] ;
/ *
* These are quests patches which should be applied .
* These are ( basically ) applied last .
* Format is : { questId : { ( patches ) } }
* /
const patches = {
2023-05-27 14:18:32 +08:00
35402 : {
gainItems : [
{
itemId : 1021 ,
count : 1 ,
} ,
] ,
} ,
35104 : {
beginExec : [
{
type : "QUEST_EXEC_SET_IS_GAME_TIME_LOCKED" ,
param : [ "1" ] ,
param _str : "" ,
} ,
] ,
} ,
2023-05-04 09:57:42 +08:00
} ;
2023-05-26 08:07:57 +08:00
/ *
* These are main quest patches which should be applied .
* These are ( basically ) applied last .
* Format is : { mainId : { ( patches ) } }
* /
2023-05-27 14:18:32 +08:00
const mainPatches = { } ;
2023-05-26 08:07:57 +08:00
// Load quest patches from the patches directory.
const questPatches = fs . readdirSync ( questPatchesDir ) ;
for ( const questPatch of questPatches ) {
2023-05-27 14:18:32 +08:00
const patchData = JSON . parse (
fs . readFileSync ( path . join ( questPatchesDir , questPatch ) , "utf-8" )
) ;
const mainQuestId = patchData . id ;
// Check if the patch has sub-quests.
if ( patchData . subQuests ) {
for ( const quest of patchData . subQuests ) {
const { subId } = quest ;
// Clean the quest data.
delete quest . subId ;
delete quest . mainId ;
// Apply the patch.
patches [ subId ] = quest ;
2023-05-26 08:07:57 +08:00
}
2023-05-27 14:18:32 +08:00
}
2023-05-26 08:07:57 +08:00
2023-05-27 14:18:32 +08:00
delete patchData . id ;
delete patchData . subQuests ;
if ( Object . keys ( patchData ) . length > 0 ) {
mainPatches [ mainQuestId ] = patchData ;
}
2023-05-26 08:07:57 +08:00
}
2023-05-04 09:57:42 +08:00
/ * *
* Returns a cleaned version of a condition / execution .
*
* @ param condition The condition data .
* /
function clean ( condition ) {
2023-05-27 14:18:32 +08:00
let { type , param , param _str , count } = {
type : condition . _type ? ? condition . type ,
param : condition . _param ? ? condition . param ,
param _str : condition . _param _str ? ? condition . param _str ,
count : condition . _count ? ? condition [ "count" ] ,
} ;
const object = {
type ,
param : ! param
? [ ]
: param . filter ( ( param ) => param !== null && param !== "" ) ,
param _str : param _str ? ? "" ,
} ;
// Check for a 'count' parameter.
if ( count ) object . count = count ;
return object ;
2023-05-04 09:57:42 +08:00
}
/ * *
* Returns a cleaned version of the guide .
*
* @ param guide The guide data .
* /
function cleanGuide ( guide ) {
2023-05-27 14:18:32 +08:00
// Check for a param field.
if ( guide . param == null ) return guide ;
2023-05-04 09:57:42 +08:00
2023-05-27 14:18:32 +08:00
guide . param = guide . param . filter ( ( param ) => param !== null && param !== "" ) ;
2023-05-04 09:57:42 +08:00
2023-05-27 14:18:32 +08:00
return guide ;
2023-05-04 09:57:42 +08:00
}
/ * *
* Removes un - used fields from an object .
*
* @ param object The object .
* @ param blacklist Fields to ignore .
* /
function removeFields ( object , blacklist = [ ] ) {
2023-05-27 14:18:32 +08:00
for ( const field in object ) {
if ( blacklist . includes ( field ) ) continue ;
2023-05-04 09:57:42 +08:00
2023-05-27 14:18:32 +08:00
if ( object [ field ] == null ) delete object [ field ] ;
if ( Array . isArray ( object [ field ] ) ) {
if ( object [ field ] . length === 0 ) delete object [ field ] ;
2023-05-04 09:57:42 +08:00
}
2023-05-27 14:18:32 +08:00
}
2023-05-04 09:57:42 +08:00
}
const binOutput = path . join ( _ _dirname , "../Resources/BinOutput/Quest" ) ;
2023-05-27 14:18:32 +08:00
const fileOutput = path . join (
_ _dirname ,
"../Resources/ExcelBinOutput/QuestExcelConfigData.json"
) ;
const mainQuestFile = path . join (
_ _dirname ,
"../Resources/ExcelBinOutput/MainQuestExcelConfigData.json"
) ;
const talkFile = path . join (
_ _dirname ,
"../Resources/ExcelBinOutput/TalkExcelConfigData.json"
) ;
2023-05-04 09:57:42 +08:00
// Load the data from the files.
const rel3 _2 = fs . readFileSync ( questData3 _2 , "utf-8" ) ;
const latest = fs . readFileSync ( fileOutput , "utf-8" ) ;
const mainQuest = fs . readFileSync ( mainQuestFile , "utf-8" ) ;
const talks = fs . readFileSync ( talkFile , "utf-8" ) ;
// Parse the data into JSON.
/** @type array */
const rel3 _2 _data = JSON . parse ( rel3 _2 ) ;
/** @type array */
const latest _data = JSON . parse ( latest ) ;
/** @type array */
const mainQuest _data = JSON . parse ( mainQuest ) ;
/** @type array */
const talks _data = JSON . parse ( talks ) ;
// Merge the data.
const quests = [ ] ;
2023-05-04 11:40:41 +08:00
const newQuests = [ ] ;
2023-05-26 18:58:49 +08:00
const newQuestsNoFound = [ ] ;
2023-05-04 09:57:42 +08:00
for ( const mainQuestData of mainQuest _data ) {
2023-05-27 14:18:32 +08:00
const mainQuestId = mainQuestData . id ;
const binfile = ` ${ binOutput } / ${ mainQuestId } .json ` ;
console . log ( ` Scanning main quest ${ mainQuestId } ... ` ) ;
// Find all sub-quests for the main quest.
let isNewQuest = false ;
let SaveBin = true ;
let subQuests = rel3 _2 _data . filter ( ( quest ) => quest . mainId === mainQuestId ) ;
if ( subQuests . length === 0 ) {
2023-05-27 15:06:47 +08:00
// This will be considered a new quest if no quest configuration is found in version 3.2. based on mainQuestData file data
2023-05-27 14:18:32 +08:00
isNewQuest = true ;
2023-05-27 15:06:47 +08:00
// since not all new quests are in `QuestExcelConfigData` we have to look again in `Quest bin folder` so both should be there. and sometimes the `Quest Bin Folder` doesn't have new quest data so we have to look in `quest main` or `quest config` in `Excel folder`
subQuests = latest _data . filter ( ( quest ) => quest . mainId === mainQuestId ) ;
2023-05-27 14:18:32 +08:00
if ( subQuests . length === 0 ) {
2023-05-27 15:06:47 +08:00
console . log ( ` Looking for alternatives quest sub ${ binfile } ` ) ;
2023-05-27 14:18:32 +08:00
const binsub _r = fs . readFileSync ( binfile ) ;
const binsub _d = JSON . parse ( binsub _r ) ;
let subQuestBin = binsub _d . subQuests ;
if ( subQuestBin !== undefined ) {
2023-05-27 15:06:47 +08:00
subQuests = subQuestBin ; // copy `subquest bin` to `subquest config`
SaveBin = false ; // meanwhile don't save quest data bin because data we use data bin so should be same unless is patched?.
2023-05-27 14:18:32 +08:00
}
}
if ( subQuests . length !== 0 ) {
newQuests . push ( mainQuestId ) ;
} else {
newQuestsNoFound . push ( mainQuestId ) ;
}
}
// Find all talks for the main quest.
const talks = talks _data . filter ( ( talk ) => talk . questId === mainQuestId ) ;
console . log ( "=====================================" ) ;
console . log ( ` Performing merge on main quest ${ mainQuestId } . ` ) ;
console . log ( ` This quest is ${ isNewQuest ? "new" : "old" } . ` ) ;
console . log ( ` There are ${ subQuests . length } sub-quests. ` ) ;
console . log ( ` There are ${ talks . length } talks. ` ) ;
console . log ( "=====================================" ) ;
// Check if the quest has sub-quests.
if ( subQuests . length === 0 ) {
console . log ( ` Main quest ${ mainQuestId } has no sub-quests, skipping... ` ) ;
continue ;
}
// Create the base quest data.
const quest = {
/** @type number */ id : mainQuestId ,
/** @type string */ type : mainQuestData . type ,
/** @type number */ series : mainQuestData . series ,
/** @type number */ titleTextMapHash : mainQuestData . titleTextMapHash ,
/** @type number */ descTextMapHash : mainQuestData . descTextMapHash ,
/** @type string */ luaPath : mainQuestData . luaPath ,
/** @type string */ showType : mainQuestData . showType ,
/** @type number[] */ suggestTrackMainQuestList :
mainQuestData . suggestTrackMainQuestList ,
/** @type number[] */ rewardIdList : mainQuestData . rewardIdList ,
/** @type number[] */ chapterId : mainQuestData . chapterId ,
/** @type any[] */ subQuests : [ ] ,
/** @type any[] */ talks : [ ] ,
} ;
// Create sub-quests for the main quest.
for ( const subQuestData of subQuests ) {
const subQuest = {
json _file : ` ${ mainQuestId } .json ` ,
... subQuestData ,
} ;
2023-05-26 18:58:49 +08:00
2023-05-27 14:18:32 +08:00
// Validate conditions.
const {
/** @type any[] */ acceptCond ,
/** @type any[] */ finishCond ,
/** @type any[] */ failCond ,
} = subQuestData ;
if ( acceptCond ) {
subQuest . acceptCond = acceptCond
2023-05-27 15:06:47 +08:00
. filter ( ( cond ) => cond . _type !== null || cond . type !== null )
2023-05-27 14:18:32 +08:00
. map ( clean ) ;
}
if ( finishCond ) {
subQuest . finishCond = finishCond
2023-05-27 15:06:47 +08:00
. filter ( ( cond ) => cond . _type !== null || cond . type !== null )
2023-05-27 14:18:32 +08:00
. map ( clean ) ;
}
if ( failCond ) {
subQuest . failCond = failCond
2023-05-27 15:06:47 +08:00
. filter ( ( cond ) => cond . _type !== null || cond . type !== null )
2023-05-27 14:18:32 +08:00
. map ( clean ) ;
}
2023-05-04 09:57:42 +08:00
2023-05-27 14:18:32 +08:00
// Validate executions.
const {
/** @type any[] */ beginExec ,
/** @type any[] */ finishExec ,
/** @type any[] */ failExec ,
} = subQuestData ;
if ( beginExec ) {
subQuest . beginExec = beginExec
2023-05-27 15:06:47 +08:00
. filter ( ( cond ) => cond . _type !== null || cond . type !== null )
2023-05-27 14:18:32 +08:00
. map ( clean ) ;
}
if ( finishExec ) {
subQuest . finishExec = finishExec
2023-05-27 15:06:47 +08:00
. filter ( ( cond ) => cond . _type !== null || cond . type !== null )
2023-05-27 14:18:32 +08:00
. map ( clean ) ;
}
if ( failExec ) {
subQuest . failExec = failExec
2023-05-27 15:06:47 +08:00
. filter ( ( cond ) => cond . _type !== null || cond . type !== null )
2023-05-27 14:18:32 +08:00
. map ( clean ) ;
}
// Check if the quest is new.
if ( isNewQuest ) {
// Add the unknown accept condition.
if ( subQuestData . acceptCond == undefined ) {
subQuestData . acceptCond = [ unknownCondition ] ;
// Create an unknownCondition if acceptCond is undefined
} else {
subQuestData . acceptCond . push ( unknownCondition ) ;
2023-05-26 18:58:49 +08:00
}
2023-05-27 14:18:32 +08:00
if ( subQuest . acceptCond == undefined ) {
subQuest . acceptCond = [ unknownCondition ] ;
2023-05-26 18:58:49 +08:00
} else {
2023-05-27 14:18:32 +08:00
subQuest . acceptCond . push ( unknownCondition ) ;
2023-05-26 18:58:49 +08:00
}
2023-05-04 09:57:42 +08:00
}
2023-05-27 14:18:32 +08:00
// fix (Expected a string but was BEGIN_OBJECT)
if ( typeof subQuestData . acceptCondComb === "object" ) {
subQuestData . acceptCondComb = "LOGIC_NONE" ; // ???
}
if ( typeof subQuest . acceptCondComb === "object" ) {
subQuest . acceptCondComb = "LOGIC_NONE" ; // ???
}
2023-05-04 09:57:42 +08:00
2023-05-27 14:18:32 +08:00
if ( typeof subQuestData . finishCondComb === "object" ) {
subQuestData . finishCondComb = "LOGIC_NONE" ; // ???
}
if ( typeof subQuest . finishCondComb === "object" ) {
subQuest . finishCondComb = "LOGIC_NONE" ; // ???
}
2023-05-04 09:57:42 +08:00
2023-05-27 14:18:32 +08:00
if ( typeof subQuestData . failCondComb === "object" ) {
subQuestData . failCondComb = "LOGIC_NONE" ; // ???
}
if ( typeof subQuest . failCondComb === "object" ) {
subQuest . failCondComb = "LOGIC_NONE" ; // ???
}
2023-05-04 09:57:42 +08:00
2023-05-27 14:18:32 +08:00
// fix null
if ( subQuestData . finishCond == null ) {
subQuestData . finishCond = [ ] ; // ???
}
if ( subQuest . finishCond == null ) {
subQuest . finishCond = [ ] ; // ???
}
if ( subQuestData . failCond == null ) {
subQuestData . failCond = [ ] ; // ???
}
if ( subQuest . failCond == null ) {
subQuest . failCond = [ ] ; // ???
2023-05-04 09:57:42 +08:00
}
2023-05-27 14:18:32 +08:00
if ( subQuestData . beginExec == null ) {
subQuestData . beginExec = [ ] ; // ???
}
if ( subQuest . beginExec == null ) {
subQuest . beginExec = [ ] ; // ???
}
if ( subQuestData . finishExec == null ) {
subQuestData . finishExec = [ ] ; // ???
}
if ( subQuest . finishExec == null ) {
subQuest . finishExec = [ ] ; // ???
}
if ( subQuestData . failExec == null ) {
subQuestData . failExec = [ ] ; // ???
}
if ( subQuest . failExec == null ) {
subQuest . failExec = [ ] ; // ???
}
// Validate the quest guide.
const { guide } = subQuestData ;
// || guide.type !== null
if ( guide !== undefined && guide . type !== undefined ) {
subQuest . guide = cleanGuide ( guide ) ;
} else subQuest . guide = { } ;
2023-05-04 09:57:42 +08:00
2023-05-27 14:18:32 +08:00
// Remove fields which are empty.
removeFields ( subQuest , questBlacklist ) ;
// Check if the quest has any patches.
if ( patches [ subQuestData . subId ] ) {
Object . assign ( subQuest , patches [ subQuestData . subId ] ) ;
2023-05-26 08:07:57 +08:00
}
2023-05-27 14:18:32 +08:00
// Add to the main quest's collection.
const subQuestForMain = Object . assign ( { } , subQuest ) ;
delete subQuestForMain . json _file ;
delete subQuestForMain . stepDescTextMapHash ;
delete subQuestForMain . guideTipsTextMapHash ;
quest . subQuests . push ( subQuestForMain ) ;
// Add to the global collection.
quests . push ( subQuest ) ;
}
// Create talks for the main quest.
for ( const talkData of talks ) {
const talk = {
... talkData ,
} ;
// Validate conditions.
const {
/** @type any[] */ beginCond ,
/** @type any[] */ finishCond ,
/** @type any[] */ failCond ,
} = talkData ;
if ( beginCond ) {
talk . beginCond = beginCond . filter ( ( cond ) => cond . type != null ) . map ( clean ) ;
}
if ( finishCond ) {
talk . finishCond = finishCond
. filter ( ( cond ) => cond . type != null )
. map ( clean ) ;
}
if ( failCond ) {
talk . failCond = failCond . filter ( ( cond ) => cond . type != null ) . map ( clean ) ;
}
// Validate executions.
const {
/** @type any[] */ beginExec ,
/** @type any[] */ finishExec ,
/** @type any[] */ failExec ,
} = talkData ;
if ( beginExec ) {
talk . beginExec = beginExec . filter ( ( cond ) => cond . type != null ) . map ( clean ) ;
}
if ( finishExec ) {
talk . finishExec = finishExec
. filter ( ( cond ) => cond . type != null )
. map ( clean ) ;
2023-05-26 18:58:49 +08:00
}
2023-05-27 14:18:32 +08:00
if ( failExec ) {
talk . failExec = failExec . filter ( ( cond ) => cond . type != null ) . map ( clean ) ;
}
// Remove un-used fields.
removeFields ( talk ) ;
// Add to the main quest's collection.
quest . talks . push ( talk ) ;
}
// Remove un-used fields.
removeFields ( quest ) ;
// Check if the main quest has any patches.
if ( mainPatches [ quest . id ] ) {
Object . assign ( quest , mainPatches [ quest . id ] ) ;
}
// Create the main quest file.
2023-05-27 15:06:47 +08:00
fs . writeFileSync ( binfile , JSON . stringify ( quest , null , 2 ) ) ;
2023-05-04 09:57:42 +08:00
}
// Write the new quest data.
2023-05-27 14:18:32 +08:00
fs . writeFileSync ( fileOutput , JSON . stringify ( quests , null , 2 ) ) ;
2023-05-04 11:40:41 +08:00
console . log ( "=====================================" ) ;
console . log ( ` There are ${ quests . length } quests. ` ) ;
console . log ( ` There are ${ newQuests . length } new quests. ` ) ;
2023-05-27 15:06:47 +08:00
console . log ( ` There are ${ newQuestsNoFound . length } quests not found. ` ) ;
2023-05-04 11:40:41 +08:00
for ( let i = 0 ; i < newQuests . length ; i += 9 ) {
2023-05-27 14:18:32 +08:00
const newQuestsSlice = newQuests . slice ( i , i + 9 ) ;
console . log ( ` New quests: ${ newQuestsSlice . join ( ", " ) } ` ) ;
2023-05-04 11:40:41 +08:00
}
console . log ( "=====================================" ) ;