formspecs.lua 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791
  1. local S = minetest.get_translator(minetest.get_current_modname())
  2. local truncate_item_names_to = 30
  3. -- Large textures can screw with the formspecs.
  4. -- See https://github.com/minetest/minetest/issues/9300 for a feature request that would simplify and improve icon generation, if supported.
  5. -- In the meantime, here's some methods for overriding item icons to manually work around this:
  6. local override_item_icon = {}
  7. commoditymarket.override_item_icon = function(item_name, new_icon_texture)
  8. override_item_icon[item_name] = new_icon_texture
  9. end
  10. local override_image_icon = {}
  11. commoditymarket.override_image_icon = function(old_icon_texture, new_icon_texture)
  12. override_image_icon[old_icon_texture] = new_icon_texture
  13. end
  14. -- And a setting for disabling icons entirely:
  15. local global_enable_item_icons = minetest.settings:get_bool("commoditymarket_enable_item_icons", true)
  16. --[inventorycube{<top>{<left>{<right>
  17. --Escaping does not apply here and `^` is replaced by `&` in texture names instead.
  18. --Example:
  19. -- [inventorycube{grass.png{dirt.png&grass_side.png{dirt.png&grass_side.png
  20. --Creates an inventorycube with `grass.png`, `dirt.png^grass_side.png` and `dirt.png^grass_side.png` textures
  21. local process_inventory_cube = function(texture_string)
  22. if not texture_string:sub(1,14) == "[inventorycube" then
  23. return texture_string
  24. end
  25. local split = texture_string:split("{")
  26. local left = split[3] -- the "front" of the cube we're seeing in the inventory list
  27. if left == nil then -- in case something weird happens, don't crash.
  28. return texture_string
  29. end
  30. left = left:gsub("&", "^")
  31. return left
  32. end
  33. local get_icon = function(item)
  34. local def = minetest.registered_items[item]
  35. local returnstring = "unknown_item.png"
  36. if def == nil then
  37. return returnstring
  38. end
  39. local override = override_item_icon[item]
  40. if override then
  41. return override
  42. end
  43. local inventory_image = def.inventory_image
  44. if inventory_image and inventory_image ~= "" then
  45. returnstring = inventory_image
  46. else
  47. local tiles = def.tiles
  48. if tiles then
  49. local tilecount = #tiles
  50. -- Textures of node; +Y, -Y, +X, -X, +Z, -Z
  51. local selected_tile = tiles[math.min(5,tilecount)]
  52. if type(selected_tile) == "string" then
  53. returnstring = selected_tile
  54. else
  55. local tile_name = selected_tile.name
  56. if tile_name then
  57. returnstring = tile_name
  58. end
  59. end
  60. end
  61. end
  62. returnstring = process_inventory_cube(returnstring)
  63. -- Formspec tables can't handle image compositing and modifiers
  64. local found_caret = returnstring:find("%^")
  65. if found_caret then
  66. returnstring = returnstring:sub(1, found_caret-1)
  67. end
  68. override = override_image_icon[returnstring]
  69. if override then
  70. return override
  71. end
  72. return minetest.formspec_escape(returnstring)
  73. end
  74. -- Exposed so that the purge_unknowns command can use it.
  75. commoditymarket.get_icon = get_icon
  76. local truncate_string = function(target, length)
  77. if target:len() > length then
  78. return target:sub(1,length-2).."..."
  79. end
  80. return target
  81. end
  82. local get_item_description = function(item)
  83. local def = minetest.registered_items[item]
  84. if def then
  85. local description = def.description
  86. if description then
  87. return minetest.formspec_escape(description:gsub("\n", " "))
  88. end
  89. end
  90. return S("Unknown Item")
  91. end
  92. -- Inventory formspec
  93. -------------------------------------------------------------------------------------
  94. local inventory_item_comp = function(invitem1, invitem2) return invitem1.item < invitem2.item end
  95. local inventory_desc_comp = function(invitem1, invitem2) return invitem1.description < invitem2.description end
  96. local get_account_formspec = function(market, account)
  97. local show_itemnames = account.show_itemnames == "true"
  98. local show_icons = global_enable_item_icons and ((account.show_icons or "true") == "true")
  99. local market_def = market.def
  100. local inventory = {}
  101. local inventory_count = 0
  102. for item, quantity in pairs(account.inventory) do
  103. local icon
  104. if show_icons then
  105. icon = get_icon(item)
  106. end
  107. table.insert(inventory, {item=item, quantity=quantity, icon=icon, description=get_item_description(item)})
  108. inventory_count = inventory_count + quantity
  109. end
  110. if show_itemnames then
  111. table.sort(inventory, inventory_item_comp)
  112. else
  113. table.sort(inventory, inventory_desc_comp)
  114. end
  115. local formspec = {
  116. "size[10,10]"
  117. .."tabheader[0,0;tabs;"..market_def.description..","..S("Your Inventory")..","..S("Market Orders")..";2;false;true]"
  118. }
  119. formspec[#formspec+1] = "tablecolumns["
  120. if show_icons then
  121. formspec[#formspec+1] = "image"
  122. for i=1, #inventory, 2 do
  123. formspec[#formspec+1] = ","..i.."="..inventory[i].icon
  124. end
  125. formspec[#formspec+1] = ";"
  126. end
  127. if show_itemnames then
  128. formspec[#formspec+1] = "text;"
  129. end
  130. formspec[#formspec+1] = "text;text,align=center"
  131. if show_icons then
  132. formspec[#formspec+1] = ";image"
  133. for i=2, #inventory, 2 do
  134. formspec[#formspec+1] = ","..i.."="..inventory[i].icon
  135. end
  136. end
  137. if show_itemnames then
  138. formspec[#formspec+1] = ";text"
  139. end
  140. formspec[#formspec+1] = ";text;text,align=center]"
  141. .."tooltip[inventory;"..S("All the items you've transfered to the market to sell and the items you've\npurchased with buy orders. Double-click on an item to bring it back into your\npersonal inventory.").."]"
  142. .."table[0,0;9.75,4;inventory;"
  143. if show_icons then
  144. formspec[#formspec+1] = "0,"
  145. end
  146. if show_itemnames then
  147. formspec[#formspec+1] = S("Item")..","
  148. end
  149. formspec[#formspec+1] = S("Description")..","..S("Quantity")
  150. if show_icons then
  151. formspec[#formspec+1] = ",0"
  152. end
  153. if show_itemnames then
  154. formspec[#formspec+1] = ","..S("Item")
  155. end
  156. formspec[#formspec+1] = ","..S("Description")..","..S("Quantity")
  157. for i, entry in ipairs(inventory) do
  158. if show_icons then
  159. formspec[#formspec+1] = "," .. i
  160. end
  161. if show_itemnames then
  162. formspec[#formspec+1] = "," .. truncate_string(entry.item, truncate_item_names_to)
  163. end
  164. -- no need to formspec_escape description here, it gets done when it's initially added to the inventory table
  165. formspec[#formspec+1] = "," .. entry.description .. "," .. entry.quantity
  166. end
  167. formspec[#formspec+1] = "]container[1,4.5]list[detached:commoditymarket:" .. market.name .. ";add;0,0;1,1;]"
  168. .."label[1,0;"..S("Drop items here to\nadd to your account").."]"
  169. .."listring[current_player;main]listring[detached:commoditymarket:" .. market.name .. ";add]"
  170. if market_def.inventory_limit then
  171. formspec[#formspec+1] = "label[3,0;"..S("Inventory limit:").."\n" .. inventory_count.."/" .. market_def.inventory_limit .. "]"
  172. .. "tooltip[3,0;1.5,1;"..S("You can still receive purchased items if you've exceeded your inventory limit,\nbut you won't be able to transfer items from your personal inventory into\nthe market until you've emptied it back down below the limit again.").."]"
  173. end
  174. formspec[#formspec+1] = "label[4.9,0;"..S("Balance:") .. "\n" .. market_def.currency_symbol .. account.balance .. "]"
  175. .."tooltip[4.9,0;3.5,1;"..S("Enter the amount of currency you'd like to withdraw then click the 'Withdraw'\nbutton to convert it into items and transfer it to your personal inventory.").."]"
  176. .."field[6.1,0.325;1,1;withdrawamount;;]"
  177. .."field_close_on_enter[withdrawamount;false]"
  178. .."button[6.7,0;1.2,1;withdraw;"..S("Withdraw").."]"
  179. .."container_end[]"
  180. .."container[1,5.75]list[current_player;main;0,0;8,1;]"
  181. .."list[current_player;main;0,1.25;8,3;8]container_end[]"
  182. return table.concat(formspec)
  183. end
  184. -- Market formspec
  185. --------------------------------------------------------------------------------------------------------
  186. local compare_market_item = function(mkt1, mkt2)
  187. return mkt1.item < mkt2.item
  188. end
  189. local compare_market_desc = function(mkt1, mkt2)
  190. -- TODO: see https://github.com/minetest/minetest/issues/8398 for sorting localized strings
  191. return get_item_description(mkt1.item) < get_item_description(mkt2.item)
  192. end
  193. local compare_buy_volume = function(mkt1, mkt2)
  194. return mkt1.buy_volume > mkt2.buy_volume
  195. end
  196. local compare_buy_max = function(mkt1, mkt2)
  197. return ((mkt1.buy_orders[#mkt1.buy_orders] or {}).price or -2^30) > ((mkt2.buy_orders[#mkt2.buy_orders] or {}).price or -2^30)
  198. end
  199. local compare_sell_volume = function(mkt1, mkt2)
  200. return mkt1.sell_volume > mkt2.sell_volume
  201. end
  202. local compare_sell_min = function(mkt1, mkt2)
  203. return ((mkt1.sell_orders[#mkt1.sell_orders] or {}).price or 2^31) < ((mkt2.sell_orders[#mkt2.sell_orders] or {}).price or 2^31)
  204. end
  205. local compare_last_price = function(mkt1, mkt2)
  206. return (mkt1.last_price or 2^31) < (mkt2.last_price or 2^31)
  207. end
  208. local sort_marketlist = function(item_list, account)
  209. -- I think tonumber is now redundant here, leaving it in in case upgrading a world that has text recorded in this field for an existing player account
  210. local sort_by = tonumber(account.sort_markets_by_column)
  211. if sort_by == nil then return end
  212. local show_itemnames = account.show_itemnames == "true"
  213. local show_icons = global_enable_item_icons and ((account.show_icons or "true") == "true")
  214. local icon_displace = 0
  215. if show_icons then
  216. icon_displace = 1
  217. end
  218. local itemname_displace = 0
  219. if show_itemnames then
  220. itemname_displace = 1
  221. end
  222. -- "Icon,Item,Description,#00FF00,Buy Vol,Buy Max,#FF0000,Sell Vol,Sell Min,Last Price"
  223. if sort_by == 1 + icon_displace and show_itemnames then
  224. table.sort(item_list, compare_market_item)
  225. elseif sort_by == 1 + icon_displace + itemname_displace then
  226. table.sort(item_list, compare_market_desc)
  227. elseif sort_by == 3 + icon_displace + itemname_displace then
  228. table.sort(item_list, compare_buy_volume)
  229. elseif sort_by == 4 + icon_displace + itemname_displace then
  230. table.sort(item_list, compare_buy_max)
  231. elseif sort_by == 6 + icon_displace + itemname_displace then
  232. table.sort(item_list, compare_sell_volume)
  233. elseif sort_by == 7 + icon_displace + itemname_displace then
  234. table.sort(item_list, compare_sell_min)
  235. elseif sort_by == 8 + icon_displace + itemname_displace then
  236. table.sort(item_list, compare_last_price)
  237. elseif sort_by == 9 + icon_displace + itemname_displace then
  238. table.sort(item_list, function(mkt1, mkt2)
  239. -- Define locally so that account is available
  240. return (account.inventory[mkt1.item] or 0) > (account.inventory[mkt2.item] or 0)
  241. end)
  242. end
  243. end
  244. local make_marketlist = function(market, account)
  245. local market_list = {}
  246. local search_filter = account.search or ""
  247. for item, row in pairs(market.orders_for_items) do
  248. if (search_filter == "" or string.find(item, search_filter)) then
  249. if account.filter_participating == "true" then
  250. local found = false
  251. for _, order in ipairs(row.buy_orders) do
  252. if account == order.account then
  253. found = true
  254. break
  255. end
  256. end
  257. if not found then
  258. for _, order in ipairs(row.sell_orders) do
  259. if account == order.account then
  260. found = true
  261. break
  262. end
  263. end
  264. end
  265. if found then
  266. table.insert(market_list, row)
  267. end
  268. else
  269. table.insert(market_list, row)
  270. end
  271. end
  272. end
  273. sort_marketlist(market_list, account)
  274. return market_list
  275. end
  276. local get_account_name = function(target_account, this_account, anonymous)
  277. if anonymous and target_account ~= this_account then
  278. return ""
  279. end
  280. return target_account.name
  281. end
  282. local get_market_formspec = function(market, account)
  283. local market_def = market.def
  284. local selected = account.selected
  285. local market_list = make_marketlist(market, account)
  286. local show_itemnames = account.show_itemnames == "true"
  287. local show_icons = global_enable_item_icons and ((account.show_icons or "true") == "true")
  288. local anonymous = market_def.anonymous
  289. local formspec = {
  290. "size[10,10]"
  291. .."tabheader[0,0;tabs;"..market_def.description..","..S("Your Inventory")..","..S("Market Orders")..";3;false;true]"
  292. }
  293. -- column definitions
  294. formspec[#formspec+1] = "tablecolumns["
  295. if show_icons then
  296. formspec[#formspec+1] = "image" -- icon
  297. for i, row in ipairs(market_list) do
  298. formspec[#formspec+1] = "," .. i .. "=" .. get_icon(row.item)
  299. end
  300. formspec[#formspec+1] = ";"
  301. end if show_itemnames then
  302. formspec[#formspec+1] = "text;" -- itemname
  303. end
  304. formspec[#formspec+1] = "text;" -- description
  305. .."color,span=2;"
  306. .."text,align=right,tooltip="..S("Number of items there's demand for in the market.")..";"
  307. .."text,align=right,tooltip="..S("Maximum price being offered to buy one of these.")..";"
  308. .."color,span=2;"
  309. .."text,align=right,tooltip="..S("Number of items available for sale in the market.")..";"
  310. .."text,align=right,tooltip="..S("Minimum price being demanded to sell one of these.")..";"
  311. .."text,align=right,tooltip="..S("Price paid for one of these the last time one was sold.")..";"
  312. .."text,align=right,tooltip="..S("Quantity of this item that you have in your inventory ready to sell.").."]"
  313. .."table[0,0;9.75,5;summary;"
  314. if show_icons then
  315. formspec[#formspec+1] = "0,"-- icon
  316. end
  317. -- header row
  318. if show_itemnames then
  319. formspec[#formspec+1] = "Item," -- itemname
  320. end
  321. formspec[#formspec+1] = S("Description")..",#00FF00,"..S("Buy Vol")..","..S("Buy Max")
  322. ..",#FF0000,"..S("Sell Vol")..","..S("Sell Min")..","..S("Last Price")..","..S("Inventory")
  323. local selected_idx
  324. local selected_row
  325. -- Show list of item market summaries
  326. for i, row in ipairs(market_list) do
  327. if show_icons then
  328. formspec[#formspec+1] = ","..i -- icon
  329. end
  330. if show_itemnames then
  331. formspec[#formspec+1] = "," .. truncate_string(row.item, truncate_item_names_to)
  332. end
  333. formspec[#formspec+1] = "," .. get_item_description(row.item)
  334. .. ",#00FF00,"
  335. .. row.buy_volume
  336. .. "," .. ((row.buy_orders[#row.buy_orders] or {}).price or "-")
  337. .. ",#FF0000,"
  338. .. row.sell_volume
  339. .. "," .. ((row.sell_orders[#row.sell_orders] or {}).price or "-")
  340. .. "," .. (row.last_price or "-")
  341. .. "," .. (account.inventory[row.item] or "-")
  342. -- we happen to be processing the row that matches the item this player has selected. Record that.
  343. if selected == row.item then
  344. selected_row = row
  345. selected_idx = i + 1
  346. end
  347. end
  348. -- a row that's visible is marked as the selected item, so make it selected in the formspec
  349. if selected_row then
  350. formspec[#formspec+1] = ";"..selected_idx
  351. end
  352. formspec[#formspec+1] = "]"
  353. -- search field
  354. formspec[#formspec+1] = "container[2.5,5]field_close_on_enter[search_filter;false]"
  355. .."field[0,0.85;2.5,1;search_filter;;"..minetest.formspec_escape(account.search or "").."]"
  356. .."image_button[2.05,0.65;0.8,0.8;commoditymarket_search.png;apply_search;]"
  357. .."image_button[2.7,0.65;0.8,0.8;commoditymarket_clear.png;clear_search;]"
  358. .."checkbox[1.77,0;filter_participating;"..S("My orders")..";".. account.filter_participating .."]"
  359. .."tooltip[filter_participating;"..S("Select this to show only the markets where you have either a buy or a sell order pending.").."]"
  360. .."tooltip[search_filter;"..S("Enter substring to search item identifiers for.").."]"
  361. .."tooltip[apply_search;"..S("Apply search to outputs.").."]"
  362. .."tooltip[clear_search;"..S("Clear search.").."]"
  363. .."container_end[]"
  364. -- if a visible item market is selected, show the orders for it in detail
  365. if selected_row then
  366. local current_time = minetest.get_gametime()
  367. local desc_display
  368. if show_itemnames then
  369. desc_display = selected
  370. else
  371. local def = minetest.registered_items[selected_row.item] or {description=S("Unknown Item")}
  372. desc_display = minetest.formspec_escape(def.description:gsub("\n", " "))
  373. end
  374. -- player inventory for this item and for currency
  375. formspec[#formspec+1] = "label[0.1,5.1;"..desc_display.."\n"..S("In inventory:").." "
  376. .. tostring(account.inventory[selected] or 0) .."\n"..S("Balance:").." "..market_def.currency_symbol..account.balance .."]"
  377. -- buy/sell controls
  378. .. "container[6.1,5]"
  379. local sell_limit = market_def.sell_limit
  380. if sell_limit then
  381. local total_sell = 0
  382. for item, orders in pairs(market.orders_for_items) do
  383. for _, order in ipairs(orders.sell_orders) do
  384. if order.account == account then
  385. total_sell = total_sell + order.quantity
  386. end
  387. end
  388. end
  389. formspec[#formspec+1] = "label[0,0;"..S("Sell limit:").." ".. total_sell .. "/" .. sell_limit .."]"
  390. .."tooltip[0,0;2,0.25;"..S("This market limits the total number of items a given seller can have for sale at a time.\nYou have @1 items remaining. Cancel old sell orders to free up space.", sell_limit-total_sell).."]"
  391. end
  392. -- Buy, sell, quantity and price button
  393. formspec[#formspec+1] = "tooltip[0,0.25;3.75,1;"..S("Use these fields to enter buy and sell orders for the selected item.").."]"
  394. .."button[0,0.55;1,1;buy;"..S("Buy").."]field[1.2,0.85;1,1;quantity;"..S("Quantity")..";]"
  395. .."field[2.1,0.85;1,1;price;"..S("Price per")..";]button[2.7,0.55;1,1;sell;Sell]"
  396. .."field_close_on_enter[quantity;false]field_close_on_enter[price;false]"
  397. .."container_end[]"
  398. -- table of buy and sell orders
  399. .."tablecolumns[color;text;"
  400. .."text,align=right,tooltip="..S("The price per item in this order.")..";"
  401. .."text,align=right,tooltip="..S("The total amount of items in this particular order.")..";"
  402. .."text,align=right,tooltip="..S("The total amount of items available at this price accounting for the other orders also currently being offered.")..";"
  403. .."text,tooltip="..S("The name of the player who placed this order.\nDouble-click your own orders to cancel them.")..";"
  404. .."text,align=right,tooltip="..S("How many days ago this order was placed.").."]"
  405. .."table[0,6.5;9.75,3.5;orders;#FFFFFF,"..S("Order")..","..S("Price")..","..S("Quantity")..","..S("Total Volume")..","..S("Player")..","..S("Days Old")
  406. local sell_volume = selected_row.sell_volume
  407. for i, sell in ipairs(selected_row.sell_orders) do
  408. formspec[#formspec+1] = ",#FF0000,"..S("Sell")..","
  409. ..sell.price..","
  410. ..sell.quantity..","
  411. ..sell_volume..","
  412. ..get_account_name(sell.account, account, anonymous)..","
  413. ..math.floor((current_time-sell.timestamp)/86400)
  414. sell_volume = sell_volume - sell.quantity
  415. end
  416. local buy_volume = 0
  417. local buy_orders = selected_row.buy_orders
  418. local buy_count = #buy_orders
  419. -- Show buy orders in reverse order
  420. for i = buy_count, 1, -1 do
  421. local buy = buy_orders[i]
  422. buy_volume = buy_volume + buy.quantity
  423. formspec[#formspec+1] = ",#00FF00,"..S("Buy")..","
  424. ..buy.price..","
  425. ..buy.quantity..","
  426. ..buy_volume..","
  427. ..get_account_name(buy.account, account, anonymous)..","
  428. ..math.floor((current_time-buy.timestamp)/86400)
  429. end
  430. formspec[#formspec+1] = "]"
  431. else
  432. formspec[#formspec+1] = "label[0.1,5.1;"..S("Select an item to view or place orders.").."]"
  433. end
  434. return table.concat(formspec)
  435. end
  436. -------------------------------------------------------------------------------------
  437. -- Information formspec
  438. --{item=item, quantity=quantity, price=price, purchaser=purchaser, seller=seller, timestamp = minetest.get_gametime()}
  439. local log_to_string = function(market, log_entry, account)
  440. local anonymous = market.def.anonymous
  441. local purchaser = log_entry.purchaser
  442. local seller = log_entry.seller
  443. local purchaser_name
  444. if purchaser == seller then
  445. purchaser_name = S("yourself")
  446. elseif anonymous and purchaser ~= account then
  447. purchaser_name = S("someone")
  448. elseif purchaser == account then
  449. purchaser_name = S("you")
  450. else
  451. purchaser_name = purchaser.name
  452. end
  453. local seller_name
  454. if anonymous and seller ~= account then
  455. seller_name = S("someone")
  456. elseif seller == account then
  457. seller_name = S("you")
  458. else
  459. seller_name = seller.name
  460. end
  461. local colour
  462. local new
  463. local last_acknowledged = account.last_acknowledged or 0
  464. if log_entry.timestamp > last_acknowledged then
  465. colour = "#FFFF00"
  466. new = true
  467. else
  468. colour = "#FFFFFF"
  469. new = false
  470. end
  471. local show_itemnames = account.show_itemnames == "true"
  472. local itemname = log_entry.item
  473. if not show_itemnames then
  474. local item_def = minetest.registered_items[log_entry.item]
  475. if item_def then
  476. itemname = minetest.formspec_escape(item_def.description:gsub("\n", " "))
  477. end
  478. end
  479. local currency_symbol = market.def.currency_symbol
  480. return colour .. S("On day @1 @2 sold @3 @4 to @5 at @6@7 each for a total of @8@9.",
  481. math.ceil(log_entry.timestamp/86400), seller_name, log_entry.quantity, itemname,
  482. purchaser_name, currency_symbol, log_entry.price, currency_symbol, log_entry.quantity*log_entry.price), new
  483. end
  484. local get_info_formspec = function(market, account)
  485. local formspec = {
  486. "size[10,10]"
  487. .."tabheader[0,0;tabs;"..market.def.description..","..S("Your Inventory")..","..S("Market Orders")..";1;false;true]"
  488. .."textarea[0.75,0.5;9.25,1.5;;"..S("Description:")..";"..market.def.long_description.."]"
  489. .."label[0.5,2.2;"..S("Your Recent Purchases and Sales:").."]"
  490. .."textlist[0.5,2.6;8.75,4;log_entries;"
  491. }
  492. if next(account.log) then
  493. local new = false
  494. for _, log_entry in ipairs(account.log) do
  495. local log_string, new_log = log_to_string(market, log_entry, account)
  496. new = new or new_log
  497. formspec[#formspec+1] = log_string
  498. formspec[#formspec+1] = ","
  499. end
  500. formspec[#formspec] = "]" -- Note: there's no +1 here deliberately, that way the "]" overwrites the last comma added by the loop above.
  501. if new then
  502. formspec[#formspec+1] = "button[7.1,6.9;2,0.5;acknowledge_log;"..S("Mark logs as read").."]" ..
  503. "tooltip[acknowledge_log;"..S("Log entries in yellow are new since last time you marked your log as read.").."]"
  504. end
  505. else
  506. formspec[#formspec+1] = "#CCCCCC"..S("No logged activities in this market yet.").."]"
  507. end
  508. local show_itemnames = account.show_itemnames or "false"
  509. formspec[#formspec+1] = "]container[0.5, 7.5]label[0,0;Settings:]checkbox[0,0.25;show_itemnames;"..S("Show Itemnames")..";"
  510. ..show_itemnames.."]"
  511. if global_enable_item_icons then
  512. local show_icons = account.show_icons or "true"
  513. formspec[#formspec+1] = "checkbox[2,0.25;show_icons;"..S("Show Icons")..";"..show_icons.."]"
  514. end
  515. formspec[#formspec+1] = "container_end[]"
  516. return table.concat(formspec)
  517. end
  518. ---------------------------------------------------------------------------------------
  519. commoditymarket.get_formspec = function(market, account)
  520. local tab = account.tab
  521. if tab == 1 then
  522. return get_info_formspec(market, account)
  523. elseif tab == 2 then
  524. return get_account_formspec(market, account)
  525. else
  526. return get_market_formspec(market, account)
  527. end
  528. end
  529. ------------------------------------------------------------------------------------
  530. -- Handling recieve_fields
  531. local add_to_player_inventory = function(name, item, amount)
  532. local playerinv = minetest.get_inventory({type="player", name=name})
  533. local not_full = true
  534. while amount > 0 and not_full do
  535. local stack = ItemStack(item .. " " .. amount)
  536. amount = amount - stack:get_count()
  537. local leftover = playerinv:add_item("main", stack)
  538. if leftover:get_count() > 0 then
  539. amount = amount + leftover:get_count()
  540. return amount
  541. end
  542. end
  543. return amount
  544. end
  545. minetest.register_on_player_receive_fields(function(player, formname, fields)
  546. local formname_split = formname:split(":")
  547. if formname_split[1] ~= "commoditymarket" then
  548. return false
  549. end
  550. local market = commoditymarket.registered_markets[formname_split[2]]
  551. if not market then
  552. return false
  553. end
  554. local name = formname_split[3]
  555. if name ~= player:get_player_name() then
  556. return false
  557. end
  558. local account = market:get_account(name)
  559. local show_icons = global_enable_item_icons and ((account.show_icons or "true") == "true")
  560. local something_changed = false
  561. if fields.tabs then
  562. account.tab = tonumber(fields.tabs)
  563. something_changed = true
  564. end
  565. -- player clicked on an item in the market summary table
  566. if fields.summary then
  567. local summaryevent = minetest.explode_table_event(fields.summary)
  568. if summaryevent.type == "DCL" or summaryevent.type == "CHG" then
  569. if summaryevent.row == 1 then
  570. -- header clicked, sort by column
  571. local column = tonumber(summaryevent.column)
  572. if not (column == 1 and show_icons) then -- ignore clicks on the icon column header
  573. account.sort_markets_by_column = column
  574. end
  575. else
  576. -- item clicked, recreate the list to find out which one
  577. local marketlist = make_marketlist(market, account)
  578. local selected = marketlist[summaryevent.row-1]
  579. if selected then
  580. account.selected = selected.item
  581. end
  582. end
  583. elseif summaryevent.type == "INV" then
  584. account.selected = nil
  585. end
  586. something_changed = true
  587. end
  588. if fields.orders then
  589. local ordersevent = minetest.explode_table_event(fields.orders)
  590. if ordersevent.type == "DCL" and ordersevent.column > 0 then
  591. local selected_idx = ordersevent.row - 1 -- account for header
  592. local selected_row = market.orders_for_items[account.selected] -- sell orders come first
  593. local sell_orders = selected_row.sell_orders
  594. local sell_order_count = #sell_orders
  595. local selected_order
  596. if selected_idx <= sell_order_count then -- if the index is within the range of sell orders,
  597. selected_order = sell_orders[selected_idx]
  598. if selected_order and selected_order.account == account then -- and the order belongs to the current player,
  599. market:cancel_sell(account.selected, selected_order) -- cancel it
  600. something_changed = true
  601. end
  602. else
  603. -- otherwise we're in the buy group, shift the index up by sell_order_count and reverse index order
  604. local buy_orders = selected_row.buy_orders
  605. local buy_orders_count = #buy_orders
  606. selected_order = buy_orders[buy_orders_count - (selected_idx - sell_order_count - 1)]
  607. if selected_order and selected_order.account == account then
  608. market:cancel_buy(account.selected, selected_order)
  609. something_changed = true
  610. end
  611. end
  612. end
  613. end
  614. if fields.buy then
  615. local quantity = tonumber(fields.quantity)
  616. local price = tonumber(fields.price)
  617. if price ~= nil and quantity ~= nil then
  618. market:buy(name, account.selected, quantity, price)
  619. something_changed = true
  620. end
  621. end
  622. if fields.sell then
  623. local quantity = tonumber(fields.quantity)
  624. local price = tonumber(fields.price)
  625. if price ~= nil and quantity ~= nil then
  626. market:sell(name, account.selected, quantity, price)
  627. something_changed = true
  628. end
  629. end
  630. -- player clicked in their inventory table, may need to give him his stuff back
  631. if fields.inventory then
  632. local invevent = minetest.explode_table_event(fields.inventory)
  633. if invevent.type == "DCL" and invevent.column > 0 then
  634. local col_count = 8
  635. local show_itemnames = account.show_itemnames == "true"
  636. if not show_itemnames then
  637. col_count = col_count - 2
  638. end
  639. if not show_icons then
  640. col_count = col_count - 2
  641. end
  642. local index = math.floor(((invevent.row-1)*col_count + invevent.column - 1)/(col_count/2)) - 1
  643. local account = market:get_account(name)
  644. -- build a local copy of the inventory that would be displayed in the formspec so we can
  645. -- figure out what item the index we were given is pointing to
  646. local inventory = {}
  647. for item, quantity in pairs(account.inventory) do
  648. table.insert(inventory, {item=item, quantity=quantity, description=get_item_description(item)})
  649. end
  650. if show_itemnames then
  651. table.sort(inventory, inventory_item_comp)
  652. else
  653. table.sort(inventory, inventory_desc_comp)
  654. end
  655. if inventory[index] then
  656. local item = inventory[index].item
  657. local amount = account.inventory[item]
  658. local remaining = add_to_player_inventory(name, item, amount)
  659. if remaining == 0 then
  660. account.inventory[item] = nil
  661. else
  662. account.inventory[item] = remaining
  663. end
  664. if remaining ~= amount then
  665. something_changed = true
  666. end
  667. end
  668. end
  669. end
  670. if fields.withdraw or fields.key_enter_field == "withdrawamount" then
  671. local withdrawvalue = tonumber(fields.withdrawamount)
  672. if withdrawvalue then
  673. local account = market:get_account(name)
  674. withdrawvalue = math.min(withdrawvalue, account.balance)
  675. for _, currency in ipairs(market.def.currency_ordered) do
  676. this_unit_amount = math.floor(withdrawvalue/currency.amount)
  677. if this_unit_amount > 0 then
  678. local remaining = add_to_player_inventory(name, currency.item, this_unit_amount)
  679. local value_given = (this_unit_amount - remaining) * currency.amount
  680. account.balance = account.balance - value_given
  681. withdrawvalue = withdrawvalue - value_given
  682. something_changed = true
  683. end
  684. end
  685. end
  686. end
  687. if fields.search_filter then
  688. local value = string.lower(fields.search_filter)
  689. if account.search ~= value then
  690. account.search = value
  691. end
  692. end
  693. local process_checkbox = function(property_name, fields, account)
  694. if (fields[property_name] == "true" and account[property_name] ~= "true") or
  695. (fields[property_name] == "false" and account[property_name] ~= "false") then
  696. account[property_name] = fields[property_name]
  697. return true
  698. end
  699. return false
  700. end
  701. if process_checkbox("filter_participating", fields, account) then something_changed = true end
  702. if process_checkbox("show_itemnames", fields, account) then something_changed = true end
  703. if process_checkbox("show_icons", fields, account) then something_changed = true end
  704. if fields.acknowledge_log then
  705. account.last_acknowledged = minetest.get_gametime()
  706. something_changed = true
  707. end
  708. if fields.apply_search or fields.key_enter_field == "search_filter" then
  709. something_changed = true
  710. end
  711. if fields.clear_search then
  712. account.search = ""
  713. something_changed = true
  714. end
  715. if something_changed then
  716. minetest.show_formspec(name, formname, market:get_formspec(account))
  717. end
  718. end)