Preview
About Game
Interactive Masturbation Support Game: Transforming a Pure Heroine into a Woman while battling the Sensation of Ejaculation
Mod Info
A Tyranoscript mod that turned out to be a bit more challenging, since the game uses Live2D elements for some of the animations.
Big thanks to everyone who contributed over at [Game Integration] Modding Tyranoscript Games, especially @affqprow who provided essential parts of the code and @Falafel for making the scripts .
The mod also utilizes @dimnogro’s EDI.
Here are some more details, for those interested in the integration / using EDI with similar games:
Integration Details
First the definitions.csv and scripts have to be created. Dimnogro already explains this step well in the original Topic post: Easy Device Integration for Games. EDI Update [12/2023]. The Topic also contains a lot of instructions and helpful details, so definitely check that out first!
Using the EDI was actually fairly similar to how you would use the FunscriptPlayer integration, since it also utilizes HTTP calls.
To start with, we’ll need to define an “EDIPlayer” Object, which for Tyranoscript games (in the “kag.tag_ext.js”), and JavaScript integrations in general, would look something like this:
// ---- ENVIRONMENT & LOGGING SETUP----
var fs = require('fs');
var currentAnimation = null;
var AnimationPlaying = false;
// Polyfill for Object.fromEntries()
if (!Object.fromEntries) {
Object.fromEntries = function (entries) {
var obj = {};
for (var i = 0; i < entries.length; i++) {
var key = entries[i][0];
var value = entries[i][1];
obj[key] = value;
}
return obj;
};
}
// Function to log messages to "EDI-Debug.txt" with local machine's time
var logBuffer = '';
var logTimer = null;
function logToFile(message) {
var timestamp = new Date(); // Gets the current time
var localTimestamp = timestamp.toLocaleString(); // Converts to local timezone format
var milliseconds = timestamp.getMilliseconds(); // Get milliseconds
var logMessage = '[' + localTimestamp + '.' + milliseconds + '] ' + message + '\n';
logBuffer += logMessage;
// Write the logs at certain time intervals
if (!logTimer) {
logTimer = setTimeout(function () {
fs.appendFile('EDI-Debug.txt', logBuffer, function (err) {
if (err) throw err;
});
logBuffer = '';
logTimer = null;
}, 50); // Increase when "enableGalleryNameLogging" is turned on, to reduce load on game
}
}
// ---- DEFINE EDIPLAYER OBJECT ----
var EDIPlayer = {
// Configuration:
baseUrl: 'http://localhost:5000/Edi/', // base URL for the EDI server
enableLogging: true, // Set logging for "EDI-Debug.txt"
enableGalleryNameLogging: true, // Option to set additioal gallery-name logging
// Helper function to extract gallery name from a full file name
filterGalleryName: function (galleryName) {
return galleryName.substr(0, galleryName.lastIndexOf('.')) || galleryName; // remove file extension
},
// Make an HTTP request
makeRequest: function (url, options) {
if (!this.enableLogging) {
options.logMessage = false;
}
try {
var requestStartTime = new Date();
var xhr = new XMLHttpRequest();
xhr.open(options.method || 'POST', url);
for (var header in options.headers) {
if (options.headers.hasOwnProperty(header)) {
xhr.setRequestHeader(header, options.headers[header]);
}
}
xhr.onreadystatechange = function () {
if (xhr.readyState === XMLHttpRequest.DONE) {
var requestEndTime = new Date();
var requestTimeMs = requestEndTime - requestStartTime;
if (options.logMessage !== false) {
var logMessage = '[EDIPlayer.makeRequest] URL: ' + url + '\n';
logToFile(logMessage);
var requestInfo = 'Request Method: ' + (options.method || 'POST') + '\n' +
'Request Headers: ' + JSON.stringify(options.headers || { 'accept': '*/*' }) +
(options.body ? 'Request Body: ' + JSON.stringify(options.body) + '\n' : '');
logToFile(requestInfo);
var responseInfo = 'Response Status: ' + xhr.status + ' ' + xhr.statusText + '\n' +
'Response Headers: ' + JSON.stringify(xhr.getAllResponseHeaders()) + '\n';
if (xhr.responseText) {
logToFile(responseInfo);
logToFile('Response Body: ' + xhr.responseText + '\n');
} else {
logToFile(responseInfo);
}
if (xhr.status >= 200 && xhr.status < 300) {
var successMsg = 'Success: ' + xhr.statusText + ' (Request completed in ' + requestTimeMs + 'ms)\n\n';
console.log(successMsg);
logToFile(successMsg);
} else {
var errorMsg = 'Failed: ' + xhr.status + ' ' + xhr.statusText + ' EDI-Server not reachable \n';
console.error(errorMsg);
logToFile(errorMsg);
}
}
}
};
xhr.send(options.body || null);
} catch (error) {
if (options.logMessage !== false) {
var errorMsg = 'Error: ' + error + '\n';
console.error(errorMsg);
logToFile(errorMsg);
}
}
},
// Functions to control gallery playback
playGallery: function (galleryName, seek) {
if (!AnimationPlaying || currentAnimation !== galleryName) {
if (!seek) seek = 0;
var filteredName = this.filterGalleryName(galleryName);
var url = this.baseUrl + 'Play/' + filteredName + '?seek=' + seek;
this.makeRequest(url, { method: 'POST', logMessage: true });
AnimationPlaying = true;
currentAnimation = galleryName;
}
},
stopGallery: function () {
if (AnimationPlaying) {
var url = this.baseUrl + 'Stop';
this.makeRequest(url, { method: 'POST', logMessage: true });
AnimationPlaying = false;
currentAnimation = null;
}
},
pauseGallery: function () {
var url = this.baseUrl + 'Pause';
this.makeRequest(url, { method: 'POST', logMessage: true });
},
resumeGallery: function () {
var url = this.baseUrl + 'Resume';
this.makeRequest(url, { method: 'POST', logMessage: true });
},
};
After defining the Object, we can call any functions within it at the right part of the code and reference the corresponding “gallery” variable. Example from this game:
// Play the scipt when the animation is called in the code
r.addEventListener("play", function(event) {
EDIPlayer.playGallery(e.storage); // where "e.storage" is used to call the animation
});
r.addEventListener("seeked", function(event) {
EDIPlayer.playGallery(e.storage);
});
// Stop script after animation is finished
r.addEventListener("pause", function(event) {
EDIPlayer.stopGallery();
});
I’ve implemented a simple logging function that writes the current requests and galleryname into a text file.
This will enable us to check if the integration is working and what galleryname is being pulled from the code, which should be especially helpful for integration in other games/engines.
The “EDI-Debug.txt” file will the saved in the same folder the EDI.exe is in.
Live2D animations will be handled differently in most cases, so we need to add some additional code if the game has them as well. In this case the motions where called in the Live2D “index.js”.
Since the game is calling multiple Live2D elements at the same time, we have to filter only the ones we want the EDI to detect.
I also changed the GalleryName that the game calls, by replacing certain parts, since the original one was quite long and contained a few file extensions. You could pretty much adjust the name however you like, as long as it is the same as what’s set in the definitions.csv:
// EDI-Intecept Live-2D Animation
var modelFileName = this._modelSetting.getModelFileName();
// Define all the scenes we'd like to use / skip all unwanted element calls
const keepStrings = ['Doris_blowjob', 'Doris_ride'];
if (keepStrings.some(str => modelFileName.startsWith(str))) {
// Remove all unwanted parts from the filename
const cleanedFileName = keepStrings.reduce((acc, str) => acc.replace(new RegExp(str, 'g'), ''), modelFileName);
n = n.replace("motion/", "").replace(".motion3", "").replace(/[\\\/]/g, '');
// Play script based on filename
EDIPlayer.playGallery(`${cleanedFileName.replace(".moc3", "")}${n}`);
}
// ___END___
That’s pretty much the gist of it. EDI should now be able to intercept the animation calls and receive the corresponding gallery name to play the script.
I also made a full English translation for the game, including all the UI-Elements, and subtitles for the voice lines.
A small bug the original game had, where it was only possible to change one part of the settings at a time, was also fixed.
This is all already included in the mod file.
Notes
-
I tried to translate the game as well as possible, but since I don’t speak Japanese, some of the translation might not be entirely accurate.
The game features more than 91 unique voice lines, accompanied by over 500 lines of dialogue that had to be integrated into the TyrenoBuilder code. If you notice any issues, please provide feedback. -
Tested with the Handy on game v1.23
-
If you enjoyed this game, you might like the previous entry as well:
Big thanks to @Falafel for providing scripts for all the scenes:
https://discuss.eroscripts.com/t/mountbatten-aesops-fables-live2d-scripts-for-modding/120670
and @dimnogro for the EDI program:
How to use
-
Step 0. Get the game - [Live2Dで動くイソップ寓話 ]
-
Step 1. Download and extract the Mod into the game’s main directory (the patch file “Aesops_Fables.tpatch” should be next to the .exe)
-
Step 2. On first launch, the game might load for a while (don’t panic) .
Afterwards, you should see the following message, meaning the patch was applied successfully! (the .tpatch file will also automatically get deleted)
Important: Wait a few more minutes (~5min) before pressing “OK”, as the game is still generating files in the background and won’t be able to launch otherwise.
-
Step 3. Launch “Edi.exe” and use your Handy key to connect over WIFI.
You can also use the Intiface Bluetooth connection for the Handy and other devices as well.
Download Links
Integration Mod:
If you want to modify the funscripts, here are the reference videos
Game:
Changelog
- 25.09.2023 - Added Japanese version of the Mod
- 24.09.2023 - v1.0 Initial release