-
Notifications
You must be signed in to change notification settings - Fork 6
Creating a new emulator
The first step to enable an emulator for RetroAchievements is to locate an open source emulator to extend. This wiki shows how the fceux emulator was extended to support RetroAchievements. Similar steps can be applied to other emulators.
First, clone the original emulator (fceux) into a directory called RANes
. Then rename the origin
remote git reference to vendor
. We'll use origin
for our local repository later.
$ git clone https://github.com/TASVideos/fceux/ RANes
$ cd RANes
$ git remote rename origin vendor
$ git submodule init
$ git submodule update --recursive
Next, create a new branch: source
. This will contains only the source emulator code. RetroAchievements code should never be put into this branch. We aren't going to do anything with this now, but it's important to have for later.
$ git checkout -b source
Then, create another new branch: RANes
. We'll use this branch to add the integration changes, then merge it to master
. master
will contain the RetroAchievements releases, and source
will be used in the future to pick up changes made in the original emulator repository.
$ git checkout master
$ git checkout -b RANes
Finally, add the RAInterface
submodule so the build can access the code and tools.
$ git submodule add https://github.com/RetroAchievements/RAInterface.git
$ cd RAInterface
$ git checkout RAIntegration.0.79
The very last line ensures the emulator version is tied to a specific integration version. You should always use the tag associated to the most recent release.
To make updating from the original emulator easier in the future, we're going to make all RetroAchievements specific changes isolated. The first step of this is to create separate build configurations where we can define our compilation constant and pre/post build commands.
Open the solution for the emulator in Visual Studio. Do a build just to make sure you have all the dependencies identified.
Open the Configuration Manager.
- Select
<New...>
from the Active Solution dropdown - Name it "
RA-Debug
" and copy the settings from "Debug
". - Repeat by copying "
RA-Release
" from "Release
".
Open the Project Settings dialog
- For
RA-Debug
andRA-Release
:- Change the Target Name to the desired RetroAchievements emulator name (i.e.
RANes
) in General - Add
RETROACHIEVEMENTS
to the Preprocessor Definitions in C/C++ > Preprocessor - Add
winhttp.lib
to the Additional Dependencies in Linker > Input - Add the following to the Pre-Build event (relative paths may need to be updated)
The second parameter is the git tag prefix, and should match the emulator name. The third parameter is an all-caps prefix used for generating the
$(ProjectDir)..\RAInterface\MakeBuildVer.bat $(ProjectDir)..\src\RA_BuildVer.h RANes RANES
#define
s. - Add the following to the Post-Build event (relative paths may need to be updated):
$(ProjectDir)..\RAInterface\CopyOverlay.bat $(TargetDir)
- Change the Target Name to the desired RetroAchievements emulator name (i.e.
- Add "
RA_BuildVer.h
" to the.gitignore
file, as well as any new output files created as a result of changing the Target Name.
It's important to keep whitespace consistent with the source, so if an .editorconfig
doesn't exist in the root source directory (or possibly the repository root directory), create one and set the values appropriately to match the source.
# More info: http://EditorConfig.org
root = true
# * here means any file type
[*]
end_of_line = crlf
insert_final_newline = true
# latin1 is a type of ASCII, should work with mbcs
[*.{h,cpp}]
charset = latin1
indent_style = tab
indent_size = 4
trim_trailing_whitespace = true
Then use "Add Existing Item" to associate the .editorconfig
to the Solution.
We want the RetroAchievements changes to the main source files to be as minimal as possible. In that regards, we're going to create a single .cpp
file to contain anything that calls into RAInterface
using more than one or two lines of code.
Create two files in the source directory (usually in the root source folder, but find somewhere appropriate). Try to match the naming convention of other files in that directory. For RANes, we've chosen src/retroachievements.cpp
and src/retroachievements.h
.
Initially, retroachievements.h
should only contain an #include
for the RA_Interface.h
file:
#ifndef __RETROACHIEVEMENTS_H_
#define __RETROACHIEVEMENTS_H_
#include "../RAInterface/RA_Interface.h"
#endif __RETROACHIEVEMENTS_H_
And retroachievements.cpp
only has an #include
for retroachievements.h
:
#include "retroachievements.h"
Add retroachievements.cpp
and RAInterface\RA_Interface.cpp
to the project.
Using the file properties dialog, exclude both from all configurations except the new RA-Debug
and RA-Release
configurations added above.
Open retroachievements.cpp
and press Ctrl+E, Ctrl+D to reformat the file to match the .editorconfig settings
Open retroachievements.h
and press Ctrl+E, Ctrl+D to reformat the file to match the .editorconfig settings
The first thing to do when integrating the DLL is to download it. Find the code that is creating the main window for the emulator. Shortly after the window has been created, but before the message loop starts, add the following code:
#ifdef RETROACHIEVEMENTS
RA_Init(hWnd);
#endif
You'll need to add a #include
at the top of the file too:
#ifdef RETROACHIEVEMENTS
#include "retroachievements.h"
#endif
RA_Init
is going to be one of those multi-line helper functions mentioned earlier, so add a prototype for it in the retroachievements.h
file:
void RA_Init(HWND hWnd);
And a whole bunch of stubs in the retroachievements.cpp
file:
#include "RA_BuildVer.h"
static HWND g_hWnd;
static void CauseUnpause() {}
static void CausePause() {}
static int GetMenuItemIndex(HMENU hMenu, const char* pItemName)
{
int nIndex = 0;
char pBuffer[256];
while (nIndex < GetMenuItemCount(hMenu))
{
if (GetMenuStringA(hMenu, nIndex, pBuffer, sizeof(pBuffer)-1, MF_BYPOSITION))
{
if (!strcmp(pItemName, pBuffer))
return nIndex;
}
nIndex++;
}
return -1;
}
static void RebuildMenu()
{
HMENU hMainMenu = GetMenu(hAppWnd);
if (!hMainMenu)
return;
// if RetroAchievements submenu exists, destroy it
int index = GetMenuItemIndex(hMainMenu, "&RetroAchievements");
if (index >= 0)
DeleteMenu(hMainMenu, index, MF_BYPOSITION);
// append RetroAchievements menu
AppendMenu(hMainMenu, MF_POPUP|MF_STRING, (UINT_PTR)RA_CreatePopupMenu(), TEXT("&RetroAchievements"));
// repaint
DrawMenuBar(hAppWnd);
}
static void GetEstimatedGameTitle(char* sNameOut) {}
static void ResetEmulator() {}
static void LoadROM(const char* sFullPath) {}
void RA_Init(HWND hWnd)
{
// initialize the DLL
RA_Init(hWnd, UnknownEmulator, RANES_VERSION);
RA_SetConsoleID(NES);
g_hWnd = hWnd;
// provide callbacks to the DLL
RA_InstallSharedFunctions(NULL, CauseUnpause, CausePause, RebuildMenu, GetEstimatedGameTitle, ResetEmulator, LoadROM);
// add a placeholder menu item and start the login process - menu will be updated when login completes
RebuildMenu();
RA_AttemptLogin(false);
// ensure titlebar text matches expected format
RA_UpdateAppTitle("");
}
And after the message loop completes, add the following:
#ifdef RETROACHIEVEMENTS
RA_Shutdown();
#endif
RA_Shutdown()
is a function provided by RAInterface
, so we don't need to make any further changes to retroachievements.cpp
.
At this point, the emulator should download the DLL and have the user log in. The menu should be updated to reflect that the user has been logged in.
Locate the code where the emulator is handling existing menu items and add this block of code:
#if RETROACHIEVEMENTS
if (LOWORD(wParam) >= IDM_RA_MENUSTART &&
LOWORD(wParam) < IDM_RA_MENUEND)
{
RA_InvokeDialog(LOWORD(wParam));
return 0;
}
#endif
This should allow you to logout/login using the menu, as well as open any of the toolkit windows.
Things to look for:
- If you're unable to type in the text fields, then the application's message loop isn't calling
TranslateMessage
(Allegro is known to have this issue). To address that, you may need a separate message loop that does callTranslateMessage
, and you should callRA_InvokeDialog
from within that loop. - If the tool windows don't update unless the mouse cursor is over the main window, call
RA_ForceRepaint(true)
afterRA_Init
to inform the DLL to manually repaint things when they change. This is primarily an issue with SDL where the message loop is constantly doing stuff, so there's never time for Windows to promote the pendingInvalidateRect
messages toWM_PAINT
messages.
Find the location in the code where the emulator is loading the game data into memory. Determine an appropriate portion of the data to hash (preferably the entire file [excluding any header information], but at a minimum, the executable area of the code). If you're uncertain what data to hash, please consult the RetroAchievements development team.
Add the following code to let the DLL calculate the hash and load the game:
#ifdef RETROACHIEVEMENTS
RA_OnLoadNewRom(pMemory, nMemorySize);
#endif
If the hash isn't known to the server, the Unknown Game dialog will be shown. Implement the GetEstimatedGameTitle
callback in retroachievements.cpp
to provide a description of the game being loaded - typically this is just the filename without path or extension. Note that sNameOut
is a pointer to a 256-byte buffer, so use snprintf
or something similar to prevent overflowing the buffer.
static void GetEstimatedGameTitle(char* sNameOut)
{
const char* ptr = GameInfo->filename;
if (ptr)
strncpy(sNameOut, ptr, 256);
}
When you load a game, you should now be presented with the Unknown Game dialog showing the filename as the description for the game. If the hash is resolved, you won't see anything happen, but you should be able to open the game's page using the menu item.
If the emulator supports dynamic patching (applying an ips after loading the game), make sure the hash calculation occurs after the ips is applied. Also make sure that the hash algorithm ignores any saved data. The game should resolve correctly whether it's the first time the user plays or the 1000th.
Find everywhere the emulator calls SetWindowText
for the main window and modify it to call RA_UpdateAppTitle
. RA_UpdateAppTitle
will automatically include the emulator name, version, and player name. You only need to provide the additional detail - like currently loaded game name.
#ifdef RETROACHIEVEMENTS
if (Memory.ROMFilename[0])
{
char def[_MAX_FNAME];
_splitpath(Memory.ROMFilename, NULL, NULL, def, NULL);
RA_UpdateAppTitle(def);
}
else
{
RA_UpdateAppTitle("");
}
#else
char buf [1024];
if (Memory.ROMFilename[0])
{
char def[_MAX_FNAME];
_splitpath(Memory.ROMFilename, NULL, NULL, def, NULL);
_sprintf(buf, "%s - %s %s", def, WINDOW_TITLE, TEXT(VERSION));
}
else
_sprintf(buf, "%s %s", WINDOW_TITLE, TEXT(VERSION));
SetWindowText (hWnd, buf);
#endif
To make the fullscreen overlay appear, you have to call RA_SetPaused(true)
, and you can call RA_SetPaused(false)
to make it disappear. Typically, the emulator will have some form of Pause functionality, just find it and add the call:
#ifdef RETROACHIEVEMENTS
RA_SetPaused(state == Paused);
#endif
To enable navigation of the overlay, find where the input is being processed and add the following code
#ifdef RETROACHIEVEMENTS
RA_ProcessInputs();
#endif
Then add the prototype to the header file and the implementation in the cpp file:
void RA_ProcessInputs()
{
if (RA_IsOverlayFullyVisible())
{
ControllerInput input;
input.m_bUpPressed = (CHECK_KEY(0, Up));
input.m_bDownPressed = (CHECK_KEY(0, Down));
input.m_bLeftPressed = (CHECK_KEY(0, Left));
input.m_bRightPressed = (CHECK_KEY(0, Right));
input.m_bConfirmPressed = (CHECK_KEY(0, A));
input.m_bCancelPressed = (CHECK_KEY(0, B));
input.m_bQuitPressed = (CHECK_KEY(0, Start));
RA_NavigateOverlay(&input);
}
}
Also make sure to implement CauseUnpause
to allow the overlay to unpause the emulator when it closes itself.
static void CauseUnpause()
{
Settings.Paused = false;
}
Add the following lines to RA_Init()
in retroachievements.cpp
(after RA_InstallSharedFunctions
):
// register the system memory
RA_ClearMemoryBanks();
RA_InstallMemoryBank(0, ByteReader, ByteWriter, 0x10000);
And provide implementations for ByteReader
and ByteWriter
:
unsigned char ByteReader(unsigned int nOffs)
{
if (GameInfo)
return static_cast<unsigned char>(ARead[nOffs](nOffs));
return 0;
}
void ByteWriter(unsigned int nOffs, unsigned char nVal)
{
if (GameInfo)
BWrite[nOffs](nOffs, nVal);
}
Then find the code associated with processing a frame. Note that not all frames may be rendered - particularly if the emulator has a speed-up feature. Even if the frame isn't rendered, we still want the DLL to process it. This should not be called if the emulator is paused.
#ifdef RETROACHIEVEMENTS
RA_DoAchievementsFrame();
#endif
With these changes, you should be able to open the memory inspector and watch memory change while the game is running. Additionally, you should be able to modify memory and affect the game.
Find the code that saves a state and add the following (after the save occurs):
#ifdef RETROACHIEVEMENTS
RA_OnSaveState(filename);
#endif
Find the code that loads a state and add the following (before the load occurs):
#ifdef RETROACHIEVEMENTS
if (!RA_WarnDisableHardcore("load a state"))
return;
#endif
And the following (after the load occurs):
#ifdef RETROACHIEVEMENTS
RA_OnLoadState(fn);
#endif
You should now be able to persist hitcounts along with the save state, and be warned that save states cannot be loaded when playing in hardcore mode. You should still be allowed to create save states in hardcore mode. Make sure to put this code where it would be called if save states are saved/loaded via menu or shortcuts.
If your emulator supports restarting content, find the related code and add the following (after the reset has completed). This tells the toolkit that the memory is in an invalid state and disables any active leaderboards.
#ifdef RETROACHIEVEMENTS
RA_OnReset();
#endif
You should also implement the ResetEmulator
callback in retroachievements.cpp
at this time. This is called when switching to hardcore mode.
static void ResetEmulator()
{
FCEUI_ResetNES();
}
Find the code related to closing the emulator and add the following code (make sure to account for closing via the menu, title bar X, and any shortcuts):
#ifdef RETROACHIEVEMENTS
if (!RA_ConfirmLoadNewRom(true))
return;
#endif
Find the code related to loading the game (you probably did this earlier) and add the following code before the load starts (make sure to test loading a new game and loading from the recent games list):
#if RETROACHIEVEMENTS
if (!RA_ConfirmLoadNewRom(false))
return;
#endif
If the emulator supports closing a game (system shutdown without closing the emulator), add this code before starting to close the game:
#if RETROACHIEVEMENTS
if (!RA_ConfirmLoadNewRom(false))
return;
#endif
And this code after the game has been closed:
#if RETROACHIEVEMENTS
RA_ActivateGame(0);
#endif
To test this, open a game, add a dummy achievement and try closing the emulator, closing the game, and loading a different game. In all cases, the user should be warned that they have modifications, and selecting "no" should prevent the action, leaving the game and emulator running.
NOTE: Any restrictions must be enforced from all possible inputs - make sure to check menu items, shortcuts, and customizable hotkeys.
If the emulator supports slowdown or frame advance, those features should be disabled in hardcore mode.
#ifdef RETROACHIEVEMENTS
if (!RA_HardcoreModeIsActive())
return;
#endif
These should not popup warnings, instead they should just not function in non-hardcore mode.
NOTE: Speed up and "turbo" should be allowed in hardcore. Speed down should only prevent going below normal speed.
NOTE: When using frame advance, RA_DoAchievementsFrame
should only be called when the frame actually advances.
Make sure to update ResetEmulator
to ensure the speed is not below normal and frame advance is disabled when switching to hardcore mode.
If the emulator provides any sort of debugger windows (i.e. memory watch) they should not be allowed in hardcore.
Any tools that play the game for you (TAS / recording playback) should be disabled in hardcore.
Other debugging tools (like disabling render layers) should be disabled in hardcore.
Find the about dialog code and insert RetroAchievements emulator version information at the top of it.
"RANes " RANES_VERSION_SHORT "\n"
Leave all of the original about information, especially original emulator version information.
- Create an empty repository on github.
- Set it as the origin for your local directory.
$ git remote add origin https://github.com/Jamiras/RANes.git
$ git checkout master
$ git push origin master
$ git branch --set-upstream-to origin/master
$ git checkout source
$ git push -u origin source
$ git checkout RANes
$ git push -u origin RANes
- Create a
PR
fromRANes
to master in your repository. - Share this PR with the RetroAchievements team.
If the emulator is accepted, we will clone your repository to our github. After that, you should delete your repository and fork from our repository so you can create PR's directly into our repository.