From 779ac3fd50be8588c53e4f637a2f217ca750d962 Mon Sep 17 00:00:00 2001 From: expwnent Date: Sun, 9 Nov 2014 18:36:21 -0500 Subject: [PATCH] Updated interaction-trigger to work better. --- NEWS | 3 + library/include/modules/EventManager.h | 4 +- library/modules/EventManager.cpp | 261 +++++++++++++++++------ scripts/modtools/interaction-trigger.lua | 75 ++++--- 4 files changed, 239 insertions(+), 104 deletions(-) diff --git a/NEWS b/NEWS index dada05bda..9bbf52602 100644 --- a/NEWS +++ b/NEWS @@ -1,4 +1,7 @@ DFHack Future + Internals: + EventManager should handle INTERACTION triggers a little better. It still can get confused about who did what but only rarely. + devel/all-bob.lua: renames everyone Bob to help test interaction-trigger DFHack 0.40.15-r1 Fixes: diff --git a/library/include/modules/EventManager.h b/library/include/modules/EventManager.h index c7aacf4bb..7d5596f16 100644 --- a/library/include/modules/EventManager.h +++ b/library/include/modules/EventManager.h @@ -84,10 +84,10 @@ namespace DFHack { struct InteractionData { std::string attackVerb; std::string defendVerb; - int32_t attackReport; - int32_t defendReport; int32_t attacker; int32_t defender; + int32_t attackReport; + int32_t defendReport; }; DFHACK_EXPORT void registerListener(EventType::EventType e, EventHandler handler, Plugin* plugin); diff --git a/library/modules/EventManager.cpp b/library/modules/EventManager.cpp index e3f47b13a..e8655b235 100644 --- a/library/modules/EventManager.cpp +++ b/library/modules/EventManager.cpp @@ -16,6 +16,7 @@ #include "df/general_ref_type.h" #include "df/general_ref_unit_workerst.h" #include "df/global_objects.h" +#include "df/interaction.h" #include "df/item.h" #include "df/item_actual.h" #include "df/item_constructed.h" @@ -956,20 +957,161 @@ static void manageUnitAttackEvent(color_ostream& out) { static std::string getVerb(df::unit* unit, std::string reportStr) { std::string result(reportStr); std::string name = unit->name.first_name + " "; - bool useName = strncmp(result.c_str(), name.c_str(), name.length()) == 0; - if ( useName ) { + bool match = strncmp(result.c_str(), name.c_str(), name.length()) == 0; + if ( match ) { result = result.substr(name.length()); result = result.substr(0,result.length()-1); return result; } //use profession name - std::string profession = "The " + Units::getProfessionName(unit) + " "; - bool match = strncmp(result.c_str(), profession.c_str(), profession.length()) == 0; - if ( !match ) + name = "The " + Units::getProfessionName(unit) + " "; + match = strncmp(result.c_str(), name.c_str(), name.length()) == 0; + if ( match ) { + result = result.substr(name.length()); + result = result.substr(0,result.length()-1); + return result; + } + + if ( unit->id != 0 ) { return ""; - result = result.substr(profession.length()); - result = result.substr(0,result.length()-1); - //TODO: special case for the player in adventure mode + } + + std::string you = "You "; + match = strncmp(result.c_str(), name.c_str(), name.length()) == 0; + if ( match ) { + result = result.substr(name.length()); + result = result.substr(0,result.length()-1); + return result; + } + return ""; +} + +static InteractionData getAttacker(color_ostream& out, df::report* attackEvent, df::unit* lastAttacker, df::report* defendEvent, vector& relevantUnits) { + vector attackers = relevantUnits; + vector defenders = relevantUnits; + + //find valid interactions: TODO + /*map > validInteractions; + for ( size_t a = 0; a < relevantUnits.size(); a++ ) { + df::unit* unit = relevantUnits[a]; + vector& interactions = validInteractions[unit->id]; + for ( size_t b = 0; b < unit->body. + }*/ + + //if attackEvent + // attacker must be same location + // attacker name must start attack str + // attack verb must match valid interaction of this attacker + std::string attackVerb; + if ( attackEvent ) { + for ( size_t a = 0; a < attackers.size(); a++ ) { + if ( attackers[a]->pos != attackEvent->pos ) { + attackers.erase(attackers.begin()+a); + a--; + continue; + } + if ( lastAttacker && attackers[a] != lastAttacker ) { + attackers.erase(attackers.begin()+a); + a--; + continue; + } + + std::string verbC = getVerb(attackers[a], attackEvent->text); + if ( verbC.length() == 0 ) { + attackers.erase(attackers.begin()+a); + a--; + continue; + } + attackVerb = verbC; + } + } + + //if defendEvent + // defender must be same location + // defender name must start defend str + // defend verb must match valid interaction of some attacker + std::string defendVerb; + if ( defendEvent ) { + for ( size_t a = 0; a < defenders.size(); a++ ) { + if ( defenders[a]->pos != defendEvent->pos ) { + defenders.erase(defenders.begin()+a); + a--; + continue; + } + std::string verbC = getVerb(defenders[a], defendEvent->text); + if ( verbC.length() == 0 ) { + defenders.erase(defenders.begin()+a); + a--; + continue; + } + defendVerb = verbC; + } + } + + //keep in mind one attacker zero defenders is perfectly valid for self-cast + if ( attackers.size() == 1 && defenders.size() == 1 && attackers[0] == defenders[0] ) { + } else { + if ( defenders.size() == 1 ) { + auto a = std::find(attackers.begin(),attackers.end(),defenders[0]); + if ( a != attackers.end() ) + attackers.erase(a); + } + if ( attackers.size() == 1 ) { + auto a = std::find(defenders.begin(),defenders.end(),attackers[0]); + if ( a != defenders.end() ) + defenders.erase(a); + } + } + + //if trying attack-defend pair and it fails to find attacker, try defend only + InteractionData result = /*(InteractionData)*/ { std::string(), std::string(), -1, -1, -1, -1 }; + if ( attackers.size() > 1 ) { + if ( Once::doOnce("EventManager interaction ambiguous attacker") ) { + out.print("%s,%d: ambiguous attacker on report\n \'%s\'\n '%s'\n", __FILE__, __LINE__, attackEvent ? attackEvent->text.c_str() : "", defendEvent ? defendEvent->text.c_str() : ""); + } + } else if ( attackers.size() < 1 ) { + if ( defendEvent ) + return getAttacker(out, NULL, NULL, defendEvent, relevantUnits); + } else { + //attackers.size() == 1 + result.attacker = attackers[0]->id; + if ( defenders.size() > 0 ) + result.defender = defenders[0]->id; + if ( defenders.size() > 1 ) { + if ( Once::doOnce("EventManager interaction ambiguous defender") ) { + out.print("%s,%d: ambiguous defender: shouldn't happen. On report\n \'%s\'\n '%s'\n", __FILE__, __LINE__, attackEvent ? attackEvent->text.c_str() : "", defendEvent ? defendEvent->text.c_str() : ""); + } + } + result.attackVerb = attackVerb; + result.defendVerb = defendVerb; + if ( attackEvent ) + result.attackReport = attackEvent->id; + if ( defendEvent ) + result.defendReport = defendEvent->id; + } + return result; +} + +static vector gatherRelevantUnits(color_ostream& out, df::report* r1, df::report* r2) { + vector reports; + if ( r1 == r2 ) r2 = NULL; + if ( r1 ) reports.push_back(r1); + if ( r2 ) reports.push_back(r2); + vector result; + unordered_set ids; + for ( size_t a = 0; a < reports.size(); a++ ) { + vector& units = reportToRelevantUnits[reports[a]->id]; + if ( units.size() > 2 ) { + if ( Once::doOnce("EventManager interaction too many relevant units") ) { + out.print("%s,%d: too many relevant units. On report\n \'%s\'\n", __FILE__, __LINE__, reports[a]->text.c_str()); + } + } + for ( size_t b = 0; b < units.size(); b++ ) + if ( ids.find(units[b]) == ids.end() ) { + ids.insert(units[b]); + result.push_back(df::unit::find(units[b])); + } + } return result; } @@ -984,83 +1126,62 @@ static void manageInteractionEvent(color_ostream& out) { } if ( a < reports.size() ) updateReportToRelevantUnits(); - - int32_t attackerId = -1; - int32_t lastTime = -1; - std::string attackVerb; - int32_t attackReport = -1; + + df::report* lastAttackEvent = NULL; + df::unit* lastAttacker = NULL; + df::unit* lastDefender = NULL; + unordered_map > history; for ( ; a < reports.size(); a++ ) { df::report* report = reports[a]; lastReportInteraction = report->id; - if ( report->flags.bits.continuation ) - continue; df::announcement_type type = report->type; if ( type != df::announcement_type::INTERACTION_ACTOR && type != df::announcement_type::INTERACTION_TARGET ) continue; - int32_t unitId = -1; - int32_t validCount = 0; - std::string verb; - //find relevant unit - for ( auto b = reportToRelevantUnits[report->id].begin(); b != reportToRelevantUnits[report->id].end(); b++ ) { - int32_t candidateId = *b; - df::unit* candidate = df::unit::find(candidateId); - if ( !candidate ) { - //TODO: error - continue; - } - if ( candidate->pos != report->pos ) - continue; - std::string verbC = getVerb(candidate, report->text); - if ( verbC.length() == 0 ) - continue; - verb = verbC; - validCount++; - unitId = candidateId; - } - if ( validCount > 1 ) { - if ( Once::doOnce("EventManager interaction too many actors") ) { - out.print("%s:%d: too many actors for report %d\n", __FILE__, __LINE__, report->id); - out.print("reportStr = \"%s\", pos = %d,%d,%d\n", report->text.c_str(), report->pos.x, report->pos.y, report->pos.z); - } - attackerId = -1; + if ( report->flags.bits.continuation ) continue; + bool attack = type == df::announcement_type::INTERACTION_ACTOR; + if ( attack ) { + lastAttackEvent = report; + lastAttacker = NULL; + lastDefender = NULL; } - if ( validCount == 0 ) { - if ( Once::doOnce("EventManager interaction too few actors") ) { - out.print("%s:%d: too few actors for report %d\n", __FILE__, __LINE__, report->id); - out.print("reportStr = \"%s\", pos = %d,%d,%d\n", report->text.c_str(), report->pos.x, report->pos.y, report->pos.z); - } - attackerId = -1; - continue; - } - //int32_t unitId = reportToRelevantUnits[report->id][0]; - bool isActor = attackerId == -1;//type == df::announcement_type::INTERACTION_ACTOR; - - if ( isActor ) { - attackReport = report->id; - attackerId = unitId; - lastTime = report->year*ticksPerYear + report->time; - attackVerb = verb; + vector relevantUnits = gatherRelevantUnits(out, lastAttackEvent, report); + InteractionData data = getAttacker(out, lastAttackEvent, lastAttacker, attack ? NULL : report, relevantUnits); + if ( data.attacker < 0 ) continue; + //if ( !attack && lastAttacker && data.attacker == lastAttacker->id && lastDefender && data.defender == lastDefender->id ) + // continue; //lazy way of preventing duplicates + if ( attack && a+1 < reports.size() && reports[a+1]->type == df::announcement_type::INTERACTION_TARGET ) { + InteractionData data2 = getAttacker(out, lastAttackEvent, lastAttacker, reports[a+1], gatherRelevantUnits(out, lastAttackEvent, reports[a+1])); + if ( data.attacker == data2.attacker && (data.defender == -1 || data.defender == data2.defender) ) { + data = data2; + a++; + } } - - if ( attackerId == -1 ) - continue; - if ( report->year*ticksPerYear + report->time != lastTime ) { - attackerId = -1; - continue; + { +#define HISTORY_ITEM 1 +#if HISTORY_ITEM + unordered_set& b = history[data.attacker]; + if ( b.find(data.defender) != b.end() ) + continue; + history[data.attacker].insert(data.defender); + //b.insert(data.defender); +#else + unordered_set& b = history[data.attackReport]; + if ( b.find(data.defendReport) != b.end() ) + continue; + history[data.attackReport].insert(data.defendReport); + //b.insert(data.defendReport); +#endif } - InteractionData data; - data.attacker = attackerId; - data.defender = unitId; - data.attackReport = attackReport; - data.defendReport = report->id; - data.attackVerb = attackVerb; - data.defendVerb = verb; + lastAttacker = df::unit::find(data.attacker); + lastDefender = df::unit::find(data.defender); + //fire event for ( auto b = copy.begin(); b != copy.end(); b++ ) { EventHandler handle = (*b).second; handle.eventHandler(out, (void*)&data); } + //TODO: deduce attacker from latest defend event first } } diff --git a/scripts/modtools/interaction-trigger.lua b/scripts/modtools/interaction-trigger.lua index 6831f482e..9edee69e2 100644 --- a/scripts/modtools/interaction-trigger.lua +++ b/scripts/modtools/interaction-trigger.lua @@ -5,15 +5,19 @@ local eventful = require 'plugins.eventful' local utils = require 'utils' -attackStrTriggers = attackStrTriggers or {} -defendStrTriggers = defendStrTriggers or {} +attackTriggers = attackTriggers or {} +defendTriggers = defendTriggers or {} +commands = commands or {} +commandCount = commandCount or 0 eventful.enableEvent(eventful.eventType.INTERACTION,1) --cheap, so every tick is fine eventful.enableEvent(eventful.eventType.UNLOAD,1) eventful.onUnload.interactionTrigger = function() - attackStrTriggers = {} - defendStrTriggers = {} + attackTriggers = {} + defendTriggers = {} + commands = {} + commandCount = 0 end local function processTrigger(args) @@ -50,21 +54,22 @@ eventful.onInteraction.interactionTrigger = function(attackVerb, defendVerb, att extras.defenderId = defender local suppressAttack = false local suppressDefend = false - for _,trigger in ipairs(attackStrTriggers[attackVerb] or {}) do - suppressAttack = suppressAttack or trigger.suppressAttack - suppressDefend = suppressDefend or trigger.suppressDefend - utils.fillTable(trigger,extras) - processTrigger(trigger) - utils.unfillTable(trigger,extras) + local todo = {} + for _,trigger in ipairs(attackTriggers[attackVerb] or {}) do + todo[trigger] = true end - for _,trigger in ipairs(defendStrTriggers[defendVerb] or {}) do - suppressAttack = suppressAttack or trigger.suppressAttack - suppressDefend = suppressDefend or trigger.suppressDefend - utils.fillTable(trigger,extras) - processTrigger(trigger) - utils.unfillTable(trigger,extras) + for _,trigger in ipairs(defendTriggers[defendVerb] or {}) do + todo[trigger] = true end - + for k,v in pairs(todo) do + command = commands[k] + suppressAttack = suppressAttack or command.suppressAttack + suppressDefend = suppressDefend or command.suppressDefend + utils.fillTable(command,extras) + processTrigger(command) + utils.unfillTable(command,extras) + end + local eraseReport = function(unit,report) for i,v in ipairs(unit.reports.log.Combat) do if v == report then @@ -85,6 +90,7 @@ eventful.onInteraction.interactionTrigger = function(attackVerb, defendVerb, att eraseReport(attacker,defendReport) eraseReport(defender,defendReport) end + --TODO: get rid of combat report on LHS of screen end ---------------------------------------------------- @@ -110,9 +116,9 @@ arguments: -clear unregisters all triggers -onAttackStr str - trigger the command when the attack verb is "str" + trigger the command when the attack verb is "str". both onAttackStr and onDefendStr MUST be specified -onDefendStr str - trigger the command when the defend verb is "str" + trigger the command when the defend verb is "str". both onAttackStr and onDefendStr MUST be specified -suppressAttack delete the attack announcement from the combat logs -suppressDefend @@ -126,32 +132,37 @@ arguments: \\DEFENDER_ID \\ATTACK_REPORT \\DEFEND_REPORT - \anything -> anything + \\anything -> \anything anything -> anything +You must specify both an attack string and a defend string to guarantee correct performance. Either will trigger the script when it happens, but it will not be triggered twice in a row if both happen. ]]) return end if args.clear then - attackStrTriggers = {} - defendStrTriggers = {} + triggers = {} + commands = {} + commandCount = 0 end if not args.command then return end -if args.onAttackStr then - if not attackStrTriggers[args.onAttackStr] then - attackStrTriggers[args.onAttackStr] = {} - end - table.insert(attackStrTriggers[args.onAttackStr], args) +if not args.onAttackStr or not args.onDefendStr then + error 'You must specify both onAttackStr and onDefendStr.' end -if args.onDefendStr then - if not defendStrTriggers[args.onDefendStr] then - defendStrTriggers[args.onDefendStr] = {} - end - table.insert(defendStrTriggers[args.onDefendStr], args) +commands[commandCount] = args + +if not attackTriggers[args.onAttackStr] then + attackTriggers[args.onAttackStr] = {} +end +table.insert(attackTriggers[args.onAttackStr],commandCount) + +if not defendTriggers[args.onDefendStr] then + defendTriggers[args.onDefendStr] = {} end +table.insert(defendTriggers[args.onDefendStr],commandCount) +commandCount = commandCount+1