Lindo - Open Source Emulator for Dofus Touch
- Published on
- Role
- Main Developer
Implementation
Dofus Touch is a game developed by Ankama, using JavaScript for its graphics engine and packaged with Cordova
as a mobile application for iOS and Android. To allow PC players to access the game, I developed an emulator using Electron
that recreates the web app environment while adding features like keyboard and mouse support for interacting with the game.
For the emulator interface, the project uses React
and the MUI
library to implement Material Design components. Vite
is employed to bundle the various parts of the application for release. One of Vite's key advantages over Webpack is its configurability, making it suitable for both Node.js (Main Process) and web (Renderer Process) contexts.
The application’s codebase is divided into five packages
, each with its own Vite configuration for bundling during the build process.
I18n
Ce package contient l'entièreté des fichiers de langues de l'application. La traduction de l'application est réalisé avec typesafe-i18n
qui permet de fournir une api de traduction pour un context node.js (Main-Process) et web (Renderer Process).
// Example of a translation file
const en: BaseTranslation = {
example: {
helloWorld: "Hello World",
}
}
// Example of usage of the traduction inside a React Component
export const ExampleComponent = ({ onSkip, onUnlock }: UnlockFormProps) => {
// retrieve the i18n context
const { LL } = useI18nContext()
return (
<div>
<Typography>{LL.example.helloWorld()}</Typography>
</div>
)
}
// Example of usage of the traduction inside a React Component
export const exampleFunction = () => {
return i18n.example.helloWorld();
}
Main
This package contains all the code executed in a Node.js context (without web rendering), allowing access to native system APIs such as window management, local file handling, and downloading updates.
Preload
This package contains code that is pre-rendered before the main view is loaded.
Renderer
This package includes the interface code executed in a web-rendering context.
Shared
This package contains code shared between the different packages (mainly renderer
and main
). The shared code primarily consists of MobX models, along with classes, interfaces, and types.
Synchronization Between Game Windows
In Electron, each rendering window and the main process
run in separate contexts. One way to facilitate communication between these processes is by using theIPC
(Inter-Process Communication) API. However, for managing application state, I use a library called mobx-state-tree
, which provides immutable stores similar to redux. These stores are used to manage emulator options.
A challenge arises in synchronizing the stores across processes to ensure they contain identical values. If a value changes in one process's store, other processes must also reflect that change.
To address this, I developed a wrapper around the root store (the main store containing all others) in the main process. This wrapper broadcasts any modifications made to the store to all renderer processes. Additionally, I created a wrapper for the root store in the renderer processes. Whenever a local modification is made to their store, it is sent to the main store. The renderer's wrapper also retrieves the current state of the main process's root store at startup to initialize its own root store.
// setup-root-store.ts (main process)
export async function setupRootStore(): Promise<RootStore> {
// create the root store instance
const rootStore: Instance<typeof RootStoreModel> = RootStoreModel.create({}, env)
const patchesFromRenderer: Array<string> = []
// Return the current value of the root store when a renderer is initialized
ipcMain.on(IPCEvents.INIT_STATE, (event) => {
event.returnValue = JSON.stringify(getSnapshot(rootStore))
})
// When receiving a patch modification from a renderer
ipcMain.on(IPCEvents.PATCH, (event, patch: IJsonPatch) => {
// save the patches in a list
patchesFromRenderer.push(hash(patch))
// apply the patch to the root store
applyPatch(rootStore, patch)
// Forward it to all of the other renderers
webContents.getAllWebContents().forEach((contents) => {
// Ignore the renderer that sent the action and chromium devtools
if (contents.id !== event.sender.id && !contents.getURL().startsWith('devtools://')) {
contents.send(IPCEvents.PATCH, patch)
}
})
})
// This function is execute every time there is change on the root store
onPatch(rootStore, (patch) => {
const patchHash = hash(patch)
// Check if the modification as already be forward (in the case of a change coming from a renderer process)
if (patchesFromRenderer.includes(patchHash)) {
// If the change has already been applied, remove the change from the list and do nothing
patchesFromRenderer.splice(patchesFromRenderer.indexOf(patchHash), 1)
return
}
// Forward it to all of the other renderers
webContents.getAllWebContents().forEach((contents) => {
contents.send(IPCEvents.PATCH, patch)
})
})
return rootStore
}
export async function setupRootStore() {
// Retrieve the current state of the main process's root store through ipc
const state = await window.lindoAPI.fetchInitialStateAsync()
// Create the instance of the root store
const rootStore: Instance<typeof RootStoreModel> = RootStoreModel.create(state, env)
// Array that contains all the changes from the main process
const patchesFromMain: Array<string> = []
// Subscribe to change coming from the main process
window.window.lindoAPI.subscribeToIPCPatch((patch: IJsonPatch) => {
patchesFromMain.push(hash(patch))
// Apply the change
applyPatch(rootStore, patch)
})
// This function is execute every time there is change on the root store
onPatch(rootStore, (patch) => {
const patchHash = hash(patch)
if (patchesFromMain.includes(patchHash)) {
patchesFromMain.splice(patchesFromMain.indexOf(patchHash), 1)
return
}
// Forward the local change to main process through ipc
window.lindoAPI.forwardPatchToMain(patch)
})
return rootStore
}
Continuous Integration with GitHub Actions
To automate testing, builds, and deployments of the application across all available platforms (Windows
, MacOS
, and Linux
), I implemented a GitHub workflow. This workflow automates all these tasks every time a new version tag (e.g., v2.2.0
) is pushed to the main branch.