2022-03-14 20:33:41 -06:00
//
// Created by josh on 7/28/21.
2022-11-17 12:17:13 -07:00
// Last updated: 11//10/22
2022-03-14 20:33:41 -06:00
//
2022-10-12 15:13:09 -06:00
# include "pause.h"
2022-11-08 12:42:12 -07:00
# include <Debug.h>
# include <Core.h>
2022-03-14 20:33:41 -06:00
# include <Export.h>
# include <PluginManager.h>
2022-11-08 12:42:12 -07:00
2022-03-14 20:33:41 -06:00
# include <modules/EventManager.h>
2022-11-09 16:49:24 -07:00
# include <modules/World.h>
# include <modules/Maps.h>
# include <modules/Gui.h>
2022-03-14 20:33:41 -06:00
# 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-11-09 16:49:24 -07:00
# include <df/creature_raw.h>
2022-03-14 20:33:41 -06:00
2022-11-09 22:10:18 -07:00
# include <array>
2022-03-14 20:33:41 -06:00
# include <random>
2022-09-04 20:32:42 -06:00
# include <cinttypes>
2022-11-09 16:49:24 -07:00
# include <functional>
2022-03-14 20:33:41 -06:00
2022-11-08 12:42:12 -07:00
// Debugging
namespace DFHack {
DBG_DECLARE ( spectate , plugin , DebugCategory : : LINFO ) ;
}
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 ;
2022-10-12 15:13:09 -06:00
using namespace Pausing ;
2022-03-14 20:33:41 -06:00
using namespace df : : enums ;
2022-11-08 12:42:12 -07:00
struct Configuration {
bool unpause = false ;
bool disengage = false ;
2022-11-09 16:49:24 -07:00
bool animals = false ;
bool hostiles = true ;
bool visitors = false ;
2022-11-08 12:42:12 -07:00
int32_t tick_threshold = 1000 ;
} config ;
2022-09-03 18:02:57 -06:00
2022-09-03 12:18:46 -06:00
Pausing : : AnnouncementLock * pause_lock = nullptr ;
bool lock_collision = false ;
2022-10-14 16:43:39 -06:00
bool announcements_disabled = false ;
2022-09-03 18:02:57 -06:00
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 ,
2022-11-09 16:49:24 -07:00
TICK_THRESHOLD ,
ANIMALS ,
HOSTILES ,
VISITORS
2022-09-03 18:02:57 -06:00
} ;
2022-11-08 12:42:12 -07:00
static PersistentDataItem pconfig ;
2022-09-03 18:02:57 -06:00
2022-11-08 12:42:12 -07:00
DFhackCExport command_result plugin_enable ( color_ostream & out , bool enable ) ;
2022-03-14 20:33:41 -06:00
command_result spectate ( color_ostream & out , std : : vector < std : : string > & parameters ) ;
2022-11-09 22:10:18 -07:00
# define COORDARGS(id) id.x, id.y, id.z
2022-03-14 20:33:41 -06:00
2022-11-08 12:42:12 -07:00
namespace SP {
2022-11-09 16:49:24 -07:00
bool following_dwarf = false ;
df : : unit * our_dorf = nullptr ;
int32_t timestamp = - 1 ;
std : : default_random_engine RNG ;
2022-11-08 12:42:12 -07:00
2022-11-17 12:17:13 -07:00
void DebugUnitVector ( std : : vector < df : : unit * > units ) {
2022-11-21 13:13:11 -07:00
if ( debug_plugin . isEnabled ( DFHack : : DebugCategory : : LDEBUG ) ) {
for ( auto unit : units ) {
DEBUG ( plugin ) . print ( " [id: %d] \n animal: %d \n hostile: %d \n visiting: %d \n " ,
unit - > id ,
Units : : isAnimal ( unit ) ,
Units : : isDanger ( unit ) ,
Units : : isVisiting ( unit ) ) ;
}
2022-11-17 12:17:13 -07:00
}
}
2022-11-08 12:42:12 -07:00
void PrintStatus ( color_ostream & out ) {
out . print ( " Spectate is %s \n " , enabled ? " ENABLED. " : " DISABLED. " ) ;
out . print ( " FEATURES: \n " ) ;
out . print ( " %-20s \t %s \n " , " auto-unpause: " , config . unpause ? " on. " : " off. " ) ;
out . print ( " %-20s \t %s \n " , " auto-disengage: " , config . disengage ? " on. " : " off. " ) ;
2022-11-09 16:49:24 -07:00
out . print ( " %-20s \t %s \n " , " animals: " , config . animals ? " on. " : " off. " ) ;
out . print ( " %-20s \t %s \n " , " hostiles: " , config . hostiles ? " on. " : " off. " ) ;
out . print ( " %-20s \t %s \n " , " visiting: " , config . visitors ? " on. " : " off. " ) ;
2022-11-08 12:42:12 -07:00
out . print ( " SETTINGS: \n " ) ;
out . print ( " %-20s \t % " PRIi32 " \n " , " tick-threshold: " , config . tick_threshold ) ;
2022-11-09 16:49:24 -07:00
if ( following_dwarf )
out . print ( " %-21s \t %s[id: %d] \n " , " FOLLOWING: " , our_dorf ? our_dorf - > name . first_name . c_str ( ) : " nullptr " , df : : global : : ui - > follow_unit ) ;
2022-11-08 12:42:12 -07:00
}
void SetUnpauseState ( bool state ) {
// we don't need to do any of this yet if the plugin isn't enabled
if ( enabled ) {
// todo: R.E. UNDEAD_ATTACK event [still pausing regardless of announcement settings]
// lock_collision == true means: enable_auto_unpause() was already invoked and didn't complete
// The onupdate function above ensure the procedure properly completes, thus we only care about
// state reversal here ergo `enabled != state`
if ( lock_collision & & config . unpause ! = state ) {
WARN ( plugin ) . print ( " Spectate auto-unpause: Not enabled yet, there was a lock collision. When the other lock holder releases, auto-unpause will engage on its own. \n " ) ;
// if unpaused_enabled is true, then a lock collision means: we couldn't save/disable the pause settings,
// therefore nothing to revert and the lock won't even be engaged (nothing to unlock)
lock_collision = false ;
config . unpause = state ;
if ( config . unpause ) {
// a collision means we couldn't restore the pause settings, therefore we only need re-engage the lock
pause_lock - > lock ( ) ;
}
return ;
}
// update the announcement settings if we can
if ( state ) {
if ( World : : SaveAnnouncementSettings ( ) ) {
World : : DisableAnnouncementPausing ( ) ;
announcements_disabled = true ;
pause_lock - > lock ( ) ;
} else {
WARN ( plugin ) . print ( " Spectate auto-unpause: Could not fully enable. There was a lock collision, when the other lock holder releases, auto-unpause will engage on its own. \n " ) ;
lock_collision = true ;
}
} else {
pause_lock - > unlock ( ) ;
if ( announcements_disabled ) {
if ( ! World : : RestoreAnnouncementSettings ( ) ) {
// this in theory shouldn't happen, if others use the lock like we do in spectate
WARN ( plugin ) . print ( " Spectate auto-unpause: Could not fully disable. There was a lock collision, when the other lock holder releases, auto-unpause will disengage on its own. \n " ) ;
lock_collision = true ;
} else {
announcements_disabled = false ;
}
}
}
if ( lock_collision ) {
ERR ( plugin ) . print ( " Spectate auto-unpause: Could not fully enable. There was a lock collision, when the other lock holder releases, auto-unpause will engage on its own. \n " ) ;
WARN ( plugin ) . print (
2022-11-09 16:49:24 -07:00
" auto-unpause: must wait for another Pausing::AnnouncementLock to be lifted. \n "
" The action you were attempting will complete when the following lock or locks lift. \n " ) ;
2022-11-08 12:42:12 -07:00
pause_lock - > reportLocks ( Core : : getInstance ( ) . getConsole ( ) ) ;
}
}
config . unpause = state ;
}
void SaveSettings ( ) {
if ( pconfig . isValid ( ) ) {
pconfig . ival ( UNPAUSE ) = config . unpause ;
pconfig . ival ( DISENGAGE ) = config . disengage ;
pconfig . ival ( TICK_THRESHOLD ) = config . tick_threshold ;
2022-11-09 16:49:24 -07:00
pconfig . ival ( ANIMALS ) = config . animals ;
pconfig . ival ( HOSTILES ) = config . hostiles ;
pconfig . ival ( VISITORS ) = config . visitors ;
2022-11-08 12:42:12 -07:00
}
}
void LoadSettings ( ) {
pconfig = World : : GetPersistentData ( CONFIG_KEY ) ;
if ( ! pconfig . isValid ( ) ) {
pconfig = World : : AddPersistentData ( CONFIG_KEY ) ;
SaveSettings ( ) ;
} else {
config . unpause = pconfig . ival ( UNPAUSE ) ;
config . disengage = pconfig . ival ( DISENGAGE ) ;
config . tick_threshold = pconfig . ival ( TICK_THRESHOLD ) ;
2022-11-09 16:49:24 -07:00
config . animals = pconfig . ival ( ANIMALS ) ;
config . hostiles = pconfig . ival ( HOSTILES ) ;
config . visitors = pconfig . ival ( VISITORS ) ;
2022-11-08 12:42:12 -07:00
pause_lock - > unlock ( ) ;
SetUnpauseState ( config . unpause ) ;
}
}
2022-11-09 16:49:24 -07:00
bool FollowADwarf ( ) {
if ( enabled & & ! World : : ReadPauseState ( ) ) {
df : : coord viewMin = Gui : : getViewportPos ( ) ;
df : : coord viewMax { viewMin } ;
const auto & dims = Gui : : getDwarfmodeViewDims ( ) . map ( ) . second ;
viewMax . x + = dims . x - 1 ;
viewMax . y + = dims . y - 1 ;
viewMax . z = viewMin . z ;
std : : vector < df : : unit * > units ;
static auto add_if = [ & ] ( std : : function < bool ( df : : unit * ) > check ) {
for ( auto unit : world - > units . active ) {
if ( check ( unit ) ) {
units . push_back ( unit ) ;
}
}
} ;
static auto valid = [ ] ( df : : unit * unit ) {
if ( Units : : isAnimal ( unit ) ) {
return config . animals ;
}
if ( Units : : isVisiting ( unit ) ) {
return config . visitors ;
}
if ( Units : : isDanger ( unit ) ) {
return config . hostiles ;
}
return true ;
} ;
2022-11-09 22:10:18 -07:00
static auto calc_extra_weight = [ ] ( size_t idx , double r1 , double r2 ) {
switch ( idx ) {
case 0 :
return r2 ;
case 1 :
return ( r2 - r1 ) / 1.3 ;
case 2 :
return ( r2 - r1 ) / 2 ;
default :
return 0.0 ;
}
} ;
/// Collecting our choice pool
///////////////////////////////
std : : array < int32_t , 10 > ranges { } ;
std : : array < bool , 5 > range_exists { } ;
static auto build_range = [ & ] ( size_t idx ) {
size_t first = idx * 2 ;
size_t second = idx * 2 + 1 ;
size_t previous = first - 1 ;
// first we get the end of the range
ranges [ second ] = units . size ( ) - 1 ;
2022-11-17 12:17:13 -07:00
// then we calculate whether the range exists, and set the first index appropriately
if ( idx = = 0 ) {
2022-11-09 22:10:18 -07:00
range_exists [ idx ] = ranges [ second ] > = 0 ;
2022-11-17 12:17:13 -07:00
ranges [ first ] = 0 ;
} else {
range_exists [ idx ] = ranges [ second ] > ranges [ previous ] ;
ranges [ first ] = ranges [ previous ] + ( range_exists [ idx ] ? 1 : 0 ) ;
2022-11-09 22:10:18 -07:00
}
} ;
/// RANGE 0 (in view + working)
// grab valid working units
2022-11-17 12:17:13 -07:00
add_if ( [ & ] ( df : : unit * unit ) {
return valid ( unit ) & &
Units : : isUnitInBox ( unit , COORDARGS ( viewMin ) , COORDARGS ( viewMax ) ) & &
Units : : isCitizen ( unit , true ) & &
unit - > job . current_job ;
2022-11-09 22:10:18 -07:00
} ) ;
build_range ( 0 ) ;
2022-11-09 16:49:24 -07:00
/// RANGE 1 (in view)
2022-11-17 12:17:13 -07:00
add_if ( [ & ] ( df : : unit * unit ) {
return valid ( unit ) & & Units : : isUnitInBox ( unit , COORDARGS ( viewMin ) , COORDARGS ( viewMax ) ) ;
} ) ;
2022-11-09 22:10:18 -07:00
build_range ( 1 ) ;
2022-11-09 16:49:24 -07:00
2022-11-09 22:10:18 -07:00
/// RANGE 2 (working citizens)
add_if ( [ ] ( df : : unit * unit ) {
return valid ( unit ) & & Units : : isCitizen ( unit , true ) & & unit - > job . current_job ;
} ) ;
build_range ( 2 ) ;
/// RANGE 3 (citizens)
2022-11-09 16:49:24 -07:00
add_if ( [ ] ( df : : unit * unit ) {
return valid ( unit ) & & Units : : isCitizen ( unit , true ) ;
} ) ;
2022-11-09 22:10:18 -07:00
build_range ( 3 ) ;
2022-11-09 16:49:24 -07:00
2022-11-09 22:10:18 -07:00
/// RANGE 4 (any valid)
2022-11-09 16:49:24 -07:00
add_if ( valid ) ;
2022-11-09 22:10:18 -07:00
build_range ( 4 ) ;
2022-11-09 16:49:24 -07:00
2022-11-09 22:10:18 -07:00
// selecting from our choice pool
2022-11-09 16:49:24 -07:00
if ( ! units . empty ( ) ) {
2022-11-09 22:10:18 -07:00
std : : array < double , 5 > bw { 23 , 17 , 13 , 7 , 1 } ; // probability weights for each range
2022-11-09 16:49:24 -07:00
std : : vector < double > i ;
std : : vector < double > w ;
2022-11-09 22:10:18 -07:00
bool at_least_one = false ;
// in one word, elegance
for ( size_t idx = 0 ; idx < range_exists . size ( ) ; + + idx ) {
if ( range_exists [ idx ] ) {
at_least_one = true ;
const auto & r1 = ranges [ idx * 2 ] ;
const auto & r2 = ranges [ idx * 2 + 1 ] ;
double extra = calc_extra_weight ( idx , r1 , r2 ) ;
i . push_back ( r1 ) ;
w . push_back ( bw [ idx ] + extra ) ;
if ( r1 ! = r2 ) {
i . push_back ( r2 ) ;
w . push_back ( bw [ idx ] + extra ) ;
}
2022-11-09 16:49:24 -07:00
}
}
2022-11-09 22:10:18 -07:00
if ( ! at_least_one ) {
return false ;
2022-11-09 16:49:24 -07:00
}
2022-11-21 13:13:11 -07:00
DebugUnitVector ( units ) ;
2022-11-09 16:49:24 -07:00
std : : piecewise_linear_distribution < > follow_any ( i . begin ( ) , i . end ( ) , w . begin ( ) ) ;
// if you're looking at a warning about a local address escaping, it means the unit* from units (which aren't local)
size_t idx = follow_any ( RNG ) ;
our_dorf = units [ idx ] ;
df : : global : : ui - > follow_unit = our_dorf - > id ;
timestamp = df : : global : : world - > frame_counter ;
return true ;
} else {
WARN ( plugin ) . print ( " units vector is empty! \n " ) ;
}
}
return false ;
2022-11-08 12:42:12 -07:00
}
void onUpdate ( color_ostream & out ) {
2022-11-09 16:49:24 -07:00
if ( ! World : : isFortressMode ( ) | | ! Maps : : IsValid ( ) )
return ;
2022-11-08 12:42:12 -07:00
// keeps announcement pause settings locked
World : : Update ( ) ; // from pause.h
2022-11-09 16:49:24 -07:00
// Plugin Management
2022-11-08 12:42:12 -07:00
if ( lock_collision ) {
if ( config . unpause ) {
// 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 ;
}
} else {
if ( World : : RestoreAnnouncementSettings ( ) ) {
lock_collision = false ;
}
}
}
int failsafe = 0 ;
while ( config . unpause & & ! world - > status . popups . empty ( ) & & + + failsafe < = 10 ) {
// dismiss announcement popup(s)
Gui : : getCurViewscreen ( true ) - > feed_key ( interface_key : : CLOSE_MEGA_ANNOUNCEMENT ) ;
if ( World : : ReadPauseState ( ) ) {
// WARNING: This has a possibility of conflicting with `reveal hell` - if Hermes himself runs `reveal hell` on precisely the right moment that is
World : : SetPauseState ( false ) ;
}
}
if ( failsafe > = 10 ) {
out . printerr ( " spectate encountered a problem dismissing a popup! \n " ) ;
}
2022-11-09 16:49:24 -07:00
// plugin logic
static int32_t last_tick = - 1 ;
int32_t tick = world - > frame_counter ;
if ( ! World : : ReadPauseState ( ) & & tick - last_tick > = 1 ) {
last_tick = tick ;
// validate follow state
2022-11-09 22:10:18 -07:00
if ( ! following_dwarf | | ! our_dorf | | df : : global : : ui - > follow_unit < 0 | | tick - timestamp > = config . tick_threshold ) {
2022-11-09 16:49:24 -07:00
// we're not following anyone
2022-11-08 12:42:12 -07:00
following_dwarf = false ;
2022-11-09 16:49:24 -07:00
if ( ! config . disengage ) {
// try to
following_dwarf = FollowADwarf ( ) ;
} else if ( ! World : : ReadPauseState ( ) ) {
plugin_enable ( out , false ) ;
2022-11-08 12:42:12 -07:00
}
}
}
}
} ;
2022-03-14 20:33:41 -06:00
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-10-12 15:13:09 -06:00
pause_lock = new AnnouncementLock ( " spectate " ) ;
2022-03-14 20:33:41 -06:00
return CR_OK ;
}
DFhackCExport command_result plugin_shutdown ( color_ostream & out ) {
2022-10-12 15:13:09 -06:00
delete 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 ) {
2022-11-08 12:42:12 -07:00
SP : : LoadSettings ( ) ;
2022-11-30 18:31:55 -07:00
if ( enabled ) {
SP : : following_dwarf = SP : : FollowADwarf ( ) ;
SP : : PrintStatus ( out ) ;
}
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 ) {
if ( enable & & ! enabled ) {
out . print ( " Spectate mode enabled! \n " ) ;
2022-10-14 16:43:39 -06:00
enabled = true ; // enable_auto_unpause won't do anything without this set now
2022-11-08 12:42:12 -07:00
SP : : SetUnpauseState ( config . unpause ) ;
2022-03-14 20:33:41 -06:00
} 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 " ) ;
2022-10-14 16:43:39 -06:00
// we need to retain whether auto-unpause is enabled, but we also need to disable its effect
2022-11-08 12:42:12 -07:00
bool temp = config . unpause ;
SP : : SetUnpauseState ( false ) ;
config . unpause = temp ;
2022-03-14 20:33:41 -06:00
}
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 ) {
2022-10-14 16:43:39 -06:00
if ( enabled ) {
2022-09-03 18:02:57 -06:00
switch ( event ) {
case SC_MAP_UNLOADED :
case SC_BEGIN_UNLOAD :
case SC_WORLD_UNLOADED :
2022-11-09 16:49:24 -07:00
SP : : our_dorf = nullptr ;
SP : : following_dwarf = false ;
2022-09-03 18:02:57 -06:00
default :
break ;
}
}
return CR_OK ;
}
2022-09-03 12:18:46 -06:00
DFhackCExport command_result plugin_onupdate ( color_ostream & out ) {
2022-11-08 12:42:12 -07:00
SP : : onUpdate ( out ) ;
2022-09-03 12:18:46 -06:00
return DFHack : : CR_OK ;
}
2022-03-14 20:33:41 -06:00
command_result spectate ( color_ostream & out , std : : vector < std : : string > & parameters ) {
2022-10-12 12:21:59 -06:00
if ( ! parameters . empty ( ) ) {
2022-10-14 18:02:43 -06:00
if ( parameters . size ( ) > = 2 & & parameters . size ( ) < = 3 ) {
2022-10-14 19:23:48 -06:00
bool state = false ;
2022-10-14 18:02:43 -06:00
bool set = false ;
if ( parameters [ 0 ] = = " enable " ) {
state = true ;
} else if ( parameters [ 0 ] = = " disable " ) {
state = false ;
} else if ( parameters [ 0 ] = = " set " ) {
set = true ;
} else {
return DFHack : : CR_WRONG_USAGE ;
}
2022-10-14 19:23:48 -06:00
if ( parameters [ 1 ] = = " auto-unpause " ) {
2022-11-08 12:42:12 -07:00
SP : : SetUnpauseState ( state ) ;
2022-10-14 19:23:48 -06:00
} else if ( parameters [ 1 ] = = " auto-disengage " ) {
2022-11-08 12:42:12 -07:00
config . disengage = state ;
2022-11-09 16:49:24 -07:00
} else if ( parameters [ 1 ] = = " animals " ) {
config . animals = state ;
} else if ( parameters [ 1 ] = = " hostiles " ) {
config . hostiles = state ;
} else if ( parameters [ 1 ] = = " visiting " ) {
config . visitors = state ;
2022-10-14 19:23:48 -06:00
} else if ( parameters [ 1 ] = = " tick-threshold " & & set & & parameters . size ( ) = = 3 ) {
2022-09-03 12:18:46 -06:00
try {
2022-11-08 12:42:12 -07:00
config . tick_threshold = std : : abs ( std : : stol ( parameters [ 2 ] ) ) ;
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-11-08 12:42:12 -07:00
SP : : PrintStatus ( out ) ;
2022-06-05 10:06:55 -06:00
}
2022-11-08 12:42:12 -07:00
SP : : SaveSettings ( ) ;
2022-08-24 12:45:16 -06:00
return DFHack : : CR_OK ;
2022-03-14 20:33:41 -06:00
}