Add randomizer mode

main
Síle Ekaterin Liszka 2021-06-04 00:26:19 -05:00
parent 592261df54
commit b13975e589
7 changed files with 615 additions and 19 deletions

View File

@ -18,6 +18,7 @@ body {
<ul>
<li>Base:<ul>
<li>None &mdash; The base game.</li>
<li>Randomizer &mdash; Job skills are randomized.</li>
<li><a href="https://www.dropbox.com/s/ldlmpoepxk5nxgl/fiesta.ups?dl=0">Unlocked Jobs</a> &mdash; All Jobs are unlocked from the beginning.</li>
<li><a href="https://www.dropbox.com/s/g52xshu0juaa2c7/ffvamod-doc.txt?dl=0">Balance</a> &mdash; A remixed version which adds the Hero Job and changes things up.</li>
<li><a href="http://jeffludwig.com/ff5a/download.php">Custom Classes</a> &mdash; A different remixed version by ludmeister.</li>

View File

@ -3,7 +3,7 @@ enum Job {
Pitfalls = 0x02,
LiteStep = 0x04,
Dash = 0x08,
Learning = 0x10
ILearning = 0x10
};
const long int job_innates = 0x156138;

View File

@ -30,9 +30,9 @@
#include <cstdlib>
#include <cstdio>
#include <ctime>
#include <climits>
Exdeath::Exdeath(QSettings *cfg, QWidget *parent) : QWidget(parent) {
rand = new QRandomGenerator(time(NULL));
error = new QErrorMessage();
filename = nullptr;
_cfg = cfg;
@ -68,6 +68,7 @@ void Exdeath::initMain(void) {
txtPortraits = new QLabel("FFT-style Portraits:");
txtSound = new QLabel("Sound Restoration:");
txtSound->setToolTip("Requires GBA BIOS if using VisualBoyAdvance");
txtSeed = new QLabel("Seed:");
txtNED = new QLabel("Neo ExDeath:");
btnROM = new QPushButton("Select ROM");
@ -77,6 +78,7 @@ void Exdeath::initMain(void) {
selMode = new QComboBox();
selMode->addItem("Base");
selMode->addItem("Randomizer");
selMode->addItem("Unlocked Jobs", "unlock.ips");
selMode->addItem("Balance", "balance.ips");
selMode->addItem("Custom Classes", "custom_classes.ips");
@ -85,6 +87,11 @@ void Exdeath::initMain(void) {
chkPortraits = new QCheckBox("Yes");
chkSound = new QCheckBox("Yes");
numSeed = new QSpinBox();
numSeed->setRange(0, INT_MAX);
// provide a default value so it's not just 0 all the time
numSeed->setValue(time(NULL) / 3600);
selNED = new QComboBox();
selNED->addItem("Random");
selNED->addItem("Vanilla");
@ -121,9 +128,11 @@ void Exdeath::initMain(void) {
layMain->addWidget(chkPortraits, 2, 1);
layMain->addWidget(txtSound, 3, 0);
layMain->addWidget(chkSound, 3, 1);
layMain->addWidget(txtNED, 4, 0);
layMain->addWidget(selNED, 4, 1);
layMain->addWidget(btnSave, 5, 1);
layMain->addWidget(txtSeed, 4, 0);
layMain->addWidget(numSeed, 4, 1);
layMain->addWidget(txtNED, 5, 0);
layMain->addWidget(selNED, 5, 1);
layMain->addWidget(btnSave, 6, 1);
}
void Exdeath::initInnates(void) {
@ -256,8 +265,10 @@ void Exdeath::btnApply_clicked(bool trigger) {
);
QFile::copy(filename, output);
int mode = selMode->currentIndex();
std::mt19937 rand(numSeed->value());
std::uniform_int_distribution<> dist(1, selNED->count() - 1);
if (mode > 0) {
if (mode > 1) {
patches << ":/patches/" + selMode->itemData(mode).toString();
}
if (chkPortraits->isChecked()) {
@ -268,10 +279,10 @@ void Exdeath::btnApply_clicked(bool trigger) {
}
int idx = selNED->currentIndex();
if (idx == 0) {
idx = rand->bounded(1, selNED->count() - 1);
idx = dist(rand);
}
if ((idx == 1) && (mode == 2)) {
error->showMessage("You can't set a custom NED with the Balance patch; it'll break.");
if ((idx == 1) && ((mode > 2) && (mode < 5))) {
error->showMessage("You can't set a custom NED with this mode; it'll break.");
return;
}
if (idx > 1) {
@ -303,19 +314,26 @@ void Exdeath::btnApply_clicked(bool trigger) {
target->open(QIODevice::ReadWrite);
if (patches.size() > 0) {
for (int i = 0; i < patches.size(); i++) {
applyPatch(target, patches[i]);
QFile *patch = new QFile(patches[i]);
patch->open(QIODevice::ReadOnly);
applyPatch(target, patch);
}
}
if (chkPassages->isChecked() || chkPitfalls->isChecked() || chkLiteStep->isChecked() || chkDash->isChecked() || chkLearning->isChecked()) {
bool global_innates = (chkPassages->isChecked() || chkPitfalls->isChecked() || chkLiteStep->isChecked() || chkDash->isChecked() || chkLearning->isChecked());
// gyahahaha RANDOMIZER!!
if (mode == 1) {
Randomizer *rando = new Randomizer(rand);
QBuffer *patch = rando->makeRandom(global_innates);
applyPatch(target, patch);
}
if (global_innates) {
applyInnates(target);
}
target->close();
}
void Exdeath::applyPatch(QFile *file, QString patch) {
QFile *data = new QFile(patch);
data->open(QIODevice::ReadOnly);
void Exdeath::applyPatch(QFile *file, QIODevice *data) {
// skipping header
data->seek(5);
@ -365,7 +383,7 @@ void Exdeath::applyInnates(QFile *file) {
if (chkPitfalls->isChecked()) base |= Job::Pitfalls;
if (chkLiteStep->isChecked()) base |= Job::LiteStep;
if (chkDash->isChecked()) base |= Job::Dash;
if (chkLearning->isChecked()) base |= Job::Learning;
if (chkLearning->isChecked()) base |= Job::ILearning;
for (int i = 0; i < 26; i++) {
char temp[2];

View File

@ -1,6 +1,8 @@
#ifndef EXDEATH_HH_GUARD
#define EXDEATH_HH_GUARD
#include <random>
#include <QButtonGroup>
#include <QCheckBox>
#include <QComboBox>
@ -15,13 +17,14 @@
#include <QLabel>
#include <QPushButton>
#include <QRadioButton>
#include <QRandomGenerator>
#include <QSettings>
#include <QSpinBox>
#include <QStandardPaths>
#include <QVBoxLayout>
#include <QWidget>
#include "data.hh"
#include "randomizer.hh"
class Exdeath : public QWidget {
public:
@ -31,7 +34,6 @@ public:
private:
QSettings *_cfg;
QString filename;
QRandomGenerator *rand;
QErrorMessage *error;
@ -48,6 +50,7 @@ private:
QLabel *txtPortraits;
QLabel *txtSound;
QLabel *txtNED;
QLabel *txtSeed;
QPushButton *btnROM;
QPushButton *btnApply;
@ -68,6 +71,7 @@ private:
QCheckBox *chkLiteStep;
QCheckBox *chkDash;
QCheckBox *chkLearning;
QSpinBox *numSeed;
QGroupBox *grpMulti;
QFormLayout *layMulti;
@ -88,7 +92,7 @@ private:
void btnROM_clicked(bool trigger);
void btnApply_clicked(bool trigger);
void btnSave_clicked(bool trigger);
void applyPatch(QFile *file, QString patch);
void applyPatch(QFile *file, QIODevice *Data);
void applyInnates(QFile *file);
};

115
src/randodata.hh Normal file
View File

@ -0,0 +1,115 @@
#pragma once
enum Skill {
// Action skills
Guard = 0x06,
Kick = 0x07,
Focus = 0x08,
Chakra = 0x09,
Flee = 0x0A,
Steal = 0x0B,
Mug = 0x0C,
Jump = 0x0D,
Lancet = 0x0E,
Smoke = 0x0F,
Image = 0x10,
Throw = 0x11,
Mineuchi = 0x12,
GilToss = 0x13,
Iainuki = 0x14,
Animals = 0x15,
Aim = 0x16,
RapidFire = 0x17,
Call = 0x18,
Check = 0x19,
Scan = 0x1A,
Calm = 0x1B,
Control = 0x1C,
Catch = 0x1D,
Mix = 0x1F,
Drink = 0x20,
Recover = 0x21,
Revive = 0x22,
Gaia = 0x23,
Hide = 0x25,
Sing = 0x28,
Flirt = 0x29,
Dance = 0x2A,
DualCast = 0x4C,
Blue = 0x4D,
// Magic-tier Action skills
Spellblade1 = 0x2C,
Spellblade2 = 0x2D,
Spellblade3 = 0x2E,
Spellblade4 = 0x2F,
Spellblade5 = 0x30,
Spellblade6 = 0x31,
White1 = 0x32,
White2 = 0x33,
White3 = 0x34,
White4 = 0x35,
White5 = 0x36,
White6 = 0x37,
Black1 = 0x38,
Black2 = 0x39,
Black3 = 0x3A,
Black4 = 0x3B,
Black5 = 0x3C,
Black6 = 0x3D,
Time1 = 0x3E,
Time2 = 0x3F,
Time3 = 0x40,
Time4 = 0x41,
Time5 = 0x42,
Time6 = 0x43,
Summon1 = 0x44,
Summon2 = 0x45,
Summon3 = 0x46,
Summon4 = 0x47,
Summon5 = 0x48,
Red1 = 0x49,
Red2 = 0x4A,
Red3 = 0x4B,
// Passive/innate skills
EquipShield = 0x80,
EquipArmor = 0x81,
EquipRibbon = 0x82,
EquipSword = 0x83,
EquipSpear = 0x84,
EquipKatana = 0x85,
EquipAxe = 0x86,
EquipBow = 0x87,
EquipWhip = 0x88,
EquipHarp = 0x89,
ArtfulDodger = 0x8A,
HP10 = 0x8B,
HP20 = 0x8C,
HP30 = 0x8D,
MP10 = 0x8E,
MP30 = 0x8F,
Barehanded = 0x90,
TwoHanded = 0x91,
DualWield = 0x92,
Pharmacology = 0x93,
Cover = 0x94,
Counter = 0x95,
Shirahadori = 0x96,
Learning = 0x97,
MagicShield = 0x98,
Berserk = 0x99,
Vigilance = 0x9A,
FirstStrike = 0x9B,
FindPassages = 0x9C,
LightStep = 0x9D,
FindPits = 0x9E,
EquipRods = 0x9F,
Sprint = 0xA0
};
#define JOB_COUNT 20
const int tier1_jobs[] = {9, 10, 11, 12, 16, 17, 18};
const int tier2_jobs[] = {2, 13, 14, 13, 1, 19};
const int tier3_jobs[] = {0, 3, 4, 5, 8, 7, 15};

400
src/randomizer.cc Normal file
View File

@ -0,0 +1,400 @@
/*
* Copyright (c) 2021, Sheila Aman.
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* * Neither the name of the software nor the
* names of its contributors may be used to endorse or promote products
* derived from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#include <algorithm>
#include "randomizer.hh"
Randomizer::Randomizer(std::mt19937 rand) {
rng = rand;
}
Randomizer::~Randomizer() {}
QByteArray Randomizer::int2byte(int num) {
QByteArray temp;
temp.append(num & 0xFF);
temp.append((num >> 8) & 0xFF);
temp.append((num >> 16) & 0xFF);
temp.append((num >> 24) & 0xFF);
return temp;
}
static std::uniform_int_distribution<> dist_13(1, 3);
static std::uniform_int_distribution<> dist_14(1, 4);
static std::uniform_int_distribution<> dist_15(1, 5);
static std::uniform_int_distribution<> dist_16(1, 6);
static std::uniform_int_distribution<> dist_17(1, 7);
static std::uniform_int_distribution<> dist_1x(1, 10);
int Randomizer::tierCost(int tier, int minimum) {
int guess;
switch (tier) {
case 0:
guess = (1 + dist_15(rng)) * 5;
if ((guess <= (minimum + 10)) && (minimum != 0)) {
return minimum + (1 + dist_13(rng)) * 5;
}
return guess;
break;
case 1:
guess = (8 + dist_17(rng)) * 5;
if ((guess <= (minimum + 15)) && (minimum != 0)) {
return minimum + (2 + dist_13(rng)) * 5;
}
return guess;
break;
case 2:
guess = (15 + dist_15(rng)) * 5;
if ((guess <= (minimum + 25)) && (minimum != 0)) {
return minimum + (4 + dist_16(rng)) * 5;
}
return guess;
break;
case 3:
guess = (14 + dist_15(rng)) * 10;
if ((guess <= (minimum + 40)) && (minimum != 0)) {
return minimum + (3 + dist_13(rng)) * 10;
}
return guess;
break;
case 4:
guess = (24 + dist_16(rng)) * 10;
if ((guess <= (minimum + 50)) && (minimum != 0)) {
return minimum + (4 + dist_16(rng)) * 10;
}
return guess;
break;
case 5:
guess = (14 + dist_16(rng)) + (19 + dist_16(rng));
guess *= 10;
if (guess <= (minimum + 100)) {
return minimum + (10 + dist_1x(rng)) * 10;
}
return guess;
break;
default:
return minimum;
}
}
void Randomizer::setupData(bool global_innates) {
magic1 = new QVector<Skill>({
Spellblade1,
White1,
Black1,
Time1,
Red1,
Summon1
});
magic2 = new QVector<Skill>({
Spellblade2,
White2,
Black2,
Time2,
Red2,
Summon2
});
magic3 = new QVector<Skill>({
Spellblade3,
White3,
Black3,
Time3,
Red3,
Summon3
});
magic4 = new QVector<Skill>({
Spellblade4,
White4,
Black4,
Time4
});
magic5 = new QVector<Skill>({
Spellblade5,
White5,
Black5,
Time5,
Summon4
});
magic6 = new QVector<Skill>({
Spellblade6,
White6,
Black6,
Time6,
Summon5
});
action1 = new QVector<Skill>({
Guard,
Kick,
Chakra,
Lancet,
Image,
Mineuchi,
Aim,
Check,
Calm,
Recover,
Hide,
Flirt,
Scan
});
action2 = new QVector<Skill>({
Steal,
Jump,
Focus,
Smoke,
Flee,
Gaia,
Dance,
Blue,
Revive,
Animals,
Catch,
Sing,
Call
});
action3 = new QVector<Skill>({
Throw,
GilToss,
Iainuki,
RapidFire,
Control,
Mix,
Drink,
DualCast
});
passive1 = new QVector<Skill>({
HP10,
MP10,
Pharmacology,
Cover,
Counter,
Learning,
MagicShield,
Berserk,
FindPassages,
LightStep,
FindPits,
Sprint
});
passive2 = new QVector<Skill>({
EquipSpear,
EquipArmor,
EquipBow,
EquipKatana,
MP30,
HP20,
Barehanded,
Shirahadori,
Vigilance,
FirstStrike
});
passive3 = new QVector<Skill>({
EquipRods,
TwoHanded,
HP30,
EquipAxe,
EquipRibbon,
EquipSword,
ArtfulDodger,
EquipHarp,
EquipWhip,
EquipShield
});
field = new QVector<Skill>({
HP10,
MP10,
Pharmacology,
Cover,
Counter,
MagicShield,
Berserk
});
if (!global_innates) {
passive1->append(*field);
}
tier1 = new QVector<Skill>(*magic1 + *passive1);
tier2 = new QVector<Skill>(*magic2 + *action1);
tier3 = new QVector<Skill>(*magic3 + *passive2);
tier4 = new QVector<Skill>(*magic4 + *action2);
tier5 = new QVector<Skill>(*magic5 + *passive3);
tier6 = new QVector<Skill>(*magic6 + *action3);
}
void Randomizer::randomizeSkills(void) {
QVector<Skill> *l_tier1 = new QVector<Skill>(*tier1);
QVector<Skill> *l_tier2 = new QVector<Skill>(*tier2);
QVector<Skill> *l_tier3 = new QVector<Skill>(*tier3);
QVector<Skill> *l_tier4 = new QVector<Skill>(*tier4);
QVector<Skill> *l_tier5 = new QVector<Skill>(*tier5);
QVector<Skill> *l_tier6 = new QVector<Skill>(*tier6);
QVector<int> *jobs1 = new QVector<int>(*tier1_jobs);
QVector<int> *jobs2 = new QVector<int>(*tier2_jobs);
QVector<int> *jobs3 = new QVector<int>(*tier3_jobs);
actions = new QVector<Skill>(*l_tier6);
QVector<Skill> *skills[6];
commands = new QVector<Skill>(JOB_COUNT);
int tierpos = 0;
std::shuffle(l_tier1->begin(), l_tier1->end(), rng);
std::shuffle(l_tier2->begin(), l_tier2->end(), rng);
std::shuffle(l_tier3->begin(), l_tier3->end(), rng);
std::shuffle(l_tier4->begin(), l_tier4->end(), rng);
std::shuffle(l_tier5->begin(), l_tier5->end(), rng);
std::shuffle(l_tier6->begin(), l_tier6->end(), rng);
skills[0] = new QVector<Skill>(*l_tier1);
skills[1] = new QVector<Skill>(*l_tier2);
skills[2] = new QVector<Skill>(*l_tier3);
skills[3] = new QVector<Skill>(*l_tier4);
skills[4] = new QVector<Skill>(*l_tier5);
skills[5] = new QVector<Skill>(*l_tier6);
std::shuffle(l_tier6->begin(), l_tier6->end(), rng);
std::shuffle(l_tier4->begin(), l_tier4->end(), rng);
std::shuffle(l_tier2->begin(), l_tier2->end(), rng);
for (int i = 0; i < 20; i++) {
jobs[i] = new QVector<Skill>();
costs[i] = new QVector<int>();
}
while (tierpos < 6) {
if (skills[tierpos]->count() < 1) {
tierpos += 1;
}
for (int i = 0; i < JOB_COUNT; i++) {
Skill temp = skills[i]->first();
int minimum = 0;
if (costs[i]->count() > 0) {
minimum = costs[i]->last();
}
skills[i]->removeFirst();
jobs[i]->append(temp);
costs[i]->append(tierCost(tierpos, minimum));
}
}
std::shuffle(jobs1->begin(), jobs1->end(), rng);
std::shuffle(jobs2->begin(), jobs2->end(), rng);
std::shuffle(jobs3->begin(), jobs3->end(), rng);
commands->insert(6, Berserk);
for (int i = 0; i < jobs1->count(); i++) {
int job = jobs1->at(i);
Skill temp = actions->first();
actions->removeFirst();
commands->insert(job, temp);
}
actions->append(*l_tier4);
for (int i = 0; i < jobs2->count(); i++) {
int job = jobs2->at(i);
Skill temp = actions->first();
actions->removeFirst();
commands->insert(job, temp);
}
actions->append(*l_tier2);
for (int i = 0; i < jobs3->count(); i++) {
int job = jobs3->at(i);
Skill temp = actions->first();
actions->removeFirst();
commands->insert(job, temp);
}
}
QByteArray be24(int num) {
QByteArray temp;
temp.append((num >> 16) & 0xFF);
temp.append((num >> 8) & 0xFF);
temp.append(num & 0xFF);
return temp;
}
QBuffer *Randomizer::writePatch(void) {
QBuffer *patch = new QBuffer();
patch->write("PATCH");
// job commands
for (int i = 0; i < JOB_COUNT; i++) {
// skip berserker
if (i == 6) {
continue;
}
patch->write(be24(0x15616c + (i * 4) + 1));
patch->write("\x00\x01");
QByteArray temp;
temp.append(commands->at(i) & 0xFF);
patch->write(temp);
}
// ability counts
patch->write(be24(0x14b1fc));
patch->write("\x00\x14");
for (int i = 0; i < JOB_COUNT; i++) {
char count = jobs[i]->count();
patch->putChar(count);
}
// table offset
patch->write(be24(0x155484));
patch->write("\x00\x50");
for (int i = 0; i < JOB_COUNT; i++) {
patch->write("\xe8\x54\x15\x08");
}
// ability lists
patch->write(be24(0x1554E8));
patch->write("\x00\x50");
for (int i = 0; i < JOB_COUNT; i++) {
QVector<Skill> *job = jobs[i];
char count = job->count();
for (int j = 0; j < count; j++) {
QByteArray data;
int cost = costs[i]->at(j);
// cost
data.append(cost & 0xFF);
data.append((cost >> 8) & 0xFF);
// skill
data.append(job->at(j) & 0xFF);
data.append((char)0);
patch->write(data);
}
}
return patch;
}
QBuffer *Randomizer::makeRandom(bool global_innates) {
setupData(global_innates);
randomizeSkills();
return writePatch();
}

58
src/randomizer.hh Normal file
View File

@ -0,0 +1,58 @@
#pragma once
#include <random>
#include <QBuffer>
#include <QByteArray>
#include <QFile>
#include <QList>
#include <QString>
#include <QVector>
#include "randodata.hh"
class Randomizer {
public:
Randomizer(std::mt19937 rand);
~Randomizer();
QBuffer *makeRandom(bool global_innates = false);
private:
std::mt19937 rng;
QVector<Skill> *magic1;
QVector<Skill> *magic2;
QVector<Skill> *magic3;
QVector<Skill> *magic4;
QVector<Skill> *magic5;
QVector<Skill> *magic6;
QVector<Skill> *action1;
QVector<Skill> *action2;
QVector<Skill> *action3;
QVector<Skill> *passive1;
QVector<Skill> *passive2;
QVector<Skill> *passive3;
// field passives; to be added to passive1 if they're not going to
// be assigned globally.
QVector<Skill> *field;
QVector<Skill> *tier1;
QVector<Skill> *tier2;
QVector<Skill> *tier3;
QVector<Skill> *tier4;
QVector<Skill> *tier5;
QVector<Skill> *tier6;
// final results
QVector<Skill> *jobs[JOB_COUNT];
QVector<int> *costs[JOB_COUNT];
QVector<Skill> *commands;
QVector<Skill> *actions;
QByteArray int2byte(int num);
void setupData(bool global_innates = false);
int tierCost(int tier, int minimum);
void randomizeSkills(void);
QBuffer *writePatch(void);
};