Skip to content

Instantly share code, notes, and snippets.

@phecdaDia
Created June 15, 2024 00:30
Show Gist options
  • Save phecdaDia/294f8ed1c67eae984103e12f8fac6da0 to your computer and use it in GitHub Desktop.
Save phecdaDia/294f8ed1c67eae984103e12f8fac6da0 to your computer and use it in GitHub Desktop.
/**
* 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