mirror of
https://github.com/0ldsk00l/nestopia.git
synced 2024-05-20 13:08:04 -04:00
1011 lines
26 KiB
C++
1011 lines
26 KiB
C++
/*
|
|
* Nestopia UE
|
|
*
|
|
* Copyright (C) 2007-2008 R. Belmont
|
|
* Copyright (C) 2012-2021 R. Danbrook
|
|
* Copyright (C) 2018-2018 Phil Smith
|
|
*
|
|
* This program is free software; you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation; either version 2 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with this program; if not, write to the Free Software
|
|
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
|
|
* MA 02110-1301, USA.
|
|
*
|
|
*/
|
|
|
|
#include <iostream>
|
|
#include <fstream>
|
|
#include <sstream>
|
|
|
|
#include <cstdio>
|
|
#include <cstring>
|
|
#include <libgen.h>
|
|
|
|
#include <errno.h>
|
|
#include <sys/stat.h>
|
|
#include <sys/types.h>
|
|
|
|
#include <archive.h>
|
|
#include <archive_entry.h>
|
|
|
|
#include <SDL.h>
|
|
|
|
// Nst Common
|
|
#include "nstcommon.h"
|
|
#include "config.h"
|
|
#include "cheats.h"
|
|
#include "homebrew.h"
|
|
#include "input.h"
|
|
#include "audio.h"
|
|
#include "video.h"
|
|
#include "samples.h"
|
|
|
|
Emulator emulator;
|
|
Video::Output *cNstVideo;
|
|
Sound::Output *cNstSound;
|
|
Input::Controllers *cNstPads;
|
|
|
|
nstpaths_t nstpaths;
|
|
|
|
static bool ffspeed = false;
|
|
static bool playing = false;
|
|
|
|
static std::ifstream *nstdb;
|
|
|
|
static std::ifstream *fdsbios;
|
|
|
|
static std::ifstream *moviefile;
|
|
static std::fstream *movierecfile;
|
|
|
|
void *custompalette = NULL;
|
|
static size_t custpalsize;
|
|
|
|
static int loaded = 0;
|
|
|
|
bool (*nst_archive_select)(const char*, char*, size_t);
|
|
|
|
static bool NST_CALLBACK nst_cb_videolock(void* userData, Video::Output& video) {
|
|
video.pitch = video_lock_screen(video.pixels);
|
|
return true;
|
|
}
|
|
|
|
static void NST_CALLBACK nst_cb_videounlock(void* userData, Video::Output& video) {
|
|
video_unlock_screen(video.pixels);
|
|
}
|
|
|
|
static bool NST_CALLBACK nst_cb_soundlock(void* userData, Sound::Output& sound) {
|
|
audio_queue();
|
|
return true;
|
|
}
|
|
|
|
static void NST_CALLBACK nst_cb_soundunlock(void* userData, Sound::Output& sound) {
|
|
// Do Nothing
|
|
}
|
|
|
|
static void NST_CALLBACK nst_cb_event(void *userData, User::Event event, const void* data) {
|
|
// Handle special events
|
|
switch (event) {
|
|
case User::EVENT_CPU_JAM:
|
|
fprintf(stderr, "Cpu: Jammed\n");
|
|
break;
|
|
case User::EVENT_CPU_UNOFFICIAL_OPCODE:
|
|
fprintf(stderr, "Cpu: Unofficial Opcode %s\n", (const char*)data);
|
|
break;
|
|
case User::EVENT_DISPLAY_TIMER:
|
|
fprintf(stderr, "\r%s", (const char*)data);
|
|
nst_video_print_time((const char*)data + strlen((char*)data) - 5, true);
|
|
break;
|
|
default: break;
|
|
}
|
|
}
|
|
|
|
static void NST_CALLBACK nst_cb_log(void *userData, const char *string, unsigned long int length) {
|
|
// Print logging information to stderr
|
|
fprintf(stderr, "%s", string);
|
|
}
|
|
|
|
static void NST_CALLBACK nst_cb_file(void *userData, User::File& file) {
|
|
unsigned char *compbuffer;
|
|
int compsize, compoffset;
|
|
char *filename;
|
|
|
|
switch (file.GetAction()) {
|
|
case User::File::LOAD_ROM:
|
|
// Nothing here for now
|
|
break;
|
|
|
|
case User::File::LOAD_SAMPLE: break;
|
|
case User::File::LOAD_SAMPLE_MOERO_PRO_YAKYUU: nst_sample_load_samples(file, "moepro"); break;
|
|
case User::File::LOAD_SAMPLE_MOERO_PRO_YAKYUU_88: nst_sample_load_samples(file, "moepro88"); break;
|
|
case User::File::LOAD_SAMPLE_MOERO_PRO_TENNIS: nst_sample_load_samples(file, "mptennis"); break;
|
|
case User::File::LOAD_SAMPLE_TERAO_NO_DOSUKOI_OOZUMOU: nst_sample_load_samples(file, "terao"); break;
|
|
case User::File::LOAD_SAMPLE_AEROBICS_STUDIO: nst_sample_load_samples(file, "ftaerobi"); break;
|
|
|
|
case User::File::LOAD_BATTERY: // load in battery data from a file
|
|
case User::File::LOAD_EEPROM: // used by some Bandai games, can be treated the same as battery files
|
|
case User::File::LOAD_TAPE: // for loading Famicom cassette tapes
|
|
case User::File::LOAD_TURBOFILE: // for loading turbofile data
|
|
{
|
|
std::ifstream batteryFile(nstpaths.savename, std::ifstream::in|std::ifstream::binary);
|
|
|
|
if (batteryFile.is_open()) { file.SetContent(batteryFile); }
|
|
break;
|
|
}
|
|
|
|
case User::File::SAVE_BATTERY: // save battery data to a file
|
|
case User::File::SAVE_EEPROM: // can be treated the same as battery files
|
|
case User::File::SAVE_TAPE: // for saving Famicom cassette tapes
|
|
case User::File::SAVE_TURBOFILE: // for saving turbofile data
|
|
{
|
|
std::ofstream batteryFile(nstpaths.savename, std::ifstream::out|std::ifstream::binary);
|
|
const void* savedata;
|
|
unsigned long savedatasize;
|
|
|
|
file.GetContent(savedata, savedatasize);
|
|
|
|
if (batteryFile.is_open()) { batteryFile.write((const char*) savedata, savedatasize); }
|
|
|
|
break;
|
|
}
|
|
|
|
case User::File::LOAD_FDS: // for loading modified Famicom Disk System files
|
|
{
|
|
char fdsname[512];
|
|
|
|
snprintf(fdsname, sizeof(fdsname), "%s.ups", nstpaths.fdssave);
|
|
|
|
std::ifstream batteryFile( fdsname, std::ifstream::in|std::ifstream::binary );
|
|
|
|
// no ups, look for ips
|
|
if (!batteryFile.is_open())
|
|
{
|
|
snprintf(fdsname, sizeof(fdsname), "%s.ips", nstpaths.fdssave);
|
|
|
|
std::ifstream batteryFile( fdsname, std::ifstream::in|std::ifstream::binary );
|
|
|
|
if (!batteryFile.is_open())
|
|
{
|
|
return;
|
|
}
|
|
|
|
file.SetPatchContent(batteryFile);
|
|
return;
|
|
}
|
|
|
|
file.SetPatchContent(batteryFile);
|
|
break;
|
|
}
|
|
|
|
case User::File::SAVE_FDS: // for saving modified Famicom Disk System files
|
|
{
|
|
char fdsname[512];
|
|
|
|
snprintf(fdsname, sizeof(fdsname), "%s.ups", nstpaths.fdssave);
|
|
|
|
std::ofstream fdsFile( fdsname, std::ifstream::out|std::ifstream::binary );
|
|
|
|
if (fdsFile.is_open())
|
|
file.GetPatchContent( User::File::PATCH_UPS, fdsFile );
|
|
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
static Machine::FavoredSystem nst_default_system() {
|
|
switch (conf.misc_default_system) {
|
|
case 2: return Machine::FAVORED_NES_PAL; break;
|
|
case 3: return Machine::FAVORED_FAMICOM; break;
|
|
case 4: return Machine::FAVORED_DENDY; break;
|
|
default: return Machine::FAVORED_NES_NTSC; break;
|
|
}
|
|
}
|
|
|
|
void* nst_ptr_video() { return &cNstVideo; }
|
|
void* nst_ptr_sound() { return &cNstSound; }
|
|
void* nst_ptr_input() { return &cNstPads; }
|
|
|
|
bool nst_archive_checkext(const char *filename) {
|
|
// Check if the file extension is valid
|
|
int len = strlen(filename);
|
|
|
|
if ((!strcasecmp(&filename[len-4], ".nes")) ||
|
|
(!strcasecmp(&filename[len-4], ".fds")) ||
|
|
(!strcasecmp(&filename[len-4], ".nsf")) ||
|
|
(!strcasecmp(&filename[len-4], ".unf")) ||
|
|
(!strcasecmp(&filename[len-5], ".unif"))||
|
|
(!strcasecmp(&filename[len-4], ".xml"))) {
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool nst_archive_select_file(const char *filename, char *reqfile, size_t reqsize) {
|
|
// Select a filename to pull out of the archive
|
|
struct archive *a;
|
|
struct archive_entry *entry;
|
|
int r, numarchives = 0;
|
|
|
|
a = archive_read_new();
|
|
archive_read_support_filter_all(a);
|
|
archive_read_support_format_all(a);
|
|
r = archive_read_open_filename(a, filename, 10240);
|
|
|
|
// Test if it's actually an archive
|
|
if (r != ARCHIVE_OK) {
|
|
r = archive_read_free(a);
|
|
return false;
|
|
}
|
|
// If it is an archive, handle it
|
|
else {
|
|
// Find files with valid extensions within the archive
|
|
while (archive_read_next_header(a, &entry) == ARCHIVE_OK) {
|
|
const char *currentfile = archive_entry_pathname(entry);
|
|
if (nst_archive_checkext(currentfile)) {
|
|
numarchives++;
|
|
snprintf(reqfile, reqsize, "%s", currentfile);
|
|
}
|
|
archive_read_data_skip(a);
|
|
break; // Load the first one found
|
|
}
|
|
// Free the archive
|
|
r = archive_read_free(a);
|
|
|
|
// If there are no valid files in the archive, return
|
|
if (numarchives == 0) { return false; }
|
|
else { return true; }
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool nst_archive_open(const char *filename, char **rom, int *romsize, const char *reqfile) {
|
|
// Opens archives
|
|
struct archive *a;
|
|
struct archive_entry *entry;
|
|
int r;
|
|
int64_t entrysize;
|
|
|
|
a = archive_read_new();
|
|
archive_read_support_filter_all(a);
|
|
archive_read_support_format_all(a);
|
|
r = archive_read_open_filename(a, filename, 10240);
|
|
|
|
// Test if it's actually an archive
|
|
if (r != ARCHIVE_OK) {
|
|
r = archive_read_free(a);
|
|
return false;
|
|
}
|
|
|
|
// Scan through the archive for files
|
|
while (archive_read_next_header(a, &entry) == ARCHIVE_OK) {
|
|
char *rombuf;
|
|
const char *currentfile = archive_entry_pathname(entry);
|
|
if (nst_archive_checkext(currentfile)) {
|
|
nst_set_paths(currentfile);
|
|
// If there's a specific file we want, load it
|
|
if (reqfile != NULL) {
|
|
if (!strcmp(currentfile, reqfile)) {
|
|
entrysize = archive_entry_size(entry);
|
|
rombuf = (char*)malloc(entrysize);
|
|
archive_read_data(a, rombuf, entrysize);
|
|
archive_read_data_skip(a);
|
|
r = archive_read_free(a);
|
|
*romsize = entrysize;
|
|
*rom = rombuf;
|
|
return true;
|
|
}
|
|
}
|
|
// Otherwise just take the first file in the archive
|
|
else {
|
|
entrysize = archive_entry_size(entry);
|
|
rombuf = (char*)malloc(entrysize);
|
|
archive_read_data(a, rombuf, entrysize);
|
|
archive_read_data_skip(a);
|
|
r = archive_read_free(a);
|
|
*romsize = entrysize;
|
|
*rom = rombuf;
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void nst_db_load() {
|
|
Nes::Api::Cartridge::Database database(emulator);
|
|
char dbpath[512];
|
|
|
|
if (nstdb) { return; }
|
|
|
|
// Try to open the database file
|
|
snprintf(dbpath, sizeof(dbpath), "%sNstDatabase.xml", nstpaths.nstdir);
|
|
nstdb = new std::ifstream(dbpath, std::ifstream::in|std::ifstream::binary);
|
|
|
|
if (nstdb->is_open()) {
|
|
database.Load(*nstdb);
|
|
database.Enable(true);
|
|
return;
|
|
}
|
|
|
|
// If it fails, try looking in the data directory
|
|
snprintf(dbpath, sizeof(dbpath), "%s/NstDatabase.xml", ".");
|
|
nstdb = new std::ifstream(dbpath, std::ifstream::in|std::ifstream::binary);
|
|
|
|
if (nstdb->is_open()) {
|
|
database.Load(*nstdb);
|
|
database.Enable(true);
|
|
return;
|
|
}
|
|
|
|
// If that fails, try looking in the working directory
|
|
char *pwd = getenv("PWD");
|
|
snprintf(dbpath, sizeof(dbpath), "%s/NstDatabase.xml", pwd);
|
|
nstdb = new std::ifstream(dbpath, std::ifstream::in|std::ifstream::binary);
|
|
|
|
if (nstdb->is_open()) {
|
|
database.Load(*nstdb);
|
|
database.Enable(true);
|
|
return;
|
|
}
|
|
else {
|
|
fprintf(stderr, "NstDatabase.xml not found!\n");
|
|
delete nstdb;
|
|
nstdb = NULL;
|
|
}
|
|
}
|
|
|
|
void nst_db_unload() {
|
|
if (nstdb) { delete nstdb; nstdb = NULL; }
|
|
}
|
|
|
|
void nst_dipswitch() {
|
|
// Print DIP switch information and call handler
|
|
DipSwitches dipswitches(emulator);
|
|
|
|
int numdips = dipswitches.NumDips();
|
|
|
|
if (numdips > 0) {
|
|
for (int i = 0; i < numdips; i++) {
|
|
fprintf(stderr, "%d: %s\n", i, dipswitches.GetDipName(i));
|
|
int numvalues = dipswitches.NumValues(i);
|
|
|
|
for (int j = 0; j < numvalues; j++) {
|
|
fprintf(stderr, " %d: %s\n", j, dipswitches.GetValueName(i, j));
|
|
}
|
|
}
|
|
|
|
char dippath[512];
|
|
snprintf(dippath, sizeof(dippath), "%s%s.dip", nstpaths.savedir, nstpaths.gamename);
|
|
nst_dip_handle(dippath);
|
|
}
|
|
}
|
|
|
|
void nst_fds_bios_load() {
|
|
// Load the Famicom Disk System BIOS
|
|
Nes::Api::Fds fds(emulator);
|
|
char biospath[512];
|
|
|
|
if (fdsbios) { return; }
|
|
|
|
snprintf(biospath, sizeof(biospath), "%sdisksys.rom", nstpaths.nstdir);
|
|
|
|
fdsbios = new std::ifstream(biospath, std::ifstream::in|std::ifstream::binary);
|
|
|
|
if (fdsbios->is_open()) {
|
|
fds.SetBIOS(fdsbios);
|
|
}
|
|
else {
|
|
fprintf(stderr, "Fds: BIOS not found: %s\n", biospath);
|
|
delete fdsbios;
|
|
fdsbios = NULL;
|
|
}
|
|
}
|
|
|
|
void nst_fds_bios_unload() {
|
|
if (fdsbios) { delete fdsbios; fdsbios = NULL; }
|
|
}
|
|
|
|
void nst_fds_info() {
|
|
Fds fds(emulator);
|
|
|
|
const char* disk;
|
|
const char* side;
|
|
char textbuf[24];
|
|
|
|
fds.GetCurrentDisk() == 0 ? disk = "1" : disk = "2";
|
|
fds.GetCurrentDiskSide() == 0 ? side = "A" : side = "B";
|
|
|
|
fprintf(stderr, "Fds: Disk %s Side %s\n", disk, side);
|
|
snprintf(textbuf, sizeof(textbuf), "Disk %s Side %s", disk, side);
|
|
nst_video_print((const char*)textbuf, 8, 16, 2, true);
|
|
}
|
|
|
|
void nst_fds_flip() {
|
|
// Flips the FDS disk
|
|
Fds fds(emulator);
|
|
|
|
if (fds.CanChangeDiskSide()) {
|
|
fds.ChangeSide();
|
|
nst_fds_info();
|
|
}
|
|
}
|
|
|
|
void nst_fds_switch() {
|
|
// Switches the FDS disk in multi-disk games
|
|
Fds fds(emulator);
|
|
|
|
int currentdisk = fds.GetCurrentDisk();
|
|
|
|
// If it's a multi-disk game, eject and insert the other disk
|
|
if (fds.GetNumDisks() > 1) {
|
|
fds.EjectDisk();
|
|
fds.InsertDisk(!currentdisk, 0);
|
|
nst_fds_info();
|
|
}
|
|
}
|
|
|
|
void nst_movie_save(const char *filename) {
|
|
// Save/Record a movie
|
|
Movie movie(emulator);
|
|
|
|
movierecfile = new std::fstream(filename, std::ifstream::out|std::ifstream::binary);
|
|
|
|
if (movierecfile->is_open()) {
|
|
movie.Record((std::iostream&)*movierecfile, Nes::Api::Movie::CLEAN);
|
|
}
|
|
else {
|
|
delete movierecfile;
|
|
movierecfile = NULL;
|
|
}
|
|
}
|
|
|
|
void nst_movie_load(const char *filename) {
|
|
// Load and play a movie
|
|
Movie movie(emulator);
|
|
|
|
moviefile = new std::ifstream(filename, std::ifstream::in|std::ifstream::binary);
|
|
|
|
if (moviefile->is_open()) {
|
|
movie.Play(*moviefile);
|
|
}
|
|
else {
|
|
delete moviefile;
|
|
moviefile = NULL;
|
|
}
|
|
}
|
|
|
|
void nst_movie_stop() {
|
|
// Stop any movie that is playing or recording
|
|
Movie movie(emulator);
|
|
|
|
if (movie.IsPlaying() || movie.IsRecording()) {
|
|
movie.Stop();
|
|
movierecfile = NULL;
|
|
delete movierecfile;
|
|
moviefile = NULL;
|
|
delete moviefile;
|
|
}
|
|
}
|
|
|
|
bool nst_nsf() {
|
|
Machine machine(emulator);
|
|
return machine.Is(Machine::SOUND);
|
|
}
|
|
|
|
void nst_nsf_play() {
|
|
Nsf nsf(emulator);
|
|
nsf.PlaySong();
|
|
video_clear_buffer();
|
|
video_disp_nsf();
|
|
}
|
|
|
|
void nst_nsf_stop() {
|
|
Nsf nsf(emulator);
|
|
nsf.StopSong();
|
|
}
|
|
|
|
void nst_nsf_prev() {
|
|
Nsf nsf(emulator);
|
|
nsf.SelectPrevSong();
|
|
video_clear_buffer();
|
|
video_disp_nsf();
|
|
}
|
|
|
|
void nst_nsf_next() {
|
|
Nsf nsf(emulator);
|
|
nsf.SelectNextSong();
|
|
video_clear_buffer();
|
|
video_disp_nsf();
|
|
}
|
|
|
|
bool nst_pal() {
|
|
Machine machine(emulator);
|
|
return machine.GetMode() == Machine::PAL;
|
|
}
|
|
|
|
bool nst_playing() { return playing; }
|
|
|
|
void nst_palette_load(const char *filename) {
|
|
// Load a custom palette
|
|
|
|
FILE *file;
|
|
long filesize; // File size in bytes
|
|
size_t result;
|
|
|
|
char custgamepalpath[512];
|
|
snprintf(custgamepalpath, sizeof(custgamepalpath), "%s%s%s", nstpaths.nstdir, nstpaths.gamename, ".pal");
|
|
|
|
// Try the game-specific palette first
|
|
file = fopen(custgamepalpath, "rb");
|
|
if (!file) { file = fopen(filename, "rb"); }
|
|
|
|
// Then try the global custom palette
|
|
if (!file) {
|
|
if (conf.video_palette_mode == 2) {
|
|
fprintf(stderr, "Custom palette: not found: %s\n", filename);
|
|
conf.video_palette_mode = 0;
|
|
}
|
|
return;
|
|
}
|
|
|
|
fseek(file, 0, SEEK_END);
|
|
filesize = ftell(file);
|
|
fseek(file, 0, SEEK_SET);
|
|
|
|
if (custompalette) { free(custompalette); }
|
|
custompalette = malloc(filesize * sizeof(uint8_t));
|
|
custpalsize = filesize * sizeof(uint8_t);
|
|
|
|
result = fread(custompalette, sizeof(uint8_t), filesize, file);
|
|
|
|
fclose(file);
|
|
}
|
|
|
|
void nst_palette_save() {
|
|
// Save a custom palette
|
|
FILE *file;
|
|
void *custpalout;
|
|
|
|
file = fopen(nstpaths.palettepath, "wb");
|
|
if (!file) { return; }
|
|
|
|
custpalout = malloc(custpalsize);
|
|
|
|
memcpy(custpalout, custompalette, custpalsize);
|
|
|
|
fwrite(custpalout, custpalsize, sizeof(uint8_t), file);
|
|
fclose(file);
|
|
free(custpalout);
|
|
}
|
|
|
|
void nst_palette_unload() {
|
|
if (custompalette) { free(custompalette); }
|
|
}
|
|
|
|
bool nst_find_patch(char *patchname, unsigned int patchname_length, const char *filename) {
|
|
// Check for a patch in the same directory as the game
|
|
FILE *file;
|
|
char filedir[512];
|
|
|
|
// Copy filename (will be used by dirname)
|
|
// dirname needs a copy because it can modify its argument
|
|
strncpy(filedir, filename, sizeof(filedir));
|
|
filedir[sizeof(filedir) - 1] = '\0';
|
|
// Use memmove because dirname can return the same pointer as its argument,
|
|
// since copying into same string as the argument we don't want any overlap
|
|
memmove(filedir, dirname(filedir), sizeof(filedir));
|
|
filedir[sizeof(filedir) - 1] = '\0';
|
|
|
|
if (!conf.misc_soft_patching) { return 0; }
|
|
|
|
snprintf(patchname, patchname_length, "%s/%s.ips", filedir, nstpaths.gamename);
|
|
|
|
if ((file = fopen(patchname, "rb")) != NULL) { fclose(file); return 1; }
|
|
else {
|
|
snprintf(patchname, patchname_length, "%s/%s.ups", filedir, nstpaths.gamename);
|
|
if ((file = fopen(patchname, "rb")) != NULL) { fclose(file); return 1; }
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
void nst_set_callbacks() {
|
|
// Set up the callbacks
|
|
void *userData = (void*)0xDEADC0DE;
|
|
|
|
Video::Output::lockCallback.Set(nst_cb_videolock, userData);
|
|
Video::Output::unlockCallback.Set(nst_cb_videounlock, userData);
|
|
|
|
Sound::Output::lockCallback.Set(nst_cb_soundlock, userData);
|
|
Sound::Output::unlockCallback.Set(nst_cb_soundunlock, userData);
|
|
|
|
User::fileIoCallback.Set(nst_cb_file, userData);
|
|
User::logCallback.Set(nst_cb_log, userData);
|
|
User::eventCallback.Set(nst_cb_event, userData);
|
|
}
|
|
|
|
void nst_set_dirs() {
|
|
// Set up system directories
|
|
// create config directory if it doesn't exist
|
|
if (getenv("XDG_CONFIG_HOME")) {
|
|
snprintf(nstpaths.nstconfdir, sizeof(nstpaths.nstconfdir), "%s/nestopia/", getenv("XDG_CONFIG_HOME"));
|
|
}
|
|
else {
|
|
snprintf(nstpaths.nstconfdir, sizeof(nstpaths.nstconfdir), "%s/.config/nestopia/", getenv("HOME"));
|
|
}
|
|
|
|
if (mkdir(nstpaths.nstconfdir, 0755) && errno != EEXIST) {
|
|
fprintf(stderr, "Failed to create %s: %d\n", nstpaths.nstconfdir, errno);
|
|
}
|
|
|
|
// create data directory if it doesn't exist
|
|
if (getenv("XDG_DATA_HOME")) {
|
|
snprintf(nstpaths.nstdir, sizeof(nstpaths.nstdir), "%s/nestopia/", getenv("XDG_DATA_HOME"));
|
|
}
|
|
else {
|
|
snprintf(nstpaths.nstdir, sizeof(nstpaths.nstdir), "%s/.local/share/nestopia/", getenv("HOME"));
|
|
}
|
|
|
|
if (mkdir(nstpaths.nstdir, 0755) && errno != EEXIST) {
|
|
fprintf(stderr, "Failed to create %s: %d\n", nstpaths.nstdir, errno);
|
|
}
|
|
|
|
// create save and state directories if they don't exist
|
|
char dirstr[256];
|
|
snprintf(dirstr, sizeof(dirstr), "%ssave", nstpaths.nstdir);
|
|
|
|
if (mkdir(dirstr, 0755) && errno != EEXIST) {
|
|
fprintf(stderr, "Failed to create %s: %d\n", dirstr, errno);
|
|
}
|
|
|
|
snprintf(dirstr, sizeof(dirstr), "%sstate", nstpaths.nstdir);
|
|
if (mkdir(dirstr, 0755) && errno != EEXIST) {
|
|
fprintf(stderr, "Failed to create %s: %d\n", dirstr, errno);
|
|
}
|
|
|
|
// create cheats directory if it doesn't exist
|
|
snprintf(dirstr, sizeof(dirstr), "%scheats", nstpaths.nstdir);
|
|
if (mkdir(dirstr, 0755) && errno != EEXIST) {
|
|
fprintf(stderr, "Failed to create %s: %d\n", dirstr, errno);
|
|
}
|
|
|
|
// create screenshots directory if it doesn't exist
|
|
snprintf(dirstr, sizeof(dirstr), "%sscreenshots", nstpaths.nstdir);
|
|
if (mkdir(dirstr, 0755) && errno != EEXIST) {
|
|
fprintf(stderr, "Failed to create %s: %d\n", dirstr, errno);
|
|
}
|
|
|
|
// Construct the custom palette path
|
|
snprintf(nstpaths.palettepath, sizeof(nstpaths.palettepath), "%s%s", nstpaths.nstdir, "custom.pal");
|
|
|
|
// Construct samples directory if it doesn't exist
|
|
snprintf(dirstr, sizeof(dirstr), "%ssamples", nstpaths.nstdir);
|
|
if (mkdir(dirstr, 0755) && errno != EEXIST) {
|
|
fprintf(stderr, "Failed to create %s: %d\n", dirstr, errno);
|
|
}
|
|
}
|
|
|
|
void nst_set_paths(const char *filename) {
|
|
|
|
// Set up the save directory
|
|
snprintf(nstpaths.savedir, sizeof(nstpaths.savedir), "%ssave/", nstpaths.nstdir);
|
|
|
|
// Copy the full file path to the savename variable
|
|
snprintf(nstpaths.savename, sizeof(nstpaths.savename), "%s", filename);
|
|
|
|
// strip the . and extention off the filename for saving
|
|
for (int i = strlen(nstpaths.savename)-1; i > 0; i--) {
|
|
if (nstpaths.savename[i] == '.') {
|
|
nstpaths.savename[i] = '\0';
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Set up the sample directory
|
|
snprintf(nstpaths.sampdir, sizeof(nstpaths.sampdir), "%ssamples/", nstpaths.nstdir);
|
|
|
|
// Get the name of the game minus file path and extension
|
|
snprintf(nstpaths.gamename, sizeof(nstpaths.gamename), "%s", basename(nstpaths.savename));
|
|
|
|
// Construct save path
|
|
snprintf(nstpaths.savename, sizeof(nstpaths.savename), "%s%s%s", nstpaths.savedir, nstpaths.gamename, ".sav");
|
|
|
|
// Construct path for FDS save patches
|
|
snprintf(nstpaths.fdssave, sizeof(nstpaths.fdssave), "%s%s", nstpaths.savedir, nstpaths.gamename);
|
|
|
|
// Construct the save state path
|
|
snprintf(nstpaths.statepath, sizeof(nstpaths.statepath), "%sstate/%s", nstpaths.nstdir, nstpaths.gamename);
|
|
|
|
// Construct the cheat path
|
|
snprintf(nstpaths.cheatpath, sizeof(nstpaths.cheatpath), "%scheats/%s.xml", nstpaths.nstdir, nstpaths.gamename);
|
|
}
|
|
|
|
void nst_set_region() {
|
|
// Set the region
|
|
Machine machine(emulator);
|
|
Cartridge::Database database(emulator);
|
|
|
|
/*if (database.IsLoaded()) {
|
|
std::ifstream dbfile(filename, std::ios::in|std::ios::binary);
|
|
Cartridge::Profile profile;
|
|
Cartridge::ReadInes(dbfile, nst_default_system(), profile);
|
|
dbentry = database.FindEntry(profile.hash, nst_default_system());
|
|
printf("Mapper: %d\n", dbentry.GetMapper());
|
|
}*/
|
|
|
|
switch (conf.misc_default_system) {
|
|
case 0: machine.SetMode(machine.GetDesiredMode()); break; // Auto
|
|
case 1: machine.SetMode(Machine::NTSC); break; // NTSC
|
|
case 2: machine.SetMode(Machine::PAL); break; // PAL
|
|
case 3: machine.SetMode(Machine::NTSC); break; // Famicom
|
|
case 4: machine.SetMode(Machine::PAL); break; // Dendy
|
|
}
|
|
}
|
|
|
|
void nst_set_rewind(int direction) {
|
|
// Set the rewinder backward or forward
|
|
switch (direction) {
|
|
case 0: Rewinder(emulator).SetDirection(Rewinder::BACKWARD); break;
|
|
case 1: Rewinder(emulator).SetDirection(Rewinder::FORWARD); break;
|
|
default: break;
|
|
}
|
|
}
|
|
|
|
void nst_state_save(const char *filename) {
|
|
// Save a state by filename
|
|
Machine machine(emulator);
|
|
|
|
std::ofstream statefile(filename, std::ifstream::out|std::ifstream::binary);
|
|
|
|
if (statefile.is_open()) { machine.SaveState(statefile, Nes::Api::Machine::NO_COMPRESSION); }
|
|
fprintf(stderr, "State Saved: %s\n", filename);
|
|
nst_video_print("State Saved", 8, 212, 2, true);
|
|
}
|
|
|
|
void nst_state_load(const char *filename) {
|
|
// Load a state by filename
|
|
Machine machine(emulator);
|
|
|
|
std::ifstream statefile(filename, std::ifstream::in|std::ifstream::binary);
|
|
|
|
if (statefile.is_open()) { machine.LoadState(statefile); }
|
|
fprintf(stderr, "State Loaded: %s\n", filename);
|
|
nst_video_print("State Loaded", 8, 212, 2, true);
|
|
}
|
|
|
|
void nst_state_quicksave(int slot) {
|
|
// Quick Save State
|
|
if (!loaded) { return; }
|
|
char slotpath[520];
|
|
snprintf(slotpath, sizeof(slotpath), "%s_%d.nst", nstpaths.statepath, slot);
|
|
nst_state_save(slotpath);
|
|
}
|
|
|
|
|
|
void nst_state_quickload(int slot) {
|
|
// Quick Load State
|
|
if (!loaded) { return; }
|
|
char slotpath[520];
|
|
snprintf(slotpath, sizeof(slotpath), "%s_%d.nst", nstpaths.statepath, slot);
|
|
|
|
struct stat qloadstat;
|
|
if (stat(slotpath, &qloadstat) == -1) {
|
|
fprintf(stderr, "No State to Load\n");
|
|
nst_video_print("No State to Load", 8, 212, 2, true);
|
|
return;
|
|
}
|
|
|
|
nst_state_load(slotpath);
|
|
}
|
|
|
|
int nst_timing_runframes() {
|
|
// Calculate how many emulation frames to run
|
|
if (ffspeed) { return conf.timing_ffspeed; }
|
|
return 1;
|
|
}
|
|
|
|
void nst_timing_set_ffspeed() {
|
|
// Set the framerate to the fast-forward speed
|
|
ffspeed = true;
|
|
audio_set_speed(conf.timing_ffspeed);
|
|
}
|
|
|
|
void nst_timing_set_default() {
|
|
// Set the framerate to the default
|
|
ffspeed = false;
|
|
audio_set_speed(1);
|
|
}
|
|
|
|
void nst_reset(bool hardreset) {
|
|
// Reset the machine (soft or hard)
|
|
Machine machine(emulator);
|
|
Fds fds(emulator);
|
|
machine.SetRamPowerState(conf.misc_power_state);
|
|
machine.Reset(hardreset);
|
|
|
|
// Set the FDS disk to defaults
|
|
fds.EjectDisk();
|
|
fds.InsertDisk(0, 0);
|
|
}
|
|
|
|
void nst_emuloop() {
|
|
// Main Emulation Loop
|
|
if (NES_SUCCEEDED(Rewinder(emulator).Enable(true))) {
|
|
Rewinder(emulator).EnableSound(true);
|
|
}
|
|
|
|
if (playing) {
|
|
// Pulse the turbo buttons
|
|
nst_input_turbo_pulse(cNstPads);
|
|
|
|
// Execute frames
|
|
for (int i = 0; i < nst_timing_runframes(); i++) {
|
|
emulator.Execute(cNstVideo, cNstSound, cNstPads);
|
|
}
|
|
}
|
|
}
|
|
|
|
void nst_unload() {
|
|
// Remove the cartridge and shut down the NES
|
|
Machine machine(emulator);
|
|
|
|
// Power down the NES
|
|
machine.Power(false);
|
|
|
|
// Remove the cartridge
|
|
machine.Unload();
|
|
}
|
|
|
|
void nst_pause() {
|
|
// Pauses the game
|
|
if (playing) {
|
|
audio_pause();
|
|
audio_deinit();
|
|
}
|
|
|
|
playing = false;
|
|
}
|
|
|
|
void nst_play() {
|
|
// Play the game
|
|
if (playing) { return; }
|
|
|
|
video_init();
|
|
audio_init();
|
|
nst_input_init();
|
|
nst_cheats_init(nstpaths.cheatpath);
|
|
nst_homebrew_init();
|
|
|
|
cNstVideo = new Video::Output;
|
|
cNstSound = new Sound::Output;
|
|
cNstPads = new Input::Controllers;
|
|
|
|
audio_set_params(cNstSound);
|
|
audio_unpause();
|
|
|
|
if (nst_nsf()) {
|
|
Nsf nsf(emulator);
|
|
nsf.PlaySong();
|
|
video_disp_nsf();
|
|
}
|
|
|
|
playing = true;
|
|
}
|
|
|
|
int nst_load(const char *filename) {
|
|
// Load a Game ROM
|
|
Machine machine(emulator);
|
|
Nsf nsf(emulator);
|
|
Sound sound(emulator);
|
|
Nes::Result result;
|
|
char *rom;
|
|
int romsize;
|
|
char patchname[512];
|
|
|
|
// Pause play before pulling out a cartridge
|
|
if (playing) { nst_pause(); }
|
|
|
|
// Pull out any inserted cartridges
|
|
if (loaded) { nst_unload(); }
|
|
nst_video_print_time("", false);
|
|
|
|
// Check if the file is an archive and select the file within
|
|
char reqfile[256]; // Requested file inside the archive
|
|
if (nst_archive_select(filename, reqfile, sizeof(reqfile))) {
|
|
// Extract the contents
|
|
nst_archive_open(filename, &rom, &romsize, reqfile);
|
|
|
|
// Convert the malloc'd char* to an istream
|
|
std::string rombuf(rom, romsize);
|
|
std::istringstream file(rombuf);
|
|
free(rom);
|
|
|
|
result = machine.Load(file, nst_default_system());
|
|
}
|
|
else { // Otherwise just load the file
|
|
std::ifstream file(filename, std::ios::in|std::ios::binary);
|
|
|
|
// Set the file paths
|
|
nst_set_paths(filename);
|
|
|
|
if (nst_find_patch(patchname, sizeof(patchname), filename)) { // Load with a patch if there is one
|
|
std::ifstream pfile(patchname, std::ios::in|std::ios::binary);
|
|
Machine::Patch patch(pfile, false);
|
|
result = machine.Load(file, nst_default_system(), patch);
|
|
}
|
|
else { result = machine.Load(file, nst_default_system()); }
|
|
}
|
|
|
|
if (NES_FAILED(result)) {
|
|
char errorstring[32];
|
|
switch (result) {
|
|
case Nes::RESULT_ERR_INVALID_FILE:
|
|
snprintf(errorstring, sizeof(errorstring), "Error: Invalid file");
|
|
break;
|
|
|
|
case Nes::RESULT_ERR_OUT_OF_MEMORY:
|
|
snprintf(errorstring, sizeof(errorstring), "Error: Out of Memory");
|
|
break;
|
|
|
|
case Nes::RESULT_ERR_CORRUPT_FILE:
|
|
snprintf(errorstring, sizeof(errorstring), "Error: Corrupt or Missing File");
|
|
break;
|
|
|
|
case Nes::RESULT_ERR_UNSUPPORTED_MAPPER:
|
|
snprintf(errorstring, sizeof(errorstring), "Error: Unsupported Mapper");
|
|
break;
|
|
|
|
case Nes::RESULT_ERR_MISSING_BIOS:
|
|
snprintf(errorstring, sizeof(errorstring), "Error: Missing Fds BIOS");
|
|
break;
|
|
|
|
default:
|
|
snprintf(errorstring, sizeof(errorstring), "Error: %d", result);
|
|
break;
|
|
}
|
|
|
|
fprintf(stderr, "%s\n", errorstring);
|
|
|
|
return 0;
|
|
}
|
|
|
|
// Deal with any DIP Switches
|
|
nst_dipswitch();
|
|
|
|
// Set the region
|
|
nst_set_region();
|
|
|
|
if (machine.Is(Machine::DISK)) {
|
|
Fds fds(emulator);
|
|
fds.InsertDisk(0, 0);
|
|
nst_fds_info();
|
|
}
|
|
|
|
// Check if this is an NSF
|
|
if (nst_nsf()) { nsf.StopSong(); }
|
|
|
|
// Check if sound distortion should be enabled
|
|
sound.SetGenie(conf.misc_genie_distortion);
|
|
|
|
// Load the custom palette
|
|
nst_palette_load(nstpaths.palettepath);
|
|
|
|
// Set the RAM's power state
|
|
machine.SetRamPowerState(conf.misc_power_state);
|
|
|
|
// Power on
|
|
machine.Power(true);
|
|
|
|
loaded = 1;
|
|
return loaded;
|
|
}
|