Created
June 15, 2024 00:30
-
-
Save phecdaDia/294f8ed1c67eae984103e12f8fac6da0 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* Simple ModLoader for Isekainosouzousha (異世界の創造者) | |
* | |
* This modloader is NOT supported by the original developers of the game and is | |
* only made out of love and enjoyment for the game. | |
* | |
* Add this snippet to the union.js file to load the modloader: | |
``` | |
$(document).ready(function () { | |
console.log("We're now running custom code"); | |
// Load a new .js file named modloader.js | |
var files = ['modloader.js']; | |
for (var file of files) { | |
var script = document.createElement('script'); | |
script.setAttribute('src', './js/game/' + file); | |
script.onload = function () { | |
console.log(`${file} has been injected!`); | |
} | |
document.head.appendChild(script); | |
} | |
}); | |
``` | |
*/ | |
(function() { | |
// Create a modloader object in the global scope if none exists yet | |
if ((typeof window.tModLoader) !== 'undefined') { | |
// We already have a modmenu object, so we can exit this script | |
console.error('Modloader already exists in the global scope'); | |
console.error('Please make sure to only include this script once'); | |
console.error('Exiting script...'); | |
return; | |
} | |
// Create a new mod loader object | |
// Using prototypes to make it easier to use | |
function tModLoader() { | |
// Initialize the modmenu object | |
this.init(); | |
} | |
tModLoader.prototype = { | |
init: function() { | |
// Initialize the modmenu object | |
this.version = (0, 1, 0); | |
}, | |
/** | |
* Create a new hook into an object's method using prototype pollution | |
* | |
* When overriding the method, please make sure to call the original method from within the hook | |
* @param {*} obj | |
* @param {string} method_name | |
* @param {function} hook | |
*/ | |
hook_prototype: function(obj, method_name, hook) { | |
// Save the original method | |
const original = obj.__proto__[method_name]; | |
// Replace the original method with the hook | |
obj.__proto__[method_name] = function(...args) { | |
// Log the method call | |
console.log(`Calling ${method_name} with arguments:`, args); | |
// If the hook is undefined, null or otherwise falsey, call the original method instead | |
try { | |
if (typeof hook === 'function') { | |
return hook(this, original, args); | |
} else { | |
return original.apply(this, args); | |
} | |
} catch (err) { | |
// Either we screwed something up or the game just throws an error with the function | |
// naturally. Either way we don't want the game to just crash because of it. | |
console.error(err); | |
} | |
} | |
}, | |
/** | |
* Logs all method calls of the object | |
* @param {*} obj | |
*/ | |
hook_all_prototypes: function(obj) { | |
// Hook all prototypes of the object to log the method calls | |
for (let method_name of Object.getOwnPropertyNames(obj.__proto__)) { | |
const original = obj.__proto__[method_name]; | |
obj.__proto__[method_name] = function(...args) { | |
console.log(`Calling ${method_name} with arguments:`, args); | |
return original.apply(this, args); | |
} | |
} | |
}, | |
/** | |
* Some functions may throw errors and thus crash the game. This function | |
* insulates the function by catching the error and logging it instead. | |
* | |
* This is not a substitute for proper error handling, but it can be useful | |
* for quick debugging. | |
* | |
* A default return value can be provided in case the function would normally return something. | |
* @param {*} obj | |
* @param {string} method_name | |
* @param {*} default_return | |
*/ | |
make_safe: function(obj, method_name, default_return) { | |
// Insulate a function, using prototype pollution to make it safe | |
const original = obj.__proto__[method_name]; | |
obj.__proto__[method_name] = function(...args) { | |
try { | |
return original.apply(this, args); | |
} catch (err) { | |
console.error(err); | |
// If this returned something normally we now have a problem, maybe. | |
return default_return; | |
} | |
} | |
}, | |
/** | |
* Once the game is loaded, call the callback(s) to execute. | |
* The callbacks will only be executed ONCE | |
* @param {function} callback | |
*/ | |
once_loaded: function(callback) { | |
this.loaded_callbacks = this.loaded_callbacks || []; | |
this.loaded_callbacks.push(callback); | |
console.log('Added callback to loaded_callbacks:', callback); | |
}, | |
call_loaded_callbacks: function() { | |
console.log('Calling loaded_callbacks:', this.loaded_callbacks); | |
this.loaded_callbacks = this.loaded_callbacks || []; | |
for (var callback of this.loaded_callbacks) { | |
callback(this); | |
} | |
} | |
}; | |
// Make the modloader object available in the global scope | |
window.tModLoader = new tModLoader(); | |
// Log that the modloader is loaded | |
console.log('Modloader loaded:', window.tModLoader); | |
})(); | |
/* | |
Example usage of the modloader | |
------------------------------ | |
Patch a couple of problematic functions once the game is loaded. | |
These patches are purely bug fixes and do not add any new functionality. | |
*/ | |
window.tModLoader.once_loaded(function(modloader) { | |
console.log('The game has been loaded, patching some functions now...'); | |
// Patch some functions here to provide additional functionality | |
// tWgm.tGameItem.getItemName has a bug where it sometimes throws an error. | |
// Adding a default return value to make it safe | |
modloader.make_safe(tWgm.tGameItem, 'getItemName', 'Unknown Item'); | |
}); | |
window.tModLoader.once_loaded(function(modloader) { | |
console.log('This is from the modloader, we\'re loaded!'); | |
// Using prototype pollution to hook into the game | |
modloader.hook_prototype(tWgm.tGameMenu, 'viewMenu'); | |
modloader.hook_prototype(tWgm.tGameCharactor, 'addItem') | |
//hook_all_prototypes(tWgm.tGameCharactor); | |
// Hook tGameMenu prototypes to potentially make our own menu | |
modloader.hook_prototype(tWgm.tGameMessageWindow, 'viewMessageWindow', function(self, original, args) { | |
// Attempt to isolate the escape menu | |
var answers = args[0].answers; | |
// Check if we have an option for 'Return to title screen' | |
var isMainMenu = answers.find(answer => answer.message === tWgm.tGameTalkResource.talkData.system.gototitle) !== undefined; | |
isMainMenu &&= answers.find(answer => answer.message === tWgm.tGameTalkResource.talkData.system.itemmiru) !== undefined; | |
// If we have the main menu, we can potentially replace it with our own. | |
// For now just add a dummy button which logs a message | |
if (isMainMenu) { | |
answers.push({ | |
message: 'Open ModMenu', | |
callBack: function() { | |
// Show a new menu | |
var answers = []; | |
// Add a dummy button | |
answers.push({ | |
message: 'Dummy button', | |
callBack: function() { | |
console.log('Dummy button pressed'); | |
tWgm.tGameRoutineMap.setFrameAction(tWgm); | |
} | |
}); | |
answers.push({ | |
message: 'tGameNumWindow', | |
callBack: function() { | |
tWgm.tGameSoundResource.play('select'); | |
tWgm.tGameNumWindow.viewNumWindow({ | |
label: tWgm.tGameTalkResource.talkData.system.ikutsuoku, | |
startNum: 1, | |
minNum: 1, | |
maxNum: 999999, | |
isSelectSound: false, | |
selectCallBack: function(num) { | |
console.log('Selected number:', num); | |
tWgm.tGameRoutineMap.setFrameAction(tWgm); | |
}, | |
cancelCallBack: function() { | |
console.log('Cancelled number selection'); | |
tWgm.tGameRoutineMap.setFrameAction(tWgm); | |
} | |
}); | |
} | |
}); | |
answers.push({ | |
message: 'tGameItemWindow', | |
callBack: function () { | |
// Go through the object `tWgm.tGameItem.masterData.items` to get all items | |
var all_items = []; | |
// Structure of `tWgm.tGameItem.masterData.items`: | |
// { item_id: [ some_data, some_data, ... ], ... } | |
for (var item_id in tWgm.tGameItem.masterData.items) { | |
var master_data = tWgm.tGameItem.masterData.items[item_id]; | |
var item_data = tWgm.tGameItem.createItem({ | |
itemId: item_id, | |
isShikibetsu: true, // Is identified item | |
isNoroi: false // Is cursed item | |
}); | |
all_items.push(item_data); | |
} | |
tWgm.tGameItemWindow.viewItemWindow({ | |
label: "This is the label, whatever that means", | |
items: all_items, | |
isSelectClear: !1, | |
isViewPrice: !1, | |
isSelectSound: !1, | |
isSell: !1, | |
selectItemCallBack: function(index) { | |
console.log('Selected item index:', index); | |
var item = all_items[index]; | |
// Add the item to the player inventory?! | |
// tWgm.tGameCharactor.addItem("player", item, 1); | |
tWgm.tGameNumWindow.viewNumWindow({ | |
label: tWgm.tGameTalkResource.talkData.system.ikutsuoku, | |
startNum: 1, | |
minNum: 1, | |
maxNum: 999999, | |
isSelectSound: false, | |
selectCallBack: function(num) { | |
console.log('Selected number:', num); | |
// If it's a money item, we need to use addMoney | |
if (item[1] >= 99999990001) { | |
tWgm.tGameCharactor.addMoney(tWgm.tGameCharactor.charas.player, num, -1, item[1]); | |
} else { | |
item[10] = num; | |
tWgm.tGameCharactor.addItem("player", item, 1); | |
} | |
tWgm.tGameItemWindow.clear(); | |
tWgm.tGameRoutineMap.setFrameAction(tWgm); | |
}, | |
cancelCallBack: function() { | |
console.log('Cancelled number selection'); | |
} | |
}); | |
}, | |
cancelCallBack: function() { | |
console.log('Cancelled item selection'); | |
tWgm.tGameRoutineMap.setFrameAction(tWgm); | |
}, | |
bottomData: tWgm.tGameItemWindow.getCharactorBottomData(tWgm.tGameCharactor.charas.player) | |
}); | |
} | |
}) | |
// Display the ModMenu | |
tWgm.tGameMessageWindow.viewMessageWindow({ | |
selectedIndex: 0, | |
answers: answers, | |
viewItemMaxNum: 12, | |
position: [null, args[0].position[1]], | |
cancelCallBack: function() { | |
console.log('ModMenu closed due to `cancelCallBack`'); | |
tWgm.tGameRoutineMap.setFrameAction(tWgm); | |
}, | |
}); | |
} | |
}); | |
} | |
original.apply(self, args); | |
}); | |
}); | |
// Enable native logging | |
tWgm.isL = true; | |
// Wait for the game to load... | |
setTimeout((() => window.tModLoader.call_loaded_callbacks()), 1000); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment