Scaffold for conversation manager

This commit is contained in:
Heron
2026-05-16 12:03:54 -04:00
parent ea1267762a
commit eeac9155bf
10 changed files with 805 additions and 0 deletions
@@ -0,0 +1,11 @@
package script.systems.conversation_manager;
public class conversation_action
{
public String actionSet;
public int order;
public String actionType;
public String arg1;
public String arg2;
public String arg3;
}
@@ -0,0 +1,162 @@
package script.systems.conversation_manager;
import script.dictionary;
import script.library.armor;
import script.library.factions;
import script.library.groundquests;
import script.library.money;
import script.library.utils;
import script.obj_id;
import script.string_id;
public class conversation_actions extends script.base_script
{
public conversation_actions()
{
}
public void executeActionSet(conversation_context ctx, conversation_definition definition, String actionSet) throws InterruptedException
{
if (actionSet == null || actionSet.equals(""))
{
return;
}
for (int order = 0; order < 1000; ++order)
{
for (conversation_action action : definition.actions)
{
if (action.actionSet != null && action.actionSet.equals(actionSet) && action.order == order)
{
debug(ctx, "execute actionSet=" + actionSet + " order=" + action.order + " type=" + action.actionType + " arg1=" + action.arg1);
execute(ctx, action);
}
}
}
}
public void execute(conversation_context ctx, conversation_action action) throws InterruptedException
{
if (action == null || action.actionType == null || action.actionType.equals(""))
{
return;
}
if (action.actionType.equals("grantQuest"))
{
groundquests.grantQuest(ctx.player, action.arg1);
return;
}
if (action.actionType.equals("requestGrantQuest"))
{
groundquests.requestGrantQuest(ctx.player, action.arg1);
return;
}
if (action.actionType.equals("clearQuest"))
{
groundquests.clearQuest(ctx.player, action.arg1);
return;
}
if (action.actionType.equals("sendQuestSignal"))
{
groundquests.sendSignal(ctx.player, action.arg1);
return;
}
if (action.actionType.equals("giveItem"))
{
createObjectInInventoryAllowOverload(action.arg1, ctx.player);
return;
}
if (action.actionType.equals("giveArmorItem"))
{
obj_id armorItem = createObjectInInventoryAllowOverload(action.arg1, ctx.player);
if (isIdValid(armorItem) && action.arg2 != null && !action.arg2.equals(""))
{
float min = utils.stringToFloat(action.arg2);
float max = min;
if (action.arg3 != null && !action.arg3.equals(""))
{
max = utils.stringToFloat(action.arg3);
}
if (!isGameObjectTypeOf(armorItem, GOT_armor_foot) && !isGameObjectTypeOf(armorItem, GOT_armor_hand))
{
armor.setArmorDataPercent(armorItem, 2, 1, min, max);
}
}
return;
}
if (action.actionType.equals("grantSchematic"))
{
grantSchematic(ctx.player, action.arg1);
return;
}
if (action.actionType.equals("rewardFaction"))
{
factions.awardFactionStanding(ctx.player, action.arg1, utils.stringToInt(action.arg2));
return;
}
if (action.actionType.equals("giveCredits"))
{
money.bankTo(money.ACCT_JABBA, ctx.player, utils.stringToInt(action.arg1));
return;
}
if (action.actionType.equals("setObjvar"))
{
setObjVar(ctx.player, action.arg1, action.arg2);
return;
}
if (action.actionType.equals("removeObjvar"))
{
removeObjVar(ctx.player, action.arg1);
return;
}
if (action.actionType.equals("playNpcAnimation"))
{
doAnimationAction(ctx.npc, action.arg1);
return;
}
if (action.actionType.equals("playPlayerAnimation"))
{
doAnimationAction(ctx.player, action.arg1);
return;
}
if (action.actionType.equals("facePlayer"))
{
faceTo(ctx.npc, ctx.player);
return;
}
if (action.actionType.equals("sendMessageNpc"))
{
dictionary params = new dictionary();
params.put("player", ctx.player);
params.put("npc", ctx.npc);
params.put("arg1", action.arg1);
params.put("arg2", action.arg2);
params.put("arg3", action.arg3);
messageTo(ctx.npc, action.arg1, params, 0, false);
return;
}
if (action.actionType.equals("sendSystemMessage"))
{
sendSystemMessage(ctx.player, new string_id(ctx.stringFile, action.arg1));
return;
}
LOG("conversation_manager", "Unknown action type '" + action.actionType + "' for conversation '" + ctx.conversationId + "'");
debug(ctx, "unknown action type=" + action.actionType);
}
public void debug(conversation_context ctx, String message) throws InterruptedException
{
if (ctx == null || !isIdValid(ctx.npc) || !isIdValid(ctx.player))
{
return;
}
if (!hasObjVar(ctx.npc, "conversation_manager.debug"))
{
return;
}
if (!getBooleanObjVar(ctx.npc, "conversation_manager.debug"))
{
return;
}
sendSystemMessage(ctx.player, "[conversation_manager] " + message, null);
}
}
@@ -0,0 +1,106 @@
package script.systems.conversation_manager;
import script.library.features;
import script.library.factions;
import script.library.groundquests;
import script.obj_id;
public class conversation_conditions extends script.base_script
{
public conversation_conditions()
{
}
public boolean passes(conversation_context ctx, String conditionType, String arg1, String arg2) throws InterruptedException
{
if (conditionType == null || conditionType.equals("") || conditionType.equals("always"))
{
return true;
}
if (conditionType.equals("questActive"))
{
return debugResult(ctx, conditionType, arg1, arg2, groundquests.isQuestActive(ctx.player, arg1));
}
if (conditionType.equals("questComplete"))
{
return debugResult(ctx, conditionType, arg1, arg2, groundquests.hasCompletedQuest(ctx.player, arg1));
}
if (conditionType.equals("questActiveOrComplete"))
{
return debugResult(ctx, conditionType, arg1, arg2, groundquests.isQuestActiveOrComplete(ctx.player, arg1));
}
if (conditionType.equals("taskActive"))
{
return debugResult(ctx, conditionType, arg1, arg2, groundquests.isTaskActive(ctx.player, arg1, arg2));
}
if (conditionType.equals("taskComplete"))
{
return debugResult(ctx, conditionType, arg1, arg2, groundquests.hasCompletedTask(ctx.player, arg1, arg2));
}
if (conditionType.equals("hasObjvar"))
{
return debugResult(ctx, conditionType, arg1, arg2, hasObjVar(ctx.player, arg1));
}
if (conditionType.equals("notHasObjvar"))
{
return debugResult(ctx, conditionType, arg1, arg2, !hasObjVar(ctx.player, arg1));
}
if (conditionType.equals("objvarEquals"))
{
return debugResult(ctx, conditionType, arg1, arg2, hasObjVar(ctx.player, arg1) && getStringObjVar(ctx.player, arg1).equals(arg2));
}
if (conditionType.equals("faction"))
{
String playerFaction = factions.getFaction(ctx.player);
return debugResult(ctx, conditionType, arg1, arg2, playerFaction != null && playerFaction.equals(arg1));
}
if (conditionType.equals("hasSkill"))
{
return debugResult(ctx, conditionType, arg1, arg2, hasSkill(ctx.player, arg1));
}
if (conditionType.equals("hasEp3"))
{
return debugResult(ctx, conditionType, arg1, arg2, features.hasEpisode3Expansion(ctx.player));
}
if (conditionType.equals("isGm"))
{
return debugResult(ctx, conditionType, arg1, arg2, hasObjVar(ctx.player, "gm"));
}
LOG("conversation_manager", "Unknown condition type '" + conditionType + "' for conversation '" + ctx.conversationId + "'");
debug(ctx, "unknown condition type=" + conditionType + " arg1=" + arg1 + " arg2=" + arg2);
return false;
}
public boolean passesNode(conversation_context ctx, conversation_node node) throws InterruptedException
{
return passes(ctx, node.conditionType, node.conditionArg1, node.conditionArg2);
}
public boolean passesResponse(conversation_context ctx, conversation_response response) throws InterruptedException
{
return passes(ctx, response.conditionType, response.conditionArg1, response.conditionArg2);
}
public boolean debugResult(conversation_context ctx, String conditionType, String arg1, String arg2, boolean result) throws InterruptedException
{
debug(ctx, "condition type=" + conditionType + " arg1=" + arg1 + " arg2=" + arg2 + " result=" + result);
return result;
}
public void debug(conversation_context ctx, String message) throws InterruptedException
{
if (ctx == null || !isIdValid(ctx.npc) || !isIdValid(ctx.player))
{
return;
}
if (!hasObjVar(ctx.npc, "conversation_manager.debug"))
{
return;
}
if (!getBooleanObjVar(ctx.npc, "conversation_manager.debug"))
{
return;
}
sendSystemMessage(ctx.player, "[conversation_manager] " + message, null);
}
}
@@ -0,0 +1,21 @@
package script.systems.conversation_manager;
import script.obj_id;
public class conversation_context
{
public obj_id npc;
public obj_id player;
public String conversationId;
public String table;
public String stringFile;
public conversation_context(obj_id npc, obj_id player, conversation_definition definition)
{
this.npc = npc;
this.player = player;
this.conversationId = definition.conversationId;
this.table = definition.table;
this.stringFile = definition.stringFile;
}
}
@@ -0,0 +1,18 @@
package script.systems.conversation_manager;
public class conversation_definition
{
public String conversationId;
public String table;
public String stringFile;
public conversation_node[] nodes;
public conversation_response[] responses;
public conversation_action[] actions;
public conversation_definition()
{
nodes = new conversation_node[0];
responses = new conversation_response[0];
actions = new conversation_action[0];
}
}
@@ -0,0 +1,216 @@
package script.systems.conversation_manager;
import script.library.ai_lib;
import script.library.utils;
import script.obj_id;
import script.string_id;
public class conversation_engine extends script.base_script
{
public conversation_engine()
{
}
public static final String CONVERSATION_NAME = "conversationManager";
public static final String SCRIPT_VAR_PREFIX = "conversation_manager.";
protected conversation_conditions conditions = new conversation_conditions();
protected conversation_actions actions = new conversation_actions();
public int startConversation(obj_id npc, obj_id player, conversation_definition definition) throws InterruptedException
{
if (ai_lib.isInCombat(npc) || ai_lib.isInCombat(player))
{
return SCRIPT_OVERRIDE;
}
conversation_context ctx = new conversation_context(npc, player, definition);
debug(ctx, "start conversation table=" + definition.table);
conversation_node startNode = findStartNode(ctx, definition);
if (startNode == null)
{
LOG("conversation_manager", "No valid start node for conversation '" + definition.conversationId + "'");
debug(ctx, "no valid start node");
return SCRIPT_OVERRIDE;
}
debug(ctx, "start node=" + startNode.nodeId);
return enterNode(ctx, definition, startNode);
}
public int continueConversation(obj_id npc, obj_id player, string_id responseId, conversation_definition definition) throws InterruptedException
{
conversation_context ctx = new conversation_context(npc, player, definition);
String currentNodeId = getCurrentNode(player, definition);
debug(ctx, "continue conversation currentNode=" + currentNodeId + " response=" + responseId.getAsciiId());
if (currentNodeId == null || currentNodeId.equals(""))
{
LOG("conversation_manager", "No current node for conversation '" + definition.conversationId + "'");
endConversation(ctx, null);
return SCRIPT_CONTINUE;
}
conversation_response response = findResponse(ctx, definition, currentNodeId, responseId);
if (response == null)
{
LOG("conversation_manager", "No matching response '" + responseId + "' for node '" + currentNodeId + "'");
debug(ctx, "no matching response for current node");
endConversation(ctx, null);
return SCRIPT_CONTINUE;
}
debug(ctx, "selected response=" + response.responseId + " actionSet=" + response.actionSet);
actions.executeActionSet(ctx, definition, response.actionSet);
if (response.endConversation)
{
debug(ctx, "response ends conversation");
endConversation(ctx, response.endTextKey);
return SCRIPT_CONTINUE;
}
conversation_node nextNode = findNode(definition, response.nextNodeId);
if (nextNode == null)
{
debug(ctx, "no next node, ending conversation");
endConversation(ctx, response.endTextKey);
return SCRIPT_CONTINUE;
}
debug(ctx, "next node=" + nextNode.nodeId);
return enterNode(ctx, definition, nextNode);
}
public int enterNode(conversation_context ctx, conversation_definition definition, conversation_node node) throws InterruptedException
{
debug(ctx, "enter node=" + node.nodeId + " enterActionSet=" + node.enterActionSet);
actions.executeActionSet(ctx, definition, node.enterActionSet);
conversation_response[] responses = getValidResponses(ctx, definition, node.nodeId);
string_id message = new string_id(ctx.stringFile, node.textKey);
if (node.endNode || responses.length == 0)
{
debug(ctx, "node ends conversation responseCount=" + responses.length);
endConversation(ctx, node.textKey);
return SCRIPT_CONTINUE;
}
string_id[] responseIds = new string_id[responses.length];
for (int i = 0; i < responses.length; ++i)
{
responseIds[i] = new string_id(ctx.stringFile, responses[i].textKey);
}
setCurrentNode(ctx.player, definition, node.nodeId);
debug(ctx, "show node=" + node.nodeId + " responseCount=" + responses.length);
npcStartConversation(ctx.player, ctx.npc, CONVERSATION_NAME, message, responseIds);
return SCRIPT_CONTINUE;
}
public void endConversation(conversation_context ctx, String textKey) throws InterruptedException
{
debug(ctx, "end conversation textKey=" + textKey);
clearCurrentNode(ctx.player, ctx.conversationId);
if (textKey != null && !textKey.equals(""))
{
npcEndConversationWithMessage(ctx.player, new string_id(ctx.stringFile, textKey));
}
else
{
npcEndConversation(ctx.player);
}
}
public conversation_node findStartNode(conversation_context ctx, conversation_definition definition) throws InterruptedException
{
conversation_node best = null;
for (conversation_node node : definition.nodes)
{
if (node.startNode && conditions.passesNode(ctx, node))
{
if (best == null || node.priority < best.priority)
{
best = node;
}
}
}
return best;
}
public conversation_node findNode(conversation_definition definition, String nodeId) throws InterruptedException
{
if (nodeId == null || nodeId.equals(""))
{
return null;
}
for (conversation_node node : definition.nodes)
{
if (node.nodeId != null && node.nodeId.equals(nodeId))
{
return node;
}
}
return null;
}
public conversation_response findResponse(conversation_context ctx, conversation_definition definition, String nodeId, string_id selectedResponse) throws InterruptedException
{
for (conversation_response response : definition.responses)
{
if (response.nodeId != null && response.nodeId.equals(nodeId) && response.textKey != null && response.textKey.equals(selectedResponse.getAsciiId()) && conditions.passesResponse(ctx, response))
{
return response;
}
}
return null;
}
public conversation_response[] getValidResponses(conversation_context ctx, conversation_definition definition, String nodeId) throws InterruptedException
{
int count = 0;
for (conversation_response response : definition.responses)
{
if (response.nodeId != null && response.nodeId.equals(nodeId) && conditions.passesResponse(ctx, response))
{
++count;
}
}
conversation_response[] valid = new conversation_response[count];
int index = 0;
for (conversation_response response : definition.responses)
{
if (response.nodeId != null && response.nodeId.equals(nodeId) && conditions.passesResponse(ctx, response))
{
valid[index++] = response;
}
}
return valid;
}
public String getCurrentNode(obj_id player, conversation_definition definition) throws InterruptedException
{
return utils.getStringScriptVar(player, getCurrentNodeScriptVar(definition.conversationId));
}
public void setCurrentNode(obj_id player, conversation_definition definition, String nodeId) throws InterruptedException
{
utils.setScriptVar(player, getCurrentNodeScriptVar(definition.conversationId), nodeId);
}
public void clearCurrentNode(obj_id player, String conversationId) throws InterruptedException
{
utils.removeScriptVar(player, getCurrentNodeScriptVar(conversationId));
}
public String getCurrentNodeScriptVar(String conversationId) throws InterruptedException
{
return SCRIPT_VAR_PREFIX + conversationId + ".currentNode";
}
public void debug(conversation_context ctx, String message) throws InterruptedException
{
if (ctx == null || !isIdValid(ctx.npc) || !isIdValid(ctx.player))
{
return;
}
if (!hasObjVar(ctx.npc, "conversation_manager.debug"))
{
return;
}
if (!getBooleanObjVar(ctx.npc, "conversation_manager.debug"))
{
return;
}
sendSystemMessage(ctx.player, "[conversation_manager] " + message, null);
}
}
@@ -0,0 +1,175 @@
package script.systems.conversation_manager;
import script.dictionary;
import script.obj_id;
public class conversation_loader extends script.base_script
{
public conversation_loader()
{
}
public static final String TABLE_OBJVAR = "conversation_manager.table";
public static final String TABLE_ROOT = "datatables/conversation_manager/";
public conversation_definition load(obj_id npc) throws InterruptedException
{
String tableName = getStringObjVar(npc, TABLE_OBJVAR);
conversation_definition definition = new conversation_definition();
definition.table = tableName;
definition.conversationId = tableName;
if (tableName == null || tableName.equals(""))
{
LOG("conversation_manager", "Missing objvar '" + TABLE_OBJVAR + "' on npc " + npc);
return definition;
}
String table = TABLE_ROOT + tableName + ".iff";
definition.stringFile = getStringCell(table, 0, "stringFile");
if (definition.stringFile == null || definition.stringFile.equals(""))
{
definition.stringFile = "conversation/" + tableName;
}
definition.nodes = loadNodes(table);
definition.responses = loadResponses(table);
definition.actions = loadActions(table);
return definition;
}
public conversation_node[] loadNodes(String table) throws InterruptedException
{
String[] nodeIds = dataTableGetStringColumn(table, "nodeId");
if (nodeIds == null)
{
return new conversation_node[0];
}
int count = 0;
for (String nodeId : nodeIds)
{
if (nodeId != null && !nodeId.equals(""))
{
++count;
}
}
conversation_node[] nodes = new conversation_node[count];
int index = 0;
for (int i = 0; i < nodeIds.length; ++i)
{
if (nodeIds[i] == null || nodeIds[i].equals(""))
{
continue;
}
dictionary row = dataTableGetRow(table, i);
conversation_node node = new conversation_node();
node.nodeId = getString(row, "nodeId");
node.textKey = getString(row, "nodeText");
node.conditionType = getString(row, "nodeCondition");
node.conditionArg1 = getString(row, "conditionArg1");
node.conditionArg2 = getString(row, "conditionArg2");
node.priority = getInt(row, "priority");
node.startNode = getBoolean(row, "startNode");
node.endNode = getBoolean(row, "endNode");
node.enterActionSet = getString(row, "enterActionSet");
nodes[index++] = node;
}
return nodes;
}
public conversation_response[] loadResponses(String table) throws InterruptedException
{
String[] responseIds = dataTableGetStringColumn(table, "responseId");
if (responseIds == null)
{
return new conversation_response[0];
}
int count = 0;
for (String responseId : responseIds)
{
if (responseId != null && !responseId.equals(""))
{
++count;
}
}
conversation_response[] responses = new conversation_response[count];
int index = 0;
for (int i = 0; i < responseIds.length; ++i)
{
if (responseIds[i] == null || responseIds[i].equals(""))
{
continue;
}
dictionary row = dataTableGetRow(table, i);
conversation_response response = new conversation_response();
response.responseId = getString(row, "responseId");
response.nodeId = getString(row, "responseNode");
response.textKey = getString(row, "responseText");
response.conditionType = getString(row, "responseCondition");
response.conditionArg1 = getString(row, "responseConditionArg1");
response.conditionArg2 = getString(row, "responseConditionArg2");
response.actionSet = getString(row, "actionSet");
response.nextNodeId = getString(row, "nextNode");
response.endConversation = getBoolean(row, "endConversation");
response.endTextKey = getString(row, "endText");
responses[index++] = response;
}
return responses;
}
public conversation_action[] loadActions(String table) throws InterruptedException
{
String[] actionSets = dataTableGetStringColumn(table, "actionSetName");
if (actionSets == null)
{
return new conversation_action[0];
}
int count = 0;
for (String actionSet : actionSets)
{
if (actionSet != null && !actionSet.equals(""))
{
++count;
}
}
conversation_action[] actions = new conversation_action[count];
int index = 0;
for (int i = 0; i < actionSets.length; ++i)
{
if (actionSets[i] == null || actionSets[i].equals(""))
{
continue;
}
dictionary row = dataTableGetRow(table, i);
conversation_action action = new conversation_action();
action.actionSet = getString(row, "actionSetName");
action.order = getInt(row, "actionOrder");
action.actionType = getString(row, "actionType");
action.arg1 = getString(row, "actionArg1");
action.arg2 = getString(row, "actionArg2");
action.arg3 = getString(row, "actionArg3");
actions[index++] = action;
}
return actions;
}
public String getStringCell(String table, int row, String column) throws InterruptedException
{
String value = dataTableGetString(table, row, column);
return value == null ? "" : value;
}
public String getString(dictionary row, String key) throws InterruptedException
{
String value = row.getString(key);
return value == null ? "" : value;
}
public int getInt(dictionary row, String key) throws InterruptedException
{
return row.getInt(key);
}
public boolean getBoolean(dictionary row, String key) throws InterruptedException
{
String value = getString(row, key);
return value.equals("1") || value.equals("true") || value.equals("yes");
}
}
@@ -0,0 +1,14 @@
package script.systems.conversation_manager;
public class conversation_node
{
public String nodeId;
public String textKey;
public String conditionType;
public String conditionArg1;
public String conditionArg2;
public int priority;
public boolean startNode;
public boolean endNode;
public String enterActionSet;
}
@@ -0,0 +1,15 @@
package script.systems.conversation_manager;
public class conversation_response
{
public String responseId;
public String nodeId;
public String textKey;
public String conditionType;
public String conditionArg1;
public String conditionArg2;
public String actionSet;
public String nextNodeId;
public boolean endConversation;
public String endTextKey;
}
@@ -0,0 +1,67 @@
package script.systems.conversation_manager;
import script.menu_info;
import script.menu_info_data;
import script.menu_info_types;
import script.obj_id;
import script.string_id;
public class table_conversation extends script.base_script
{
public table_conversation()
{
}
protected conversation_loader loader = new conversation_loader();
protected conversation_engine engine = new conversation_engine();
public int OnInitialize(obj_id self) throws InterruptedException
{
if ((!isTangible(self)) || (isPlayer(self)))
{
detachScript(self, "systems.conversation_manager.table_conversation");
return SCRIPT_CONTINUE;
}
setCondition(self, CONDITION_CONVERSABLE);
return SCRIPT_CONTINUE;
}
public int OnAttach(obj_id self) throws InterruptedException
{
setCondition(self, CONDITION_CONVERSABLE);
return SCRIPT_CONTINUE;
}
public int OnObjectMenuRequest(obj_id self, obj_id player, menu_info menuInfo) throws InterruptedException
{
int menu = menuInfo.addRootMenu(menu_info_types.CONVERSE_START, null);
menu_info_data menuInfoData = menuInfo.getMenuItemById(menu);
menuInfoData.setServerNotify(false);
setCondition(self, CONDITION_CONVERSABLE);
faceTo(self, player);
return SCRIPT_CONTINUE;
}
public int OnIncapacitated(obj_id self, obj_id killer) throws InterruptedException
{
clearCondition(self, CONDITION_CONVERSABLE);
detachScript(self, "systems.conversation_manager.table_conversation");
return SCRIPT_CONTINUE;
}
public int OnStartNpcConversation(obj_id self, obj_id player) throws InterruptedException
{
conversation_definition definition = loader.load(self);
return engine.startConversation(self, player, definition);
}
public int OnNpcConversationResponse(obj_id self, String conversationId, obj_id player, string_id response) throws InterruptedException
{
if (!conversationId.equals(conversation_engine.CONVERSATION_NAME))
{
return SCRIPT_CONTINUE;
}
conversation_definition definition = loader.load(self);
return engine.continueConversation(self, player, response, definition);
}
}