2022-03-14 20:33:41 -06:00
//
// Created by josh on 7/28/21.
//
# include "Core.h"
2022-06-05 10:06:55 -06:00
# include <modules/Gui.h>
2022-03-14 20:33:41 -06:00
# include <Console.h>
# include <Export.h>
# include <PluginManager.h>
2022-08-28 19:11:50 -06:00
# include <modules/World.h>
2022-03-14 20:33:41 -06:00
# include <modules/EventManager.h>
# include <modules/Job.h>
# include <modules/Units.h>
# include <df/job.h>
# include <df/unit.h>
# include <df/historical_figure.h>
# include <df/global_objects.h>
# include <df/world.h>
2022-06-05 10:06:55 -06:00
# include <df/viewscreen.h>
2022-03-14 20:33:41 -06:00
# include <map>
# include <set>
# include <random>
2022-09-03 18:02:57 -06:00
DFHACK_PLUGIN ( " spectate " ) ;
DFHACK_PLUGIN_IS_ENABLED ( enabled ) ;
REQUIRE_GLOBAL ( world ) ;
REQUIRE_GLOBAL ( ui ) ;
REQUIRE_GLOBAL ( pause_state ) ;
REQUIRE_GLOBAL ( d_init ) ;
2022-03-14 20:33:41 -06:00
using namespace DFHack ;
using namespace df : : enums ;
2022-09-03 18:02:57 -06:00
void onTick ( color_ostream & out , void * tick ) ;
void onJobStart ( color_ostream & out , void * job ) ;
void onJobCompletion ( color_ostream & out , void * job ) ;
uint64_t tick_threshold = 50 ;
bool focus_jobs_enabled = false ;
bool disengage_enabled = false ;
bool unpause_enabled = false ;
2022-09-03 12:18:46 -06:00
Pausing : : AnnouncementLock * pause_lock = nullptr ;
bool lock_collision = false ;
2022-09-03 18:02:57 -06:00
2022-03-14 20:33:41 -06:00
bool following_dwarf = false ;
2022-06-05 10:06:55 -06:00
df : : unit * our_dorf = nullptr ;
2022-09-03 12:18:46 -06:00
df : : job * job_watched = nullptr ;
2022-03-14 20:33:41 -06:00
int32_t timestamp = - 1 ;
2022-09-03 18:02:57 -06:00
std : : set < int32_t > job_tracker ;
std : : map < uint16_t , uint16_t > freq ;
std : : default_random_engine RNG ;
2022-06-05 10:06:55 -06:00
# define base 0.99
2022-03-14 20:33:41 -06:00
2022-09-03 18:02:57 -06:00
static const std : : string CONFIG_KEY = std : : string ( plugin_name ) + " /config " ;
enum ConfigData {
UNPAUSE ,
DISENGAGE ,
JOB_FOCUS ,
TICK_THRESHOLD
} ;
static PersistentDataItem config ;
inline void saveConfig ( ) {
if ( config . isValid ( ) ) {
config . ival ( UNPAUSE ) = unpause_enabled ;
config . ival ( DISENGAGE ) = disengage_enabled ;
config . ival ( JOB_FOCUS ) = focus_jobs_enabled ;
config . ival ( TICK_THRESHOLD ) = tick_threshold ;
}
}
2022-03-14 20:33:41 -06:00
command_result spectate ( color_ostream & out , std : : vector < std : : string > & parameters ) ;
DFhackCExport command_result plugin_init ( color_ostream & out , std : : vector < PluginCommand > & commands ) {
commands . push_back ( PluginCommand ( " spectate " ,
" Automated spectator mode. " ,
2022-08-24 12:45:16 -06:00
spectate ,
2022-09-02 22:38:48 -06:00
false ) ) ;
2022-08-28 19:11:50 -06:00
pause_lock = World : : AcquireAnnouncementPauseLock ( " spectate " ) ;
2022-03-14 20:33:41 -06:00
return CR_OK ;
}
DFhackCExport command_result plugin_shutdown ( color_ostream & out ) {
2022-08-28 19:11:50 -06:00
World : : ReleasePauseLock ( pause_lock ) ;
2022-03-14 20:33:41 -06:00
return CR_OK ;
}
2022-09-03 18:02:57 -06:00
DFhackCExport command_result plugin_load_data ( color_ostream & out ) {
config = World : : GetPersistentData ( CONFIG_KEY ) ;
2022-09-03 12:18:46 -06:00
2022-09-03 18:02:57 -06:00
if ( ! config . isValid ( ) ) {
config = World : : AddPersistentData ( CONFIG_KEY ) ;
saveConfig ( ) ;
2022-09-03 12:18:46 -06:00
} else {
2022-09-03 18:02:57 -06:00
unpause_enabled = config . ival ( UNPAUSE ) ;
disengage_enabled = config . ival ( DISENGAGE ) ;
focus_jobs_enabled = config . ival ( JOB_FOCUS ) ;
tick_threshold = config . ival ( TICK_THRESHOLD ) ;
2022-08-28 19:11:50 -06:00
}
2022-09-03 18:02:57 -06:00
return DFHack : : CR_OK ;
2022-08-28 19:11:50 -06:00
}
2022-03-14 20:33:41 -06:00
DFhackCExport command_result plugin_enable ( color_ostream & out , bool enable ) {
namespace EM = EventManager ;
if ( enable & & ! enabled ) {
out . print ( " Spectate mode enabled! \n " ) ;
using namespace EM : : EventType ;
EM : : EventHandler ticking ( onTick , 15 ) ;
EM : : EventHandler start ( onJobStart , 0 ) ;
EM : : EventHandler complete ( onJobCompletion , 0 ) ;
EM : : registerListener ( EventType : : TICK , ticking , plugin_self ) ;
EM : : registerListener ( EventType : : JOB_STARTED , start , plugin_self ) ;
EM : : registerListener ( EventType : : JOB_COMPLETED , complete , plugin_self ) ;
} else if ( ! enable & & enabled ) {
2022-06-05 10:06:55 -06:00
// warp 8, engage!
2022-03-14 20:33:41 -06:00
out . print ( " Spectate mode disabled! \n " ) ;
EM : : unregisterAll ( plugin_self ) ;
job_tracker . clear ( ) ;
freq . clear ( ) ;
}
enabled = enable ;
2022-08-24 12:45:16 -06:00
return DFHack : : CR_OK ;
2022-03-14 20:33:41 -06:00
}
2022-09-03 18:02:57 -06:00
DFhackCExport command_result plugin_onstatechange ( color_ostream & out , state_change_event event ) {
if ( enabled & & world ) {
switch ( event ) {
case SC_MAP_UNLOADED :
case SC_BEGIN_UNLOAD :
case SC_WORLD_UNLOADED :
our_dorf = nullptr ;
job_watched = nullptr ;
following_dwarf = false ;
default :
break ;
}
}
return CR_OK ;
}
2022-09-03 12:18:46 -06:00
DFhackCExport command_result plugin_onupdate ( color_ostream & out ) {
if ( lock_collision ) {
if ( unpause_enabled ) {
// player asked for auto-unpause enabled
World : : SaveAnnouncementSettings ( ) ;
if ( World : : DisableAnnouncementPausing ( ) ) {
// now that we've got what we want, we can lock it down
lock_collision = false ;
pause_lock - > lock ( ) ;
}
} else {
if ( World : : RestoreAnnouncementSettings ( ) ) {
lock_collision = false ;
}
}
}
while ( unpause_enabled & & ! world - > status . popups . empty ( ) ) {
// dismiss announcement popup(s)
Gui : : getCurViewscreen ( true ) - > feed_key ( interface_key : : CLOSE_MEGA_ANNOUNCEMENT ) ;
}
if ( disengage_enabled ) {
if ( our_dorf & & our_dorf - > id ! = df : : global : : ui - > follow_unit ) {
plugin_enable ( out , false ) ;
}
}
return DFHack : : CR_OK ;
}
2022-09-03 18:02:57 -06:00
void enable_auto_unpause ( color_ostream & out , bool state ) {
if ( unpause_enabled ! = state & & lock_collision ) {
// when enabled, lock collision means announcements haven't been disabled
// when disabled, lock collision means announcement are still disabled
// the only state left to consider here is what the lock should be set to
lock_collision = false ;
unpause_enabled = state ;
if ( unpause_enabled ) {
pause_lock - > lock ( ) ;
} else {
// this one should be redundant, the lock should already be unlocked right now
pause_lock - > unlock ( ) ;
}
return ;
}
unpause_enabled = state ;
// update the announcement settings if we can
if ( unpause_enabled ) {
if ( World : : SaveAnnouncementSettings ( ) ) {
World : : DisableAnnouncementPausing ( ) ;
pause_lock - > lock ( ) ;
} else {
lock_collision = true ;
}
} else {
pause_lock - > unlock ( ) ;
if ( ! World : : RestoreAnnouncementSettings ( ) ) {
// this in theory shouldn't happen, if others use the lock like we do in spectate
lock_collision = true ;
}
}
if ( lock_collision ) {
out . printerr ( " auto-unpause: must wait for another Pausing::AnnouncementLock to be lifted. This setting will complete when the lock lifts. \n " ) ;
}
}
2022-03-14 20:33:41 -06:00
command_result spectate ( color_ostream & out , std : : vector < std : : string > & parameters ) {
2022-06-05 10:06:55 -06:00
if ( ! parameters . empty ( ) ) {
2022-09-03 12:18:46 -06:00
if ( parameters . size ( ) % 2 ! = 0 ) {
return DFHack : : CR_WRONG_USAGE ;
}
2022-09-03 12:41:29 -06:00
for ( size_t i = 0 ; i + 1 < parameters . size ( ) ; i + = 2 ) {
2022-09-03 12:18:46 -06:00
if ( parameters [ i ] = = " auto-unpause " ) {
if ( parameters [ i + 1 ] = = " 0 " ) {
enable_auto_unpause ( out , false ) ;
} else if ( parameters [ i + 1 ] = = " 1 " ) {
enable_auto_unpause ( out , true ) ;
2022-08-28 19:11:50 -06:00
} else {
2022-09-03 12:18:46 -06:00
return DFHack : : CR_WRONG_USAGE ;
2022-08-28 19:11:50 -06:00
}
2022-09-03 12:18:46 -06:00
} else if ( parameters [ i ] = = " auto-disengage " ) {
if ( parameters [ i + 1 ] = = " 0 " ) {
disengage_enabled = false ;
} else if ( parameters [ i + 1 ] = = " 1 " ) {
disengage_enabled = true ;
} else {
return DFHack : : CR_WRONG_USAGE ;
}
} else if ( parameters [ i ] = = " focus-jobs " ) {
if ( parameters [ i + 1 ] = = " 0 " ) {
focus_jobs_enabled = false ;
} else if ( parameters [ i + 1 ] = = " 1 " ) {
focus_jobs_enabled = true ;
} else {
return DFHack : : CR_WRONG_USAGE ;
}
2022-09-03 18:02:57 -06:00
} else if ( parameters [ i ] = = " tick-threshold " ) {
2022-09-03 12:18:46 -06:00
try {
2022-09-03 18:02:57 -06:00
tick_threshold = std : : stol ( parameters [ i + 1 ] ) ;
2022-09-03 12:18:46 -06:00
} catch ( const std : : exception & e ) {
out . printerr ( " %s \n " , e . what ( ) ) ;
2022-08-28 19:11:50 -06:00
}
} else {
2022-06-05 10:06:55 -06:00
return DFHack : : CR_WRONG_USAGE ;
}
}
2022-08-24 12:45:16 -06:00
} else {
2022-09-03 18:02:57 -06:00
out . print ( " Spectate is %s \n " , enabled ? " ENABLED. " : " DISABLED. " ) ;
out . print ( " tick-threshold: %zu \n " , tick_threshold ) ;
out . print ( " focus-jobs: %s \n " , focus_jobs_enabled ? " on. " : " off. " ) ;
out . print ( " auto-unpause: %s \n " , unpause_enabled ? " on. " : " off. " ) ;
out . print ( " auto-disengage: %s \n " , disengage_enabled ? " on. " : " off. " ) ;
2022-06-05 10:06:55 -06:00
}
2022-09-03 18:02:57 -06:00
saveConfig ( ) ;
2022-08-24 12:45:16 -06:00
return DFHack : : CR_OK ;
2022-03-14 20:33:41 -06:00
}
2022-06-05 10:06:55 -06:00
// every tick check whether to decide to follow a dwarf
2022-03-14 20:33:41 -06:00
void onTick ( color_ostream & out , void * ptr ) {
2022-09-03 18:02:57 -06:00
if ( ! df : : global : : ui ) return ;
2022-03-14 20:33:41 -06:00
int32_t tick = df : : global : : world - > frame_counter ;
2022-06-05 10:06:55 -06:00
if ( our_dorf ) {
if ( ! Units : : isAlive ( our_dorf ) ) {
following_dwarf = false ;
df : : global : : ui - > follow_unit = - 1 ;
}
}
2022-09-03 18:02:57 -06:00
if ( ! following_dwarf | | ( focus_jobs_enabled & & ! job_watched ) | | ( tick - timestamp ) > ( int32_t ) tick_threshold ) {
2022-03-14 20:33:41 -06:00
std : : vector < df : : unit * > dwarves ;
for ( auto unit : df : : global : : world - > units . active ) {
if ( ! Units : : isCitizen ( unit ) ) {
continue ;
}
dwarves . push_back ( unit ) ;
}
std : : uniform_int_distribution < uint64_t > follow_any ( 0 , dwarves . size ( ) - 1 ) ;
if ( df : : global : : ui ) {
2022-09-03 12:18:46 -06:00
// if you're looking at a warning about a local address escaping, it means the unit* from dwarves (which aren't local)
2022-06-05 10:06:55 -06:00
our_dorf = dwarves [ follow_any ( RNG ) ] ;
df : : global : : ui - > follow_unit = our_dorf - > id ;
job_watched = our_dorf - > job . current_job ;
2022-03-14 20:33:41 -06:00
following_dwarf = true ;
if ( ! job_watched ) {
timestamp = tick ;
}
}
}
}
2022-06-05 10:06:55 -06:00
// every new worked job needs to be considered
2022-03-14 20:33:41 -06:00
void onJobStart ( color_ostream & out , void * job_ptr ) {
2022-06-05 10:06:55 -06:00
// todo: detect mood jobs
2022-03-14 20:33:41 -06:00
int32_t tick = df : : global : : world - > frame_counter ;
auto job = ( df : : job * ) job_ptr ;
2022-06-05 10:06:55 -06:00
// don't forget about it
2022-03-14 20:33:41 -06:00
int zcount = + + freq [ job - > pos . z ] ;
job_tracker . emplace ( job - > id ) ;
2022-06-05 10:06:55 -06:00
// if we're not doing anything~ then let's pick something
2022-09-03 18:02:57 -06:00
if ( ( focus_jobs_enabled & & ! job_watched ) | | ( tick - timestamp ) > ( int32_t ) tick_threshold ) {
2022-03-14 20:33:41 -06:00
following_dwarf = true ;
2022-06-05 10:06:55 -06:00
// todo: allow the user to configure b, and also revise the math
const double b = base ;
double p = b * ( ( double ) zcount / job_tracker . size ( ) ) ;
2022-03-14 20:33:41 -06:00
std : : bernoulli_distribution follow_job ( p ) ;
if ( ! job - > flags . bits . special & & follow_job ( RNG ) ) {
2022-09-03 12:18:46 -06:00
job_watched = job ;
2022-03-14 20:33:41 -06:00
df : : unit * unit = Job : : getWorker ( job ) ;
if ( df : : global : : ui & & unit ) {
2022-06-05 10:06:55 -06:00
our_dorf = unit ;
2022-03-14 20:33:41 -06:00
df : : global : : ui - > follow_unit = unit - > id ;
}
} else {
timestamp = tick ;
std : : vector < df : : unit * > nonworkers ;
for ( auto unit : df : : global : : world - > units . active ) {
if ( ! Units : : isCitizen ( unit ) | | unit - > job . current_job ) {
continue ;
}
nonworkers . push_back ( unit ) ;
}
std : : uniform_int_distribution < > follow_drunk ( 0 , nonworkers . size ( ) - 1 ) ;
if ( df : : global : : ui ) {
df : : global : : ui - > follow_unit = nonworkers [ follow_drunk ( RNG ) ] - > id ;
}
}
}
}
2022-06-05 10:06:55 -06:00
// every job completed can be forgotten about
2022-03-14 20:33:41 -06:00
void onJobCompletion ( color_ostream & out , void * job_ptr ) {
auto job = ( df : : job * ) job_ptr ;
2022-06-05 10:06:55 -06:00
// forget about it
2022-03-14 20:33:41 -06:00
freq [ job - > pos . z ] - - ;
freq [ job - > pos . z ] = freq [ job - > pos . z ] < 0 ? 0 : freq [ job - > pos . z ] ;
2022-06-05 10:06:55 -06:00
// the job doesn't exist, so we definitely need to get rid of that
2022-09-03 12:18:46 -06:00
job_tracker . erase ( job - > id ) ;
// the event manager clones jobs and returns those clones for completed jobs. So the pointers won't match without a refactor of EM passing clones to both events
2022-09-03 18:58:40 -06:00
if ( job_watched & & job_watched - > id = = job - > id ) {
2022-03-14 20:33:41 -06:00
job_watched = nullptr ;
}
}