mirror of
https://github.com/0ldsk00l/nestopia.git
synced 2025-04-02 10:31:51 -04:00
578 lines
14 KiB
C++
578 lines
14 KiB
C++
/*
|
|
NEStopia / Linux
|
|
Port by R. Belmont
|
|
|
|
cheats.cpp - Cheat Manager UI and cheat handling
|
|
*/
|
|
|
|
#include <iostream>
|
|
#include <fstream>
|
|
#include <strstream>
|
|
#include <sstream>
|
|
#include <iomanip>
|
|
#include <string.h>
|
|
#include <cassert>
|
|
#include <sys/stat.h>
|
|
#include <sys/types.h>
|
|
#include <errno.h>
|
|
#include <vector>
|
|
#include <cstdlib>
|
|
#include <cwchar>
|
|
#include <cerrno>
|
|
#include <cstring>
|
|
|
|
#include "core/api/NstApiEmulator.hpp"
|
|
#include "core/api/NstApiCheats.hpp"
|
|
#include "../core/NstStream.hpp"
|
|
#include "../core/NstXml.hpp" // not entirely kosher but Marty does it on Windows :)
|
|
#include "cheats.h"
|
|
#include "settings.h"
|
|
|
|
extern "C" {
|
|
#include <gtk/gtk.h>
|
|
|
|
#include "interface.h"
|
|
#include "support.h"
|
|
#include "callbacks.h"
|
|
}
|
|
|
|
#include "uihelp.h"
|
|
|
|
using namespace Nes::Api;
|
|
using namespace LinuxNst;
|
|
|
|
typedef Nes::Core::Xml Xml;
|
|
|
|
typedef struct
|
|
{
|
|
Cheats::Code code;
|
|
char gg[9], par[9], raw[32], description[64];
|
|
bool enabled;
|
|
unsigned int crc;
|
|
} NstCheatEntry;
|
|
|
|
|
|
extern Emulator emulator;
|
|
extern GtkWidget *mainwindow;
|
|
extern char rootname[512];
|
|
|
|
static GtkWidget *cheatwin, *cheattree, *ggedit, *paredit, *ggokbut, *parokbut;
|
|
static GtkWidget *chopen, *chsave, *chdelete;
|
|
static GtkTreeStore *treestore;
|
|
static GtkCellRenderer *renderer, *togglerend;
|
|
static GtkTreeViewColumn *column[5];
|
|
static GtkTreeSelection *selection;
|
|
static std::vector<NstCheatEntry> cheatlist; // our private list of cheats
|
|
static std::vector<GtkTreeIter> treeiters;
|
|
static bool sIsOpen;
|
|
|
|
static int find_current_selection(void)
|
|
{
|
|
int i;
|
|
|
|
for (i = 0; i < treeiters.size(); i++)
|
|
{
|
|
if (gtk_tree_selection_iter_is_selected(selection, &treeiters[i]))
|
|
{
|
|
return i;
|
|
}
|
|
}
|
|
|
|
return -1;
|
|
}
|
|
|
|
static void enable_toggle(GtkCellRendererToggle *cell_renderer, gchar *path, gpointer user_data)
|
|
{
|
|
int item;
|
|
char *eptr;
|
|
|
|
item = strtoul(path, &eptr, 0);
|
|
|
|
cheatlist[item].enabled = !cheatlist[item].enabled;
|
|
|
|
gtk_tree_store_set(treestore, &treeiters[item], 0, cheatlist[item].enabled, -1);
|
|
}
|
|
|
|
static void on_cheatwin_destroy(GObject *object, gpointer user_data)
|
|
{
|
|
treeiters.clear();
|
|
sIsOpen = false;
|
|
}
|
|
|
|
CheatMgr::CheatMgr()
|
|
{
|
|
sIsOpen = false;
|
|
}
|
|
|
|
CheatMgr::~CheatMgr()
|
|
{
|
|
}
|
|
|
|
void CheatMgr::ShowManager()
|
|
{
|
|
int i;
|
|
unsigned short addr;
|
|
unsigned char val, cmp;
|
|
bool usecmp;
|
|
Cheats cheats(emulator);
|
|
std::vector<NstCheatEntry> templist;
|
|
|
|
if (sIsOpen)
|
|
{
|
|
return;
|
|
}
|
|
|
|
sIsOpen = true;
|
|
|
|
cheatwin = create_cheatwind();
|
|
cheattree = lookup_widget(cheatwin, "cheattree");
|
|
ggedit = lookup_widget(cheatwin, "ggedit");
|
|
paredit = lookup_widget(cheatwin, "paredit");
|
|
ggokbut = lookup_widget(cheatwin, "genieok");
|
|
parokbut = lookup_widget(cheatwin, "parok");
|
|
chopen = lookup_widget(cheatwin, "cheatopen");
|
|
chsave = lookup_widget(cheatwin, "cheatsave");
|
|
chdelete = lookup_widget(cheatwin, "chdelete");
|
|
|
|
gtk_window_set_icon(GTK_WINDOW(cheatwin), UIHelp_GetNSTIcon());
|
|
|
|
g_signal_connect((gpointer)cheatwin, "destroy", G_CALLBACK(on_cheatwin_destroy), NULL);
|
|
|
|
/* The cheats dialog seems to still work the same without this codeblock.
|
|
* I have no idea why. Not a GTK+ expert. Maybe I'll figure it out later.
|
|
* Either way, these functions are deprecated and no longer exist in GTK+3,
|
|
* so this will need to change in the future if it's ever re-enabled.
|
|
// set various widgets' user data to our "this" pointer
|
|
gtk_object_set_user_data(GTK_OBJECT(ggokbut), (gpointer)this);
|
|
gtk_object_set_user_data(GTK_OBJECT(parokbut), (gpointer)this);
|
|
gtk_object_set_user_data(GTK_OBJECT(chopen), (gpointer)this);
|
|
gtk_object_set_user_data(GTK_OBJECT(chsave), (gpointer)this);
|
|
gtk_object_set_user_data(GTK_OBJECT(chdelete), (gpointer)this); */
|
|
|
|
gtk_tree_view_set_fixed_height_mode(GTK_TREE_VIEW (cheattree), FALSE);
|
|
|
|
// set up our tree store
|
|
treestore = gtk_tree_store_new(5, G_TYPE_BOOLEAN, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING);
|
|
|
|
// attach the store to the tree
|
|
gtk_tree_view_set_model(GTK_TREE_VIEW(cheattree), GTK_TREE_MODEL(treestore));
|
|
|
|
// create a cell renderer using the stock text one
|
|
renderer = gtk_cell_renderer_text_new();
|
|
|
|
// get the stock toggle renderer
|
|
togglerend = gtk_cell_renderer_toggle_new();
|
|
|
|
// get us an event handler for it
|
|
g_signal_connect(G_OBJECT(togglerend), "toggled", G_CALLBACK(enable_toggle), NULL);
|
|
|
|
// create the display columns
|
|
column[0] = gtk_tree_view_column_new_with_attributes("Enable", togglerend, "active", 0, NULL);
|
|
column[1] = gtk_tree_view_column_new_with_attributes("Game Genie", renderer, "text", 1, NULL);
|
|
column[2] = gtk_tree_view_column_new_with_attributes("PAR", renderer, "text", 2, NULL);
|
|
column[3] = gtk_tree_view_column_new_with_attributes("Raw", renderer, "text", 3, NULL);
|
|
column[4] = gtk_tree_view_column_new_with_attributes("Description", renderer, "text", 4, NULL);
|
|
|
|
// add the display column and renderer to the tree view
|
|
gtk_tree_view_append_column(GTK_TREE_VIEW(cheattree), column[0]);
|
|
gtk_tree_view_append_column(GTK_TREE_VIEW(cheattree), column[1]);
|
|
gtk_tree_view_append_column(GTK_TREE_VIEW(cheattree), column[2]);
|
|
gtk_tree_view_append_column(GTK_TREE_VIEW(cheattree), column[3]);
|
|
gtk_tree_view_append_column(GTK_TREE_VIEW(cheattree), column[4]);
|
|
|
|
// get the selection object too
|
|
selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(cheattree));
|
|
|
|
// add the existing cheats
|
|
// since AddCode inserts into the master cheatlist we cheat ourselves and
|
|
// make a copy of the cheatlist, clear the master, then insert from the copy
|
|
templist = cheatlist;
|
|
cheatlist.clear();
|
|
for (i = 0; i < templist.size(); i++)
|
|
{
|
|
Cheats::Code codetoadd;
|
|
|
|
codetoadd = templist[i].code;
|
|
AddCode(codetoadd, true, templist[i].enabled, templist[i].description);
|
|
}
|
|
|
|
// show the window
|
|
gtk_widget_show(cheatwin);
|
|
}
|
|
|
|
void CheatMgr::Enable(void)
|
|
{
|
|
Cheats cheats(emulator);
|
|
int i;
|
|
|
|
// clear anything in the engine
|
|
cheats.ClearCodes();
|
|
|
|
// now scan our list and add any enabled codes
|
|
for (i = 0; i < cheatlist.size(); i++)
|
|
{
|
|
if (cheatlist[i].enabled)
|
|
{
|
|
cheats.SetCode(cheatlist[i].code);
|
|
}
|
|
}
|
|
}
|
|
|
|
void CheatMgr::Unload(void)
|
|
{
|
|
Cheats cheats(emulator);
|
|
|
|
// clear any in-engine cheats
|
|
cheats.ClearCodes();
|
|
|
|
// clear our cheatlist
|
|
cheatlist.clear();
|
|
}
|
|
|
|
void CheatMgr::AddCode(Cheats::Code &code, bool useenable, bool enable, char *description)
|
|
{
|
|
Cheats cheats(emulator);
|
|
GtkTreeIter tmpiter;
|
|
unsigned short addr;
|
|
unsigned char val, cmp;
|
|
bool usecmp;
|
|
int iteridx = treeiters.size();
|
|
int clidx = cheatlist.size();
|
|
NstCheatEntry newentry;
|
|
|
|
newentry.code = code;
|
|
newentry.crc = 0;
|
|
newentry.description[0] = '\0';
|
|
|
|
if (description != NULL)
|
|
{
|
|
strncpy(newentry.description, description, 63);
|
|
}
|
|
|
|
if (useenable)
|
|
{
|
|
newentry.enabled = enable;
|
|
}
|
|
else
|
|
{
|
|
newentry.enabled = true;
|
|
}
|
|
|
|
if (cheats.GameGenieEncode(code, newentry.gg) != Nes::RESULT_OK)
|
|
{
|
|
strcpy(newentry.gg, "---");
|
|
}
|
|
|
|
// not all GG codes can be represented in PAR
|
|
if (cheats.ProActionRockyEncode(code, newentry.par) != Nes::RESULT_OK)
|
|
{
|
|
strcpy(newentry.par, "---");
|
|
}
|
|
|
|
// add it to the list box
|
|
sprintf(newentry.raw, "%04x - %02x - %02x", code.address, code.value, code.compare);
|
|
|
|
// printf("AddCode [GG %s]: iteridx = %d, clidx = %d, useenable %c, enable %c\n", newentry.gg, iteridx, clidx, useenable ? 'T' : 'F', enable ? 'T' : 'F');
|
|
|
|
cheatlist.push_back(newentry);
|
|
treeiters.push_back(tmpiter);
|
|
gtk_tree_store_insert(treestore, &treeiters[iteridx], NULL, 999999);
|
|
gtk_tree_store_set(treestore, &treeiters[iteridx], 0, cheatlist[clidx].enabled, 1, cheatlist[clidx].gg, 2, cheatlist[clidx].par, 3, cheatlist[clidx].raw, 4, cheatlist[clidx].description, -1);
|
|
|
|
// add it to the engine so it takes effect
|
|
// cheats.SetCode(code);
|
|
}
|
|
|
|
void on_parok_clicked(GtkButton *button, gpointer user_data)
|
|
{
|
|
Cheats cheats(emulator);
|
|
Cheats::Code code;
|
|
CheatMgr *cmgr = (CheatMgr *)user_data;
|
|
|
|
if (cheats.ProActionRockyDecode(gtk_entry_get_text(GTK_ENTRY(paredit)), code) == Nes::RESULT_OK)
|
|
{
|
|
cmgr->AddCode(code);
|
|
gtk_entry_set_text(GTK_ENTRY(ggedit), "");
|
|
gtk_entry_set_text(GTK_ENTRY(paredit), "");
|
|
}
|
|
}
|
|
|
|
void on_parvalid_clicked(GtkButton *button, gpointer user_data)
|
|
{
|
|
Cheats cheats(emulator);
|
|
Cheats::Code code;
|
|
char gg[9];
|
|
|
|
if (cheats.ProActionRockyDecode(gtk_entry_get_text(GTK_ENTRY(paredit)), code) == Nes::RESULT_OK)
|
|
{
|
|
cheats.GameGenieEncode(code, gg);
|
|
gtk_entry_set_text(GTK_ENTRY(ggedit), gg);
|
|
}
|
|
}
|
|
|
|
void on_genieok_clicked(GtkButton *button, gpointer user_data)
|
|
{
|
|
Cheats cheats(emulator);
|
|
Cheats::Code code;
|
|
CheatMgr *cmgr = (CheatMgr *)user_data;
|
|
|
|
if (cheats.GameGenieDecode(gtk_entry_get_text(GTK_ENTRY(ggedit)), code) == Nes::RESULT_OK)
|
|
{
|
|
cmgr->AddCode(code);
|
|
gtk_entry_set_text(GTK_ENTRY(ggedit), "");
|
|
gtk_entry_set_text(GTK_ENTRY(paredit), "");
|
|
}
|
|
}
|
|
|
|
void on_chtggvalid_clicked(GtkButton *button, gpointer user_data)
|
|
{
|
|
Cheats cheats(emulator);
|
|
Cheats::Code code;
|
|
char par[9];
|
|
|
|
if (cheats.GameGenieDecode(gtk_entry_get_text(GTK_ENTRY(ggedit)), code) == Nes::RESULT_OK)
|
|
{
|
|
cheats.ProActionRockyEncode(code, par);
|
|
gtk_entry_set_text(GTK_ENTRY(paredit), par);
|
|
}
|
|
}
|
|
|
|
void on_chdelete_clicked(GtkButton *button, gpointer user_data)
|
|
{
|
|
int cursel = find_current_selection();
|
|
|
|
if (cursel != -1)
|
|
{
|
|
gtk_tree_store_remove(treestore, &treeiters[cursel]);
|
|
memset(&treeiters[cursel], 0, sizeof(GtkTreeIter));
|
|
cheatlist.erase(cheatlist.begin()+cursel);
|
|
treeiters.erase(treeiters.begin()+cursel);
|
|
}
|
|
}
|
|
|
|
void on_cheatopen_clicked(GtkButton *button, gpointer user_data)
|
|
{
|
|
GtkWidget *dialog;
|
|
GtkFileFilter *filter;
|
|
Xml xml;
|
|
Cheats cheats(emulator);
|
|
CheatMgr *cmgr = (CheatMgr *)user_data;
|
|
char description[64];
|
|
|
|
dialog = gtk_file_chooser_dialog_new ("Import cheats (.xml)",
|
|
GTK_WINDOW(mainwindow),
|
|
GTK_FILE_CHOOSER_ACTION_OPEN,
|
|
GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL,
|
|
GTK_STOCK_OPEN, GTK_RESPONSE_ACCEPT,
|
|
NULL);
|
|
|
|
filter = gtk_file_filter_new();
|
|
gtk_file_filter_set_name(filter, "NEStopia XML cheats");
|
|
gtk_file_filter_add_pattern(filter, "*.xml");
|
|
gtk_file_chooser_add_filter(GTK_FILE_CHOOSER(dialog), filter);
|
|
|
|
if (gtk_dialog_run (GTK_DIALOG (dialog)) == GTK_RESPONSE_ACCEPT)
|
|
{
|
|
char *filename = gtk_file_chooser_get_filename (GTK_FILE_CHOOSER (dialog));
|
|
|
|
std::ifstream stream( filename, std::ifstream::in|std::ifstream::binary );
|
|
|
|
if (stream.is_open())
|
|
{
|
|
xml.Read( stream );
|
|
|
|
if (xml.GetRoot().IsType( L"cheats" ))
|
|
{
|
|
for (Xml::Node node(xml.GetRoot().GetFirstChild()); node; node=node.GetNextSibling())
|
|
{
|
|
if (!node.IsType( L"cheat" ))
|
|
continue;
|
|
|
|
Cheats::Code code;
|
|
|
|
code.useCompare = false;
|
|
|
|
if (const Xml::Node address=node.GetChild( L"address" ))
|
|
{
|
|
uint v;
|
|
|
|
if (0xFFFF < (v=address.GetUnsignedValue()))
|
|
continue;
|
|
|
|
code.address = v;
|
|
|
|
if (const Xml::Node value=node.GetChild( L"value" ))
|
|
{
|
|
if (0xFF < (v=value.GetUnsignedValue()))
|
|
continue;
|
|
|
|
code.value = v;
|
|
}
|
|
|
|
if (const Xml::Node compare=node.GetChild( L"compare" ))
|
|
{
|
|
if (0xFF < (v=compare.GetUnsignedValue()))
|
|
continue;
|
|
|
|
code.compare = v;
|
|
code.useCompare = true;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
char codestr[512];
|
|
|
|
if (const Xml::Node genie=node.GetChild( L"genie" ))
|
|
{
|
|
wcstombs(codestr, genie.GetValue(), 511);
|
|
if (cheats.GameGenieDecode(codestr, code) != Nes::RESULT_OK)
|
|
{
|
|
continue;
|
|
}
|
|
}
|
|
else if (const Xml::Node rocky=node.GetChild( L"rocky" ))
|
|
{
|
|
wcstombs(codestr, rocky.GetValue(), 511);
|
|
if (cheats.ProActionRockyDecode(codestr, code) != Nes::RESULT_OK)
|
|
{
|
|
continue;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
continue;
|
|
}
|
|
}
|
|
|
|
// add the code, with a description if it's got one
|
|
if (node.GetChild( L"description" ))
|
|
{
|
|
wcstombs(description, node.GetChild( L"description" ).GetValue(), 63);
|
|
cmgr->AddCode(code, true, node.GetAttribute( L"enabled" ).IsValue( L"0" ) ? false : true, description);
|
|
}
|
|
else
|
|
{
|
|
cmgr->AddCode(code, true, node.GetAttribute( L"enabled" ).IsValue( L"0" ) ? false : true);
|
|
}
|
|
|
|
// poke in the CRC too if there is one
|
|
cheatlist[cheatlist.size()-1].crc = node.GetAttribute( L"game" ).GetUnsignedValue();
|
|
}
|
|
}
|
|
}
|
|
|
|
g_free (filename);
|
|
}
|
|
|
|
gtk_widget_destroy(dialog);
|
|
}
|
|
|
|
void on_cheatsave_clicked(GtkButton *button, gpointer user_data)
|
|
{
|
|
GtkWidget *dialog;
|
|
char defname[512];
|
|
Cheats cheats(emulator);
|
|
|
|
// nothing to export?
|
|
if (cheats.NumCodes() == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
defname[0] = '\0';
|
|
strcpy(defname, rootname);
|
|
strcat(defname, ".xml");
|
|
|
|
dialog = gtk_file_chooser_dialog_new ("Export cheats (.xml)",
|
|
GTK_WINDOW(mainwindow),
|
|
GTK_FILE_CHOOSER_ACTION_SAVE,
|
|
GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL,
|
|
GTK_STOCK_SAVE, GTK_RESPONSE_ACCEPT,
|
|
NULL);
|
|
|
|
gtk_file_chooser_set_current_name(GTK_FILE_CHOOSER(dialog), defname);
|
|
|
|
if (gtk_dialog_run (GTK_DIALOG (dialog)) == GTK_RESPONSE_ACCEPT)
|
|
{
|
|
char *filename = gtk_file_chooser_get_filename (GTK_FILE_CHOOSER (dialog));
|
|
|
|
std::ofstream stream( filename, std::ifstream::out|std::ifstream::binary );
|
|
|
|
if (stream.is_open())
|
|
{
|
|
int i;
|
|
Xml xml;
|
|
Xml::Node root(xml.GetRoot());
|
|
|
|
root = xml.Create( L"cheats" );
|
|
root.AddAttribute( L"version", L"1.0" );
|
|
|
|
for (i = 0; i < cheatlist.size(); i++)
|
|
{
|
|
Cheats::Code code;
|
|
char buffer[9];
|
|
wchar_t wbuffer[64];
|
|
|
|
code = cheatlist[i].code;
|
|
|
|
Xml::Node node( root.AddChild( L"cheat" ) );
|
|
node.AddAttribute( L"enabled", cheatlist[i].enabled ? L"1" : L"0" );
|
|
|
|
if (cheatlist[i].crc)
|
|
{
|
|
sprintf(buffer, "%08X", cheatlist[i].crc);
|
|
mbstowcs(wbuffer, buffer, 63);
|
|
node.AddAttribute( L"game", wbuffer );
|
|
}
|
|
|
|
if (NES_SUCCEEDED(cheats.GameGenieEncode(code, buffer)))
|
|
{
|
|
mbstowcs(wbuffer, buffer, 63);
|
|
node.AddChild( L"genie", wbuffer );
|
|
}
|
|
|
|
if (NES_SUCCEEDED(cheats.ProActionRockyEncode(code, buffer)))
|
|
{
|
|
mbstowcs(wbuffer, buffer, 63);
|
|
node.AddChild( L"rocky", wbuffer );
|
|
}
|
|
|
|
sprintf(buffer, "0x%04X", code.address);
|
|
mbstowcs(wbuffer, buffer, 63);
|
|
node.AddChild( L"address", wbuffer );
|
|
sprintf(buffer, "0x%02X", code.value);
|
|
mbstowcs(wbuffer, buffer, 63);
|
|
node.AddChild( L"value", wbuffer );
|
|
|
|
if (code.useCompare)
|
|
{
|
|
sprintf(buffer, "0x%02X", code.compare);
|
|
mbstowcs(wbuffer, buffer, 63);
|
|
node.AddChild( L"compare", wbuffer );
|
|
}
|
|
|
|
if (strlen(cheatlist[i].description))
|
|
{
|
|
mbstowcs(wbuffer, cheatlist[i].description, 63);
|
|
node.AddChild( L"description", wbuffer );
|
|
}
|
|
}
|
|
|
|
xml.Write( root, stream );
|
|
}
|
|
|
|
g_free (filename);
|
|
}
|
|
|
|
gtk_widget_destroy(dialog);
|
|
}
|
|
|
|
void on_cheatok_clicked(GtkButton *button, gpointer user_data)
|
|
{
|
|
gtk_widget_destroy(cheatwin);
|
|
|
|
treeiters.clear();
|
|
|
|
sIsOpen = false;
|
|
}
|
|
|