timhul / ClassicSim

An event-driven simulation tool written in C++ for World of Warcraft Classic.
Other
116 stars 70 forks source link

Request: Any form of automation #145

Open jokal2 opened 4 years ago

jokal2 commented 4 years ago

I like to keep track of my current gear as a baseline and then have each item on my to-acquire list be simmed and tallied up. e.g.

Current: 623
With DFT: 661
With Crul'shorukh: 672
With Drake Talon Pauldrons: 630
... continue for another ~20 items

etc. However, every time I make an item adjustment this entire list is invalidated and I'm required to manually recalculate and record every item in my list by hand. e.g.

It would be nice to have some sort of automation of this step.

timhul commented 4 years ago

You could "automate" with a CLI (see https://github.com/timhul/ClassicSim/issues/38). It would be interesting to do this in the GUI as well, but I hope that people understand the risks of possible permutation explosions (it'd be easy to calculate this and warn the user about it though).

jokal2 commented 4 years ago

Is the code in Test::test_queue effectively all you need to run a full simulation? Having never touched Qt before some of the emit and binding magic obscures exactly how the simulation is actually launched. If test_queue is all you actually need I could tinker around and build off that.

timhul commented 4 years ago

Unfortunately no, that code only runs a single iteration (so e.g. 1 of 1000). It is effectively a sanity test that the Queue class doesn't immediately crash when we try to run events. It does not handle any results from that single iteration either.

The class that is the heavy duty lifter of interest in order to simply run the sim is SimulationThreadPool. It has dependencies on NumberCruncher, EquipmentDb, RandomAffixes, and SimSettings, but those are trivial to initialize as the default settings are fine.

const auto equipment_db = new EquipmentDb();
const auto random_affixes_db = new RandomAffixes();
const auto sim_settings = new SimSettings();
const auto number_cruncher = new NumberCruncher();
const auto thread_pool = new SimulationThreadPool(equipment_db, random_affixes_db, sim_settings, number_cruncher);

SimulationThreadPool

This class sets up the number of threads defined by SimSettings (default is as many threads as physically available on the machine) and will given setup strings (extracted data from Character instances) delegate the number of iterations to each thread (so, say 8 threads and 1000 iterations then this class makes sure each thread runs 125 iterations). The thread pool is responsible for scaling the number of threads up or down if the user changes it.

It is also responsible for waiting for each thread to finish and signals when all threads have completed their assigned iterations with the threads_finished() signal.

Since it uses a signal threads_finished() the SimulationThreadPool object does require a Q_OBJECT class to run, which is currently GUIControl. Q_OBJECT is a macro put in the class definition that tells QMake to generate a bunch of extra code that handles signals and slots. You should check how GUIControl sets up SimulationThreadPool and what the GUIControl::compile_thread_results does (which is what is currently connected to SimulationThreadPool::threads_finished() signal via the following line:

QObject::connect(thread_pool, SIGNAL(threads_finished()), this, SLOT(compile_thread_results()));

See GUIControl::runQuickSim() for an example how the SimulationThreadPool is being used to run the sim.

Compiling Statistics

Results are collected in NumberCruncher for all the involved Character instances (remember that CSIM supports simulating an entire raid).

If you check GUIControl::compile_thread_results() you can see many classes that are interested in getting results, but you seem to for this purpose only be interested in the personal dps.

qDebug() << number_cruncher->get_personal_dps(SimOption::Name::NoScale));
// Do a reset so it is ready for the next sim
number_cruncher->reset();

Putting it all together

You will need a Q_OBJECT class because you'll need to connect SimulationThreadPool::threads_finished() to your print of NumberCruncher::get_personal_dps().

.h definition:

#include <QObject>

class Foo : public QObject {
    Q_OBJECT
public:
    Foo(QObject* parent = nullptr);
   ~Foo();

    void run();

public slots:
    void compile_thread_results();

private:
    EquipmentDb* equipment_db;
    NumberCruncher* number_cruncher;
    RandomAffixes* random_affixes_db;
    SimSettings* sim_settings;
    SimulationThreadPool* thread_pool;
}

.cpp implementation:

#include "Foo.h"

#include <QDebug>

#include "EquipmentDb.h"
#include "NumberCruncher.h"
#include "RandomAffixes.h"
#include "SimSettings.h"
#include "SimulationThreadPool.h"

Foo::Foo(QObject* parent):
    QObject(parent),
    equipment_db(new EquipmentDb()),
    number_cruncher(new NumberCruncher()),
    random_affixes_db(new RandomAffixes()),
    sim_settings(new SimSettings())
    {
     thread_pool = new SimulationThreadPool(equipment_db, random_affixes_db, sim_settings, number_cruncher);
}

Foo::~Foo() {
    delete thread_pool;
    delete sim_settings;
    delete random_affixes_db;
    delete number_cruncher;
    delete equipment_db;
}

Foo::run() {
    // You can copy the Character setup from test_queue here.
    // See GUIControl::runQuickSim() how to extract setup strings from Character and how to set up the QVector to pass to SimulationThreadPool.

   // false below implies quick sim, no scaling of stat weights
   // 1 further implies no scaling of stat weights, don't remember right now why both are used.
   thread_pool->run_sim(setup_strings, false, sim_settings->get_combat_iterations_quick_sim(), 1);
}

Foo::compile_thread_results() {
    qDebug() << number_cruncher->get_personal_dps(SimOption::Name::NoScale));
    // Do a reset so it is ready for the next sim
    number_cruncher->reset();
}

Now you can, from e.g. main.cpp, invoke this class with Foo().run() and you should get an output from the Character setup you chose. Edit: No, you'll need a started event loop (which is done in main.cpp via QApplication::exec(). For simplicity I would put Foo().run() somewhere in GUIControl (must not be constructor, because event loop is not started yet).

I have not tested this, I'm sure there might be something I've missed here.

Side note: the Qt magic in this project is actually fairly limited since it only is used for cross-thread communication and objects that directly interact with the QML GUI. I've been in projects that used Qt stuff for everything but at that point the code reads like Qt++ rather than C++ with Qt enhancements.

jokal2 commented 4 years ago

Thanks for this!