Skip to content

Commit

Permalink
Unique npc ids, allow interacting with npcs that are not in reality b…
Browse files Browse the repository at this point in the history
…ubble (#57533)

* First version, prevents duplicate placing and no more.

* Add location handling

* Fix

* Update src/npctalk.cpp

Co-authored-by: Binrui Dong <brett.browning.dong@gmail.com>

* Fixes

* Update npctalk.cpp

Co-authored-by: Binrui Dong <brett.browning.dong@gmail.com>
  • Loading branch information
Ramza13 and BrettDong committed May 9, 2022
1 parent e3e4e28 commit dc48ac2
Show file tree
Hide file tree
Showing 15 changed files with 133 additions and 24 deletions.
1 change: 1 addition & 0 deletions doc/MAPGEN.md
Original file line number Diff line number Diff line change
Expand Up @@ -759,6 +759,7 @@ Example:
| class | (required, string) the npc class id, see `data/json/npcs/npc.json` or define your own npc class.
| target | (optional, bool) this NPC is a mission target. Only valid for `update_mapgen`.
| add_trait | (optional, string or string array) this NPC gets these traits, in addition to any from the class definition.
| unique_id | (optional, string) This is a unique id to descibe the npc, if an npc with this id already exists the command will silently fail.


### Place signs with "signs"
Expand Down
2 changes: 1 addition & 1 deletion doc/NPCs.md
Original file line number Diff line number Diff line change
Expand Up @@ -754,7 +754,7 @@ Effect | Description
`offer_mission" mission_type_id string or array`. Adds mission_type_id(s) to the npc's missions that they offer.
`run_eocs : effect_on_condition_array or single effect_condition_object` | Will run up all members of the `effect_on_condition_array`. Members should either be the id of an effect_on_condition or an inline effect_on_condition.
`queue_eocs : effect_on_condition_array or single effect_condition_object`, `time_in_future: time_in_future_int or string or variable_object` | Will queue up all members of the `effect_on_condition_array`. Members should either be the id of an effect_on_condition or an inline effect_on_condition. Members will be run `time_in_future` seconds or if it is a string the future values of them or if they are variable objects the variable they name. If the eoc is global the avatar will be u and npc will be invalid. Otherwise it will be queued for the current alpha if they are a character and not be queued otherwise.
`u_run_npc_eocs or npc_run_npc_eocs : effect_on_condition_array`, (*optional* `npcs_to_affect: npcs_to_affect_string_array`, (*optional* `npcs_must_see: npcs_must_see_bool`), (*optional* `npc_range: npc_range_int`) | Will run all members of the `effect_on_condition_array` on nearby npcs. Members should either be the id of an effect_on_condition or an inline effect_on_condition. If any names are listed in `npcs_to_affect` then only they will be affected. If a value is given for `npc_range` the npc must be that close to the source and if `npcs_must_see`(defaults to false) is true the npc must be able to see the source. For `u_run_npc_eocs` u is the source for `npc_run_npc_eocs` it is the npc.
`u_run_npc_eocs or npc_run_npc_eocs : effect_on_condition_array`, (*optional* `unique_ids: unique_ids_string_array`), (*optional* `npcs_must_see: npcs_must_see_bool`), (*optional* `npc_range: npc_range_int`), (*optional* `local: local_bool`) | Will run all members of the `effect_on_condition_array` on npcs. Members should either be the id of an effect_on_condition or an inline effect_on_condition. If `local`(default: false) is false, then regardless of location all npcs with unique ids in the array `unique_ids` will be affected. If `local` is true, only unique_ids listed in `unique_ids` will be affected, if it is empty all npcs in range will be effected. If a value is given for `npc_range` the npc must be that close to the source and if `npcs_must_see`(defaults to false) is true the npc must be able to see the source. For `u_run_npc_eocs` u is the source for `npc_run_npc_eocs` it is the npc.
`weighted_list_eocs: array_array` | Will choose one of a list of eocs to activate based on weight. Members should be an array of first the id of an effect_on_condition or an inline effect_on_condition and second an object that resolves to an integer weight.
Example: This will cause "EOC_SLEEP" 1/10 as often as it makes a test message appear.
``` json
Expand Down
3 changes: 3 additions & 0 deletions src/debug_menu.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1621,6 +1621,9 @@ static void character_edit_menu()
std::stringstream data;
data << np->get_name() << " - " << ( np->male ? _( "Male" ) : _( "Female" ) ) << " " <<
np->myclass->get_name() << std::endl;
if( !np->get_unique_id().empty() ) {
data << string_format( _( "Unique Id: %s" ), np->get_unique_id() ) << std::endl;
}
data << string_format( _( "Faction: %s (api v%d)" ), np->get_faction()->id.str(),
np->get_faction_ver() ) << "; "
<< string_format( _( "Attitude: %s" ), npc_attitude_name( np->get_attitude() ) ) << std::endl;
Expand Down
26 changes: 26 additions & 0 deletions src/game.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -734,6 +734,7 @@ void game::setup()
remoteveh_cache = nullptr;
global_variables &globvars = get_globals();
globvars.clear_global_values();
unique_npcs.clear();
get_weather().weather_override = WEATHER_NULL;
// back to menu for save loading, new game etc
}
Expand Down Expand Up @@ -1533,6 +1534,11 @@ npc *game::find_npc( character_id id )
return overmap_buffer.find_npc( id ).get();
}

npc *game::find_npc_by_unique_id( std::string unique_id )
{
return overmap_buffer.find_npc_by_unique_id( unique_id ).get();
}

void game::add_npc_follower( const character_id &id )
{
follower_ids.insert( id );
Expand Down Expand Up @@ -2906,6 +2912,26 @@ memorial_logger &game::memorial()
return *memorial_logger_ptr;
}

void game::update_unique_npc_location( std::string id, point_abs_om loc )
{
unique_npcs[id] = loc;
}

point_abs_om game::get_unique_npc_location( std::string id )
{
if( unique_npc_exists( id ) ) {
return unique_npcs[id];
} else {
debugmsg( "Tried to find npc %s which doesn't exist.", id );
return point_abs_om();
}
}

bool game::unique_npc_exists( std::string id )
{
return unique_npcs.count( id ) > 0;
}

spell_events &game::spell_events_subscriber()
{
return *spell_events_ptr;
Expand Down
6 changes: 6 additions & 0 deletions src/game.h
Original file line number Diff line number Diff line change
Expand Up @@ -497,6 +497,8 @@ class game
int assign_mission_id();
/** Find the npc with the given ID. Returns NULL if the npc could not be found. Searches all loaded overmaps. */
npc *find_npc( character_id id );
/** Find the npc with the given unique ID. Returns NULL if the npc could not be found. Searches all loaded overmaps. */
npc *find_npc_by_unique_id( std::string unique_id );
/** Makes any nearby NPCs on the overmap active. */
void load_npcs();
private:
Expand Down Expand Up @@ -1018,7 +1020,11 @@ class game
memorial_logger &memorial();

global_variables global_variables_instance;
std::unordered_map<std::string, point_abs_om> unique_npcs;
public:
void update_unique_npc_location( std::string id, point_abs_om loc );
point_abs_om get_unique_npc_location( std::string id );
bool unique_npc_exists( std::string id );
std::vector<effect_on_condition_id> inactive_global_effect_on_condition_vector;
std::priority_queue<queued_eoc, std::vector<queued_eoc>, eoc_compare>
queued_global_effect_on_conditions;
Expand Down
12 changes: 12 additions & 0 deletions src/mapgen.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1740,6 +1740,7 @@ class jmapgen_npc : public jmapgen_piece
mapgen_value<string_id<npc_template>> npc_class;
bool target;
std::vector<trait_id> traits;
std::string unique_id;
jmapgen_npc( const JsonObject &jsi, const std::string &/*context*/ ) :
npc_class( jsi.get_member( "class" ) )
, target( jsi.get_bool( "target", false ) ) {
Expand All @@ -1750,13 +1751,21 @@ class jmapgen_npc : public jmapgen_piece
} else if( jsi.has_array( "add_trait" ) ) {
jsi.read( "add_trait", traits );
}
if( jsi.has_string( "unique_id" ) ) {
jsi.read( "unique_id", unique_id );
}
}
void apply( const mapgendata &dat, const jmapgen_int &x, const jmapgen_int &y,
const std::string &/*context*/ ) const override {
string_id<npc_template> chosen_id = npc_class.get( dat );
if( chosen_id.is_null() ) {
return;
}
if( !unique_id.empty() && g->unique_npc_exists( unique_id ) ) {
get_avatar().add_msg_debug_if_player( debugmode::DF_NPC, "NPC with unique id %s already exists.",
unique_id );
return;
}
character_id npc_id = dat.m.place_npc( point( x.get(), y.get() ), chosen_id );
if( dat.mission() && target ) {
dat.mission()->set_target_npc_id( npc_id );
Expand All @@ -1766,6 +1775,9 @@ class jmapgen_npc : public jmapgen_piece
for( const trait_id &new_trait : traits ) {
p->set_mutation( new_trait );
}
if( !unique_id.empty() ) {
p->set_unique_id( unique_id );
}
}
}

Expand Down
18 changes: 18 additions & 0 deletions src/npc.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1081,6 +1081,9 @@ void npc::on_move( const tripoint_abs_ms &old_pos )
if( !is_fake() && pos_om_old != pos_om_new ) {
overmap &om_old = overmap_buffer.get( pos_om_old );
overmap &om_new = overmap_buffer.get( pos_om_new );
if( !unique_id.empty() ) {
g->update_unique_npc_location( unique_id, pos_om_new );
}
if( const auto ptr = om_old.erase_npc( getID() ) ) {
om_new.insert_npc( ptr );
} else {
Expand Down Expand Up @@ -3539,6 +3542,21 @@ attitude_group npc::get_attitude_group( npc_attitude att ) const
return attitude_group::neutral;
}

void npc::set_unique_id( std::string id )
{
if( !unique_id.empty() ) {
debugmsg( "Tried to set unique_id of npc with one already of value: ", unique_id );
} else {
unique_id = id;
g->update_unique_npc_location( id, project_to<coords::om>( get_location().xy() ) );
}
}

std::string npc::get_unique_id() const
{
return unique_id;
}

void npc::set_mission( npc_mission new_mission )
{
if( new_mission != mission ) {
Expand Down
5 changes: 4 additions & 1 deletion src/npc.h
Original file line number Diff line number Diff line change
Expand Up @@ -1405,7 +1405,8 @@ class npc : public Character
bool has_companion_mission() const;
npc_companion_mission get_companion_mission() const;
attitude_group get_attitude_group( npc_attitude att ) const;

void set_unique_id( std::string id );
std::string get_unique_id() const;
protected:
void store( JsonOut &json ) const;
void load( const JsonObject &data );
Expand All @@ -1425,6 +1426,8 @@ class npc : public Character
std::vector<sphere> find_dangerous_explosives() const;

npc_companion_mission comp_mission;

std::string unique_id;
};

/** An NPC with standard stats */
Expand Down
60 changes: 40 additions & 20 deletions src/npctalk.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3729,34 +3729,54 @@ void talk_effect_fun_t::set_run_npc_eocs( const JsonObject &jo,
{
std::vector<effect_on_condition_id> eocs = load_eoc_vector( jo, member );

std::vector<std::string> names = jo.get_string_array( "npcs_to_affect" );
std::vector<std::string> unique_ids = jo.get_string_array( "unique_ids" );

bool local = jo.get_bool( "local", false );
cata::optional<int> npc_range;
if( jo.has_int( "npc_range" ) ) {
npc_range = jo.get_int( "npc_range" );
}
bool npc_must_see = jo.get_bool( "npc_must_see", false );
function = [eocs, names, npc_must_see, npc_range, is_npc]( const dialogue & d ) {
tripoint actor_pos = d.actor( is_npc )->pos();
const std::vector<npc *> available = g->get_npcs_if( [npc_must_see, npc_range, actor_pos,
names]( const npc & guy ) {
bool name_valid = names.empty();
for( const std::string &name : names ) {
if( name == guy.name ) {
name_valid = true;
break;
if( local ) {
function = [eocs, unique_ids, npc_must_see, npc_range, is_npc]( const dialogue & d ) {
tripoint actor_pos = d.actor( is_npc )->pos();
const std::vector<npc *> available = g->get_npcs_if( [npc_must_see, npc_range, actor_pos,
unique_ids]( const npc & guy ) {
bool id_valid = unique_ids.empty();
for( const std::string &id : unique_ids ) {
if( id == guy.get_unique_id() ) {
id_valid = true;
break;
}
}
return id_valid && ( !npc_range.has_value() || actor_pos.z == guy.posz() ) && ( !npc_must_see ||
guy.sees( actor_pos ) ) &&
( !npc_range.has_value() || rl_dist( actor_pos, guy.pos() ) <= npc_range.value() );
} );
for( npc *target : available ) {
for( const effect_on_condition_id &eoc : eocs ) {
dialogue newDialog( get_talker_for( target ), nullptr );
eoc->activate( newDialog );
}
}
return name_valid && ( !npc_range.has_value() || actor_pos.z == guy.posz() ) && ( !npc_must_see ||
guy.sees( actor_pos ) ) &&
( !npc_range.has_value() || rl_dist( actor_pos, guy.pos() ) <= npc_range.value() );
} );
for( npc *target : available ) {
for( const effect_on_condition_id &eoc : eocs ) {
dialogue newDialog( get_talker_for( target ), nullptr );
eoc->activate( newDialog );
};
} else {
function = [eocs, unique_ids]( const dialogue & ) {
for( const std::string &target : unique_ids ) {
if( g->unique_npc_exists( target ) ) {
for( const effect_on_condition_id &eoc : eocs ) {
npc *npc = g->find_npc_by_unique_id( target );
if( npc ) {
dialogue newDialog( get_talker_for( npc ), nullptr );
eoc->activate( newDialog );
} else {
debugmsg( "Tried to use invalid npc: %s", target );
}
}
}
}
}
};
};
}
}

void talk_effect_fun_t::set_queue_eocs( const JsonObject &jo, const std::string &member )
Expand Down
10 changes: 10 additions & 0 deletions src/overmap.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -6432,6 +6432,16 @@ shared_ptr_fast<npc> overmap::find_npc( const character_id &id ) const
return nullptr;
}

shared_ptr_fast<npc> overmap::find_npc_by_unique_id( const std::string &id ) const
{
for( const auto &guy : npcs ) {
if( guy->get_unique_id() == id ) {
return guy;
}
}
return nullptr;
}

cata::optional<basecamp *> overmap::find_camp( const point_abs_omt &p )
{
for( auto &v : camps ) {
Expand Down
2 changes: 1 addition & 1 deletion src/overmap.h
Original file line number Diff line number Diff line change
Expand Up @@ -369,7 +369,7 @@ class overmap
void for_each_npc( const std::function<void( const npc & )> &callback ) const;

shared_ptr_fast<npc> find_npc( const character_id &id ) const;

shared_ptr_fast<npc> find_npc_by_unique_id( const std::string &id ) const;
const std::vector<shared_ptr_fast<npc>> &get_npcs() const {
return npcs;
}
Expand Down
6 changes: 6 additions & 0 deletions src/overmapbuffer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1267,6 +1267,12 @@ shared_ptr_fast<npc> overmapbuffer::find_npc( character_id id )
return nullptr;
}

shared_ptr_fast<npc> overmapbuffer::find_npc_by_unique_id( std::string unique_id )
{
point_abs_om loc = g->get_unique_npc_location( unique_id );
return get( loc ).find_npc_by_unique_id( unique_id );
}

cata::optional<basecamp *> overmapbuffer::find_camp( const point_abs_omt &p )
{
for( auto &it : overmaps ) {
Expand Down
1 change: 1 addition & 0 deletions src/overmapbuffer.h
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,7 @@ class overmapbuffer
* Searches all loaded overmaps.
*/
shared_ptr_fast<npc> find_npc( character_id id );
shared_ptr_fast<npc> find_npc_by_unique_id( std::string unique_id );
/**
* Get all NPCs active on the overmap
*/
Expand Down
3 changes: 2 additions & 1 deletion src/savegame.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ void game::serialize( std::ostream &fout )
json.end_array();
global_variables_instance.serialize( json );
Messages::serialize( json );

json.member( "unique_npcs", unique_npcs );
json.end_object();
}

Expand Down Expand Up @@ -269,6 +269,7 @@ void game::unserialize( std::istream &fin, const std::string &path )
queued_global_effect_on_conditions.push( temp );
}
global_variables_instance.unserialize( data );
data.read( "unique_npcs", unique_npcs );
inp_mngr.pump_events();
data.read( "stats_tracker", *stats_tracker_ptr );
data.read( "achievements_tracker", *achievements_tracker_ptr );
Expand Down
2 changes: 2 additions & 0 deletions src/savegame_json.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2235,6 +2235,7 @@ void npc::load( const JsonObject &data )
member.read( p );
complaints.emplace( member.name(), p );
}
data.read( "unique_id", unique_id );
}

/*
Expand Down Expand Up @@ -2302,6 +2303,7 @@ void npc::store( JsonOut &json ) const
json.member( "restock", restock );

json.member( "complaints", complaints );
json.member( "unique_id", unique_id );
}

////////////////////////////////////////////////////////////////////////////////////////////////////
Expand Down

0 comments on commit dc48ac2

Please sign in to comment.