- --[[
- * DYNAMIC GOSSIP
- * - VERSION 1.0
- * - AUTHOR Grandelf
- *
- * This script allows you to dynamically build gossip
- * options, without having to worry about linking the
- * intid's or pagination (next and back buttons).
- * Furthermore is it more efficient doing it this way,
- * and in my opinion a lot clearer as well.
- *
- * Table structure, '[]' means optional:
- *
- * local actions = {
- * ['menu_name'] = {
- * ['sub_menu'] = function(player, object)
- * player:DoAction();
- * end,
- * ['sub_menu2'] = {
- * function(player, object)
- * player:DoAction();
- * end,
- * ['check'] = function(player, object)
- * return expr;
- * end
- * },
- * ['check'] = [{]
- * function(player, object)
- * return expr;
- * end[,
- * -- Add more checks if you like.
- * }]
- * },
- * [_next] = false,
- * [_prev] = false
- * };
- ]]
- local stored_options = {
- ['Creature'] = {},
- ['GameObject'] = {},
- ['Item'] = {}
- };
- -- Default config
- local DEFAULT_NEXT = 10 + 1;
- local DEFAULT_BACK = true;
- -- Next and back variables
- local next, back;
- --[[
- * Custom iterator, used to loop over a 'map',
- * ordered alfabetically.
- *
- * @param t Table value
- *
- * @return Iterator function.
- ]]
- local function ordered_pairs(t)
- local sorted_names = {};
- for name in pairs(t) do
- table.insert(sorted_names, name);
- end
- table.sort(sorted_names);
- local coro = coroutine.create(function()
- for _, name in ipairs(sorted_names) do
- coroutine.yield(name, t[name]);
- end
- end);
- return function()
- return select(2, coroutine.resume(coro));
- end
- end
- --[[
- * Function that will check if the gossip option,
- * must be shown to the player.
- *
- * @param check Either a function or a table of functions.
- * @param player Player userdata
- * @param object Creature, Item or Gameobject userdata.
- *
- * @return boolean indicating if the check(s) passed.
- ]]
- local function check_conditions(check, player, object)
- if type(check) == 'function' then
- return check(player, object);
- elseif type(check) == 'table' then
- for _, func in pairs(check) do
- if not func(player, object) then return false; end
- end
- end
- return true;
- end
- --[[
- * Function that will send a gossip menu to the player,
- * based on a couple of variables.
- *
- * @param player Player userdata
- * @param object Creature, Item or Gameobject userdata
- * @param options Gossip options that must be shown.
- * @param parent The parent of the current menu.
- ]]
- local function sendGossipMenu(player, object, options, parent)
- local parent = parent or 0;
- for menu_id, options in ipairs(options) do
- if options.parent == parent and check_conditions(options.check, player, object) then
- player:GossipMenuAddItem(0, options.name, 0, menu_id);
- end
- end
- player:GossipSendMenu(1, object);
- end
- --[[
- * ON_GOSSIP_HELLO event.
- * This function will create the main menu,
- * basically all the options without a parent.
- ]]
- local function onGossipHello(_, player, object)
- sendGossipMenu(player, object, stored_options[object:GetObjectType()][object:GetEntry()]);
- end
- --[[
- * ON_GOSSIP_SELECT event.
- * This function will create a menu based on the option that was clicked.
- * For example, if an option with id 2 was clicked, it will select
- * all options that have id 2 as a parent.
- * If there are no further options however, but there is an action (function)
- * it will execute that action.
- ]]
- local function onGossipSelect(_, player, object, _, intid)
- local gossip_options = stored_options[object:GetObjectType()][object:GetEntry()];
- if type(gossip_options[intid].callback) ~= 'function' then
- sendGossipMenu(player, object, gossip_options, intid);
- else -- Reached an action.
- player:GossipComplete();
- gossip_options[intid].callback(player, object);
- end
- end
- --[[
- * Function to check if an individual option, has a check.
- *
- * @param Option table.
- *
- * @return Boolean, indicating if it's a check.
- * @return Function The action function
- * @return Function The check function.
- ]]
- local function is_action_check(option)
- local action_func, chk_func, items = false, nil, 0;
- for key, val in pairs(option) do
- if key == 'check' then
- chk_func = val;
- elseif type(val) == 'function' then
- action_func = val;
- end
- items = items + 1;
- end
- if action_func and chk_func and items == 2 then
- return true, action_func, chk_func;
- end
- return false;
- end
- --[[
- * Table containing the process functions.
- ]]
- local process = {};
- --[[
- * Function that will produce an error report,
- * and print it to the console.
- *
- * @param name Name of the wrong gossip option.
- * @param val Value of the wrong gossip option.
- ]]
- function process:report_error(name, val)
- local error_msg = '\n'
- .. 'Error in gossip table structure:\n'
- .. '["' .. name .. '"] = {\n'
- .. ' ' .. val .. '\n'
- .. '}\n'
- .. 'Value "' .. val .. '" is a ' .. type(val) .. ' value, '
- .. 'expected a table or function value.';
- error(error_msg, 5 + self._iteration_level);
- end
- --[[
- * Function that will keep track whether to make a next button
- * or not. Partly done, to make the process function smaller.
- *
- * @param parent Will be used as the key for the data.
- *
- * @return A boolean value, indicating whether a next button is needed.
- ]]
- function process:next_btn()
- local parent = self._parent;
- if self._next[parent] == nil then
- self._next[parent] = 1;
- else
- self._next[parent] = self._next[parent] + 1;
- end
- return self._next[parent] % next == 0;
- end
- --[[
- * Function that will create a back button, for the specified menu.
- ]]
- function process:create_back_button()
- self._menu_id = self._menu_id + 1;
- local parent = self._parent;
- self[self._menu_id] = {
- parent = parent,
- name = '[back]',
- callback = function(player, object)
- sendGossipMenu(player, object, stored_options[object:GetObjectType()][object:GetEntry()], self[parent].parent);
- end
- };
- end
- --[[
- * Function that will process the table of a single 'row'.
- *
- * @param name The name of the current option.
- * @param value The value of the current option.
- ]]
- function process:process_table(name, val)
- local parent = self._parent;
- local menu_id = self._menu_id;
- local iteration_level = self._iteration_level;
- local is_check, callback, chk_func = is_action_check(val);
- if is_check then
- self[menu_id] = {
- parent = parent,
- name = name,
- callback = callback,
- check = chk_func
- };
- else
- self[menu_id] = {
- parent = parent,
- name = name
- };
- self._parent = menu_id;
- self._iteration_level = iteration_level + 1;
- self:process_options(val, self);
- self._parent = parent;
- self._iteration_level = iteration_level;
- end
- end
- --[[
- * Function that will process a single 'row' of the table.
- *
- * @param options The user made table, which has to be converted.
- * @param name The name of the current option.
- * @param value The value of the current option.
- ]]
- function process:process_option(options, name, val)
- if name ~= 'check' then
- self._menu_id = self._menu_id + 1;
- end
- local parent = self._parent;
- local menu_id = self._menu_id;
- local iteration_level = self._iteration_level;
- if name:lower() == 'check' then
- self[parent].check = val;
- elseif self:next_btn() then
- self:process_table('[next]', options);
- return true;
- elseif type(val) == 'function' then
- self[menu_id] = {
- parent = parent,
- name = name,
- callback = val
- };
- elseif type(val) == 'table' then
- self:process_table(name, val);
- else
- self:report_error(name, val);
- end
- options[name] = nil;
- end
- --[[
- * Function that will convert a user made table, into a table
- * which can be used by this script.
- *
- * @param options The user made table, which has to be converted.
- * @param obj The newly constructed table, ONLY FOR RECURSIVE USE.
- *
- * @return The new table which can be used by this script.
- ]]
- function process:process_options(options, obj)
- local obj = obj or setmetatable({
- _menu_id = 0,
- _parent = 0,
- _iteration_level = 1,
- _next = {}
- }, {
- __index = self
- });
- for name, val in ordered_pairs(options) do
- local bool = obj:process_option(options, name, val);
- if bool then
- break;
- end
- end
- if obj._parent ~= 0 and back then
- obj:create_back_button();
- end
- if obj._iteration_level == 1 then
- obj._menu_id, obj._parent, obj._iteration_level, obj._next =
- nil, nil, nil, nil;
- end
- return obj;
- end
- --[[
- * Function for easy assertion, I'd rather do it with 'error'.
- *
- * @param expr The expression to evaluate.
- * @param msg The error message.
- * @param level The stack level we're currently on.
- ]]
- local function assert(expr, msg, level)
- if not expr then
- error(msg, level);
- end
- end
- --[[
- * Global function that will execute the necessary functions to create
- * a gossip option table and register the needed gossip events.
- *
- * @param creature_id The object id the gossip applies to.
- * @param options A table containing the gossip options and actions.
- ]]
- local function RegisterDynamicGossipEvent(object_id, options)
- local def_back, def_next = options._back, options._next;
- if def_back ~= nil then
- assert(type(def_back) == 'boolean',
- '[_back]: Boolean expected, got ' .. type(def_back), 4);
- back = def_back;
- else
- back = DEFAULT_BACK;
- end
- if def_next ~= nil then
- assert(type(def_next) == 'boolean' or type(def_next) == 'number',
- '[_next]: Boolean or number expected, got ' .. type(def_next), 4);
- if not def_next or def_next <= 0 then
- next = 0;
- else
- next = def_next + 1;
- end
- else
- next = DEFAULT_NEXT;
- end
- options._next, options._back = nil, nil;
- return process:process_options(options);
- end
- --[[
- * Table that holds the cancel methods.
- * Put inside a different table, because it is rarely used anyway.
- ]]
- local cancel_methods = {
- ['Item'] = {},
- ['Creature'] = {},
- ['GameObject'] = {}
- };
- --[[
- * Function that will call the cancel methods, if they exist.
- *
- * @param object_type The type of the world object.
- * @param object_id The id of the world object.
- ]]
- local function cancel(object_type, object_id)
- local methods = cancel_methods[object_type][object_id];
- if methods == nil then
- return;
- end
- for _, cancel_method in pairs(methods) do
- cancel_method();
- end
- end
- --[[
- * The individual register functions.
- ]]
- function RegisterDynamicItemGossipEvent(item_id, options)
- stored_options['Item'][item_id] = RegisterDynamicGossipEvent(item_id, options);
- cancel_methods['Item'][creature_id] = {
- RegisterItemGossipEvent(item_id, 1, onGossipHello),
- RegisterItemGossipEvent(item_id, 2, onGossipSelect)
- };
- return function()
- cancel('Item', item_id);
- end
- end
- function RegisterDynamicCreatureGossipEvent(creature_id, options)
- stored_options['Creature'][creature_id] = RegisterDynamicGossipEvent(creature_id, options);
- cancel_methods['Creature'][creature_id] = {
- RegisterCreatureGossipEvent(creature_id, 1, onGossipHello),
- RegisterCreatureGossipEvent(creature_id, 2, onGossipSelect)
- };
- return function()
- cancel('Creature', creature_id);
- end
- end
- function RegisterDynamicGameObjectGossipEvent(gameobject_id, options)
- stored_options['GameObject'][gameobject_id] = RegisterDynamicGossipEvent(gameobject_id, options);
- cancel_methods['GameObject'][creature_id] = {
- RegisterGameObjectGossipEvent(gameobject_id, 1, onGossipHello),
- RegisterGameObjectGossipEvent(gameobject_id, 2, onGossipSelect);
- };
- return function()
- cancel('GameObject', gameobject_id);
- end
- end