1. --[[
  2. * DYNAMIC GOSSIP
  3. * - VERSION 1.0
  4. * - AUTHOR Grandelf
  5. *
  6. * This script allows you to dynamically build gossip
  7. * options, without having to worry about linking the
  8. * intid's or pagination (next and back buttons).
  9. * Furthermore is it more efficient doing it this way,
  10. * and in my opinion a lot clearer as well.
  11. *
  12. * Table structure, '[]' means optional:
  13. *
  14. * local actions = {
  15. * ['menu_name'] = {
  16. * ['sub_menu'] = function(player, object)
  17. * player:DoAction();
  18. * end,
  19. * ['sub_menu2'] = {
  20. * function(player, object)
  21. * player:DoAction();
  22. * end,
  23. * ['check'] = function(player, object)
  24. * return expr;
  25. * end
  26. * },
  27. * ['check'] = [{]
  28. * function(player, object)
  29. * return expr;
  30. * end[,
  31. * -- Add more checks if you like.
  32. * }]
  33. * },
  34. * [_next] = false,
  35. * [_prev] = false
  36. * };
  37. ]]
  38. local stored_options = {
  39. ['Creature'] = {},
  40. ['GameObject'] = {},
  41. ['Item'] = {}
  42. };
  43. -- Default config
  44. local DEFAULT_NEXT = 10 + 1;
  45. local DEFAULT_BACK = true;
  46. -- Next and back variables
  47. local next, back;
  48. --[[
  49. * Custom iterator, used to loop over a 'map',
  50. * ordered alfabetically.
  51. *
  52. * @param t Table value
  53. *
  54. * @return Iterator function.
  55. ]]
  56. local function ordered_pairs(t)
  57. local sorted_names = {};
  58. for name in pairs(t) do
  59. table.insert(sorted_names, name);
  60. end
  61. table.sort(sorted_names);
  62. local coro = coroutine.create(function()
  63. for _, name in ipairs(sorted_names) do
  64. coroutine.yield(name, t[name]);
  65. end
  66. end);
  67. return function()
  68. return select(2, coroutine.resume(coro));
  69. end
  70. end
  71. --[[
  72. * Function that will check if the gossip option,
  73. * must be shown to the player.
  74. *
  75. * @param check Either a function or a table of functions.
  76. * @param player Player userdata
  77. * @param object Creature, Item or Gameobject userdata.
  78. *
  79. * @return boolean indicating if the check(s) passed.
  80. ]]
  81. local function check_conditions(check, player, object)
  82. if type(check) == 'function' then
  83. return check(player, object);
  84. elseif type(check) == 'table' then
  85. for _, func in pairs(check) do
  86. if not func(player, object) then return false; end
  87. end
  88. end
  89. return true;
  90. end
  91. --[[
  92. * Function that will send a gossip menu to the player,
  93. * based on a couple of variables.
  94. *
  95. * @param player Player userdata
  96. * @param object Creature, Item or Gameobject userdata
  97. * @param options Gossip options that must be shown.
  98. * @param parent The parent of the current menu.
  99. ]]
  100. local function sendGossipMenu(player, object, options, parent)
  101. local parent = parent or 0;
  102. for menu_id, options in ipairs(options) do
  103. if options.parent == parent and check_conditions(options.check, player, object) then
  104. player:GossipMenuAddItem(0, options.name, 0, menu_id);
  105. end
  106. end
  107. player:GossipSendMenu(1, object);
  108. end
  109. --[[
  110. * ON_GOSSIP_HELLO event.
  111. * This function will create the main menu,
  112. * basically all the options without a parent.
  113. ]]
  114. local function onGossipHello(_, player, object)
  115. sendGossipMenu(player, object, stored_options[object:GetObjectType()][object:GetEntry()]);
  116. end
  117. --[[
  118. * ON_GOSSIP_SELECT event.
  119. * This function will create a menu based on the option that was clicked.
  120. * For example, if an option with id 2 was clicked, it will select
  121. * all options that have id 2 as a parent.
  122. * If there are no further options however, but there is an action (function)
  123. * it will execute that action.
  124. ]]
  125. local function onGossipSelect(_, player, object, _, intid)
  126. local gossip_options = stored_options[object:GetObjectType()][object:GetEntry()];
  127. if type(gossip_options[intid].callback) ~= 'function' then
  128. sendGossipMenu(player, object, gossip_options, intid);
  129. else -- Reached an action.
  130. player:GossipComplete();
  131. gossip_options[intid].callback(player, object);
  132. end
  133. end
  134. --[[
  135. * Function to check if an individual option, has a check.
  136. *
  137. * @param Option table.
  138. *
  139. * @return Boolean, indicating if it's a check.
  140. * @return Function The action function
  141. * @return Function The check function.
  142. ]]
  143. local function is_action_check(option)
  144. local action_func, chk_func, items = false, nil, 0;
  145. for key, val in pairs(option) do
  146. if key == 'check' then
  147. chk_func = val;
  148. elseif type(val) == 'function' then
  149. action_func = val;
  150. end
  151. items = items + 1;
  152. end
  153. if action_func and chk_func and items == 2 then
  154. return true, action_func, chk_func;
  155. end
  156. return false;
  157. end
  158. --[[
  159. * Table containing the process functions.
  160. ]]
  161. local process = {};
  162. --[[
  163. * Function that will produce an error report,
  164. * and print it to the console.
  165. *
  166. * @param name Name of the wrong gossip option.
  167. * @param val Value of the wrong gossip option.
  168. ]]
  169. function process:report_error(name, val)
  170. local error_msg = '\n'
  171. .. 'Error in gossip table structure:\n'
  172. .. '["' .. name .. '"] = {\n'
  173. .. ' ' .. val .. '\n'
  174. .. '}\n'
  175. .. 'Value "' .. val .. '" is a ' .. type(val) .. ' value, '
  176. .. 'expected a table or function value.';
  177. error(error_msg, 5 + self._iteration_level);
  178. end
  179. --[[
  180. * Function that will keep track whether to make a next button
  181. * or not. Partly done, to make the process function smaller.
  182. *
  183. * @param parent Will be used as the key for the data.
  184. *
  185. * @return A boolean value, indicating whether a next button is needed.
  186. ]]
  187. function process:next_btn()
  188. local parent = self._parent;
  189. if self._next[parent] == nil then
  190. self._next[parent] = 1;
  191. else
  192. self._next[parent] = self._next[parent] + 1;
  193. end
  194. return self._next[parent] % next == 0;
  195. end
  196. --[[
  197. * Function that will create a back button, for the specified menu.
  198. ]]
  199. function process:create_back_button()
  200. self._menu_id = self._menu_id + 1;
  201. local parent = self._parent;
  202. self[self._menu_id] = {
  203. parent = parent,
  204. name = '[back]',
  205. callback = function(player, object)
  206. sendGossipMenu(player, object, stored_options[object:GetObjectType()][object:GetEntry()], self[parent].parent);
  207. end
  208. };
  209. end
  210. --[[
  211. * Function that will process the table of a single 'row'.
  212. *
  213. * @param name The name of the current option.
  214. * @param value The value of the current option.
  215. ]]
  216. function process:process_table(name, val)
  217. local parent = self._parent;
  218. local menu_id = self._menu_id;
  219. local iteration_level = self._iteration_level;
  220. local is_check, callback, chk_func = is_action_check(val);
  221. if is_check then
  222. self[menu_id] = {
  223. parent = parent,
  224. name = name,
  225. callback = callback,
  226. check = chk_func
  227. };
  228. else
  229. self[menu_id] = {
  230. parent = parent,
  231. name = name
  232. };
  233. self._parent = menu_id;
  234. self._iteration_level = iteration_level + 1;
  235. self:process_options(val, self);
  236. self._parent = parent;
  237. self._iteration_level = iteration_level;
  238. end
  239. end
  240. --[[
  241. * Function that will process a single 'row' of the table.
  242. *
  243. * @param options The user made table, which has to be converted.
  244. * @param name The name of the current option.
  245. * @param value The value of the current option.
  246. ]]
  247. function process:process_option(options, name, val)
  248. if name ~= 'check' then
  249. self._menu_id = self._menu_id + 1;
  250. end
  251. local parent = self._parent;
  252. local menu_id = self._menu_id;
  253. local iteration_level = self._iteration_level;
  254. if name:lower() == 'check' then
  255. self[parent].check = val;
  256. elseif self:next_btn() then
  257. self:process_table('[next]', options);
  258. return true;
  259. elseif type(val) == 'function' then
  260. self[menu_id] = {
  261. parent = parent,
  262. name = name,
  263. callback = val
  264. };
  265. elseif type(val) == 'table' then
  266. self:process_table(name, val);
  267. else
  268. self:report_error(name, val);
  269. end
  270. options[name] = nil;
  271. end
  272. --[[
  273. * Function that will convert a user made table, into a table
  274. * which can be used by this script.
  275. *
  276. * @param options The user made table, which has to be converted.
  277. * @param obj The newly constructed table, ONLY FOR RECURSIVE USE.
  278. *
  279. * @return The new table which can be used by this script.
  280. ]]
  281. function process:process_options(options, obj)
  282. local obj = obj or setmetatable({
  283. _menu_id = 0,
  284. _parent = 0,
  285. _iteration_level = 1,
  286. _next = {}
  287. }, {
  288. __index = self
  289. });
  290. for name, val in ordered_pairs(options) do
  291. local bool = obj:process_option(options, name, val);
  292. if bool then
  293. break;
  294. end
  295. end
  296. if obj._parent ~= 0 and back then
  297. obj:create_back_button();
  298. end
  299. if obj._iteration_level == 1 then
  300. obj._menu_id, obj._parent, obj._iteration_level, obj._next =
  301. nil, nil, nil, nil;
  302. end
  303. return obj;
  304. end
  305. --[[
  306. * Function for easy assertion, I'd rather do it with 'error'.
  307. *
  308. * @param expr The expression to evaluate.
  309. * @param msg The error message.
  310. * @param level The stack level we're currently on.
  311. ]]
  312. local function assert(expr, msg, level)
  313. if not expr then
  314. error(msg, level);
  315. end
  316. end
  317. --[[
  318. * Global function that will execute the necessary functions to create
  319. * a gossip option table and register the needed gossip events.
  320. *
  321. * @param creature_id The object id the gossip applies to.
  322. * @param options A table containing the gossip options and actions.
  323. ]]
  324. local function RegisterDynamicGossipEvent(object_id, options)
  325. local def_back, def_next = options._back, options._next;
  326. if def_back ~= nil then
  327. assert(type(def_back) == 'boolean',
  328. '[_back]: Boolean expected, got ' .. type(def_back), 4);
  329. back = def_back;
  330. else
  331. back = DEFAULT_BACK;
  332. end
  333. if def_next ~= nil then
  334. assert(type(def_next) == 'boolean' or type(def_next) == 'number',
  335. '[_next]: Boolean or number expected, got ' .. type(def_next), 4);
  336. if not def_next or def_next <= 0 then
  337. next = 0;
  338. else
  339. next = def_next + 1;
  340. end
  341. else
  342. next = DEFAULT_NEXT;
  343. end
  344. options._next, options._back = nil, nil;
  345. return process:process_options(options);
  346. end
  347. --[[
  348. * Table that holds the cancel methods.
  349. * Put inside a different table, because it is rarely used anyway.
  350. ]]
  351. local cancel_methods = {
  352. ['Item'] = {},
  353. ['Creature'] = {},
  354. ['GameObject'] = {}
  355. };
  356. --[[
  357. * Function that will call the cancel methods, if they exist.
  358. *
  359. * @param object_type The type of the world object.
  360. * @param object_id The id of the world object.
  361. ]]
  362. local function cancel(object_type, object_id)
  363. local methods = cancel_methods[object_type][object_id];
  364. if methods == nil then
  365. return;
  366. end
  367. for _, cancel_method in pairs(methods) do
  368. cancel_method();
  369. end
  370. end
  371. --[[
  372. * The individual register functions.
  373. ]]
  374. function RegisterDynamicItemGossipEvent(item_id, options)
  375. stored_options['Item'][item_id] = RegisterDynamicGossipEvent(item_id, options);
  376. cancel_methods['Item'][creature_id] = {
  377. RegisterItemGossipEvent(item_id, 1, onGossipHello),
  378. RegisterItemGossipEvent(item_id, 2, onGossipSelect)
  379. };
  380. return function()
  381. cancel('Item', item_id);
  382. end
  383. end
  384. function RegisterDynamicCreatureGossipEvent(creature_id, options)
  385. stored_options['Creature'][creature_id] = RegisterDynamicGossipEvent(creature_id, options);
  386. cancel_methods['Creature'][creature_id] = {
  387. RegisterCreatureGossipEvent(creature_id, 1, onGossipHello),
  388. RegisterCreatureGossipEvent(creature_id, 2, onGossipSelect)
  389. };
  390. return function()
  391. cancel('Creature', creature_id);
  392. end
  393. end
  394. function RegisterDynamicGameObjectGossipEvent(gameobject_id, options)
  395. stored_options['GameObject'][gameobject_id] = RegisterDynamicGossipEvent(gameobject_id, options);
  396. cancel_methods['GameObject'][creature_id] = {
  397. RegisterGameObjectGossipEvent(gameobject_id, 1, onGossipHello),
  398. RegisterGameObjectGossipEvent(gameobject_id, 2, onGossipSelect);
  399. };
  400. return function()
  401. cancel('GameObject', gameobject_id);
  402. end
  403. end