init.lua 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600
  1. -- mod: [bounty]
  2. -- description: adds a death certificate, to get a bounty rewards.
  3. -- notes: the idea is to get a death certificate, so you can get a reward from
  4. -- an automated machine.
  5. -- author: boxface
  6. -- license: MIT
  7. -- version: 1.0
  8. -- date: 2019-10-20
  9. --
  10. -- version: 1.1
  11. -- date: 2022-08-20
  12. --[[
  13. *DONE: add bounty mark item
  14. use item, show dialog, enter username, and place bounty on player
  15. if more than x players place bounty on player then the bounty becomes
  16. active and the player bones will drop an item proving the player was
  17. killed and someone took his bones. (captured bones)
  18. * do not give player head (bounty reward) to same player
  19. * show a chat message like: "Server: Someone put a price on <player>'s head."
  20. * may be interesting to add a list to the 'dead proof' of the items
  21. captured in the bones.
  22. * may be bounty price needs to expire at some point.
  23. * trusted players may place a higher bounty level.
  24. * new (just registered) players may not place bounty.
  25. ** Add reason for capture.
  26. * must not place bounty on yourself.
  27. ** TODO: if owner take items from his own bones, give no reward for trash bones.
  28. ** Require at least 2 players to activate bounty. (?)
  29. --]]
  30. if not minetest.global_exists("bounty") then
  31. bounty = {}
  32. bounty.wanted = {test = true}
  33. bounty.author = "boxface"
  34. bounty.version = 1.1
  35. bounty.date = "2022-08-20"
  36. bounty.modname = minetest.get_current_modname()
  37. bounty.modpath = minetest.get_modpath(bounty.modname)
  38. bounty.storage = minetest.get_mod_storage()
  39. -- load register file only the first time
  40. dofile(bounty.modpath .. "/register.lua")
  41. else
  42. if not bounty.author == "boxface" then
  43. local err = "[bounty] another mod registered my global variable."
  44. minetest.log("error", err)
  45. return false
  46. end
  47. end
  48. -- code
  49. bounty.reward = {}
  50. bounty.bones = {}
  51. function bounty.chat_send_player(pname, msg)
  52. local s = minetest.colorize("goldenrod", msg)
  53. minetest.chat_send_player(pname, s)
  54. end
  55. function bounty.command_list(pname, param)
  56. local msg = ""
  57. nwanted = 0
  58. for k, v in pairs(bounty.wanted) do
  59. local count = bounty.get_count(k)
  60. msg = "Wanted: %s, by %d players."
  61. msg = string.format(msg, k, count)
  62. bounty.chat_send_player(pname, msg)
  63. nwanted = nwanted + 1
  64. end
  65. if nwanted == 0 then
  66. bounty.chat_send_player(pname, "Bounty list is empty.")
  67. else
  68. bounty.chat_send_player(pname, "End of list.")
  69. end
  70. return true
  71. end
  72. function bounty.get_bounty_data(pos)
  73. local meta = minetest.get_meta(pos)
  74. local data = {}
  75. data.pos = {x = pos.x, y = pos.y, z = pos.z}
  76. data.owner = meta:get_string("owner")
  77. data.bounty = meta:get_string("bounty")
  78. data.date = meta:get_string("date")
  79. return data
  80. end
  81. function bounty.give_reward(data, player)
  82. assert(type(data)=="table")
  83. local pname = player:get_player_name()
  84. -- do not give bounty proof to same player
  85. if data.bounty == "" or data.bounty == pname then
  86. minetest.log("action", "[bounty] no bounty to the same player.")
  87. return false
  88. end
  89. -- check if player is wanted
  90. if (bounty.check_bounty(data.bounty) == 0) then
  91. -- no bounty on player, no reward
  92. -- avoid reward to any bones
  93. return false
  94. end
  95. -- check if bones removed and empty inventory
  96. local meta = minetest.get_meta(data.pos)
  97. local owner = meta:get_string("owner")
  98. local inv = meta:get_inventory("main")
  99. if owner ~= "" or not inv:is_empty("main") then
  100. -- inventory must be empty
  101. -- owner removed
  102. -- and bones node removed
  103. return false
  104. end
  105. -- create bounty reward stack
  106. local stack = ItemStack("bounty:reward")
  107. local smeta = stack:get_meta()
  108. local player_inv = player:get_inventory()
  109. smeta:set_string("bounty", data.bounty)
  110. smeta:set_string("date", data.date)
  111. smeta:set_string("capturedby", pname)
  112. smeta:set_string("tombstone", "")
  113. smeta:set_string("infotext", data.bounty .. "\n dead proof")
  114. smeta:set_string("description", data.bounty .. " dead proof")
  115. if player_inv:room_for_item("main", stack) then
  116. player_inv:add_item("main", stack)
  117. bounty.chat_send_player(pname, "[bounty] Reward added to your inventory")
  118. else
  119. minetest.add_item(data.pos, stack)
  120. bounty.chat_send_player(pname, "[bounty] Your inventory is full, reward dropped.")
  121. end
  122. return true
  123. end
  124. function bounty.mark_use(itemstack, user, pointed_thing)
  125. -- mark a player as wanted
  126. -- put price on a player's head
  127. local pname = user:get_player_name()
  128. local formspec = bounty.get_mark_formspec()
  129. minetest.show_formspec(pname, "bounty:mark", formspec)
  130. end
  131. function bounty.place_bounty(target, placer)
  132. -- set bounty on player
  133. -- record must contain:
  134. --
  135. -- playername: who set bounty.
  136. -- targetname: wanted player name.
  137. -- dateadded: date the bounty was placed.
  138. -- expiry time?
  139. local pname = placer:get_player_name()
  140. local has_bounty = bounty.wanted[target] ~= nil
  141. local result = false
  142. if not has_bounty then
  143. -- place first bounty
  144. local data = {}
  145. data.placers = {}
  146. data.placers[pname] = {
  147. date = os.date("%Y-%m-%d %H:%M")
  148. }
  149. bounty.wanted[target] = data
  150. result = true
  151. else
  152. -- check if already placed
  153. if bounty.wanted[target] and bounty.wanted[target].placers and
  154. not bounty.wanted[target].placers[pname] then
  155. -- place bounty if not paced on this target
  156. bounty.wanted[target].placers[pname] = {
  157. date = os.date("%Y-%m-%d %H:%H")
  158. }
  159. result = true
  160. end
  161. end
  162. if result then
  163. local msg = "You have placed bounty on <%s>."
  164. msg = string.format(msg, target)
  165. bounty.chat_send_player(pname, msg)
  166. end
  167. return result
  168. end
  169. function bounty.get_count(target)
  170. local count = 0
  171. if bounty.wanted[target] and bounty.wanted[target].placers then
  172. for k, v in pairs(bounty.wanted[target].placers) do
  173. count = count + 1
  174. end
  175. end
  176. return count
  177. end
  178. function bounty.remove_bounty(target, placer)
  179. local pname = placer:get_player_name()
  180. local result = false
  181. if bounty.wanted[target] then
  182. if bounty.wanted[target].placers and
  183. bounty.wanted[target].placers[pname] then
  184. bounty.wanted[target].placers[pname] = nil
  185. local msg = "You removed bounty from <%s>."
  186. msg = string.format(msg, target)
  187. bounty.chat_send_player(pname, msg)
  188. result = true
  189. -- if list is empty, clear record
  190. if bounty.get_count(target) == 0 then
  191. bounty.wanted[target] = nil
  192. local msg = "Bounty on %s has been cleared."
  193. msg = string.format(msg, target)
  194. bounty.chat_send_player(pname, msg)
  195. end
  196. end
  197. end
  198. return result
  199. end
  200. function bounty.check_bounty(target)
  201. local data = bounty.wanted[target]
  202. print("Dump " .. target .. ": " .. dump(data))
  203. local count = 0
  204. if data and data.placers then
  205. for k, v in pairs(data.placers) do
  206. count = count + 1
  207. end
  208. end
  209. print("Count: " .. count)
  210. return count
  211. end
  212. -- test
  213. function bounty.do_dieplayer(objref, reason)
  214. local deadplayer = objref:get_player_name()
  215. local wanted_count = bounty.check_bounty(deadplayer)
  216. local msg = "Player <%s> died, wanted level %d."
  217. msg = string.format(msg, deadplayer, wanted_count)
  218. msg = minetest.colorize("cyan", msg)
  219. minetest.chat_send_all(msg)
  220. if wanted_count > 0 then
  221. local msg = "Player <%s> is wanted by %d players."
  222. msg = string.format(msg, deadplayer, wanted_count)
  223. msg = minetest.colorize("cyan", msg)
  224. minetest.chat_send_all(msg)
  225. end
  226. if reason and reason.object and reason.object:is_player() then
  227. if wanted_count > 0 then
  228. local pname = reason.object:get_player_name()
  229. local msg = "Server: Player <%s> has a price on his head."
  230. msg = string.format(msg, deadplayer)
  231. msg = minetest.colorize("darkorange", msg)
  232. minetest.chat_send_player(pname, msg)
  233. end
  234. end
  235. end
  236. function bounty.get_mark_formspec()
  237. local formspec = {
  238. "formspec_version[2]",
  239. "size[8,4]",
  240. "box[0.25,0.25;7.5,1;#80C0C0]",
  241. "item_image[0.4,0.4;0.8,0.8;bounty:mark]",
  242. "label[1.5,0.5;Bounty marker]",
  243. "label[1.5,1.0;Put a price on a player's head.]",
  244. -- "label[1.5,1.5;Get their bones and get a proof.]",
  245. "field[0.5,2;7,0.8;bountytarget;Player name:;]",
  246. "field_close_on_enter[bountytarget;false]",
  247. "style[place;bgcolor=#E00000]",
  248. "style[remove;bgcolor=#00E000]",
  249. "tooltip[place;Place bounty.]",
  250. "tooltip[remove;Remove bounty.]",
  251. "button_exit[0.5,3;1.75,0.8;place;Place]",
  252. "button_exit[2.5,3;1.75,0.8;remove;Remove]",
  253. "button_exit[4.5,3;1.75,0.8;cancel;Cancel]",
  254. -- debug
  255. --"style[dump;bgcolor=#0000F0]",
  256. --"button_exit[6.5,3;1,0.8;dump;Test]"
  257. }
  258. local str = table.concat(formspec)
  259. -- str = string.gsub(str, "${variable}", variable)
  260. return str
  261. end
  262. function bounty.get_tombstone_formspec(data, readonly)
  263. if data == nil then
  264. data = {}
  265. end
  266. local bountyname = data.bounty or "unknown"
  267. local capturedby = data.capturedby or "unknown"
  268. local captureddate = data.date or ""
  269. local tombstone = data.tombstone or ""
  270. local formspec = {
  271. "formspec_version[2]",
  272. "size[6,5]",
  273. "box[0.25,0.25;5.5,1;#80C0C0]",
  274. "item_image[0.4,0.4;0.8,0.8;bounty:reward]",
  275. "label[1.5,0.5;Captured bones]",
  276. "label[1.5,1.0;${bountyname}]",
  277. "label[0.25,1.5;Captured by: ${capturedby}]",
  278. "label[0.25,2.0;Date: ${date}]",
  279. "field[0.25,3.0;5.5,0.8;tombstone;Tombstone text;${tombstone}]",
  280. "button_exit[1.5,4;3,0.8;write;Write]"
  281. }
  282. if readonly then
  283. formspec = {
  284. "formspec_version[2]",
  285. "size[6,5]",
  286. "box[0.25,0.25;5.5,1;#80C0C0]",
  287. "item_image[0.4,0.4;0.8,0.8;bounty:reward]",
  288. "label[1.5,0.5;Captured bones]",
  289. "label[1.5,1.0;${bountyname}]",
  290. "label[0.25,1.5;Captured by: ${capturedby}]",
  291. "label[0.25,2.0;Date: ${date}]",
  292. "label[0.25,3.0;${tombstone}]",
  293. "button_exit[1.5,4;3,0.8;close;Close]"
  294. }
  295. end
  296. str = table.concat(formspec)
  297. str = string.gsub(str, "${bountyname}", bountyname)
  298. str = string.gsub(str, "${date}", captureddate)
  299. str = string.gsub(str, "${capturedby}", capturedby)
  300. str = string.gsub(str, "${tombstone}", tombstone)
  301. return str
  302. end
  303. function bounty.reward.do_after_place_node(pos, placer, itemstack, pointed_thing)
  304. -- bounty reward
  305. local meta = minetest.get_meta(pos)
  306. local imeta = itemstack:get_meta()
  307. meta:set_string("bounty", imeta:get_string("bounty"))
  308. meta:set_string("date", imeta:get_string("date"))
  309. meta:set_string("capturedby", imeta:get_string("capturedby"))
  310. meta:set_string("tombstone", imeta:get_string("tombstone"))
  311. meta:set_string("infotext", imeta:get_string("bounty") ..
  312. "\nDate: " .. imeta:get_string("date") ..
  313. "\nCaptured by: " .. imeta:get_string("capturedby") ..
  314. "\n" .. imeta:get_string("tombstone"))
  315. meta:set_string("description", imeta:get_string("description"))
  316. return false
  317. end
  318. function bounty.reward.do_preserve_metadata(pos, oldnode, oldmeta, drops)
  319. -- bounty reward
  320. -- debug
  321. --minetest.chat_send_all(
  322. -- minetest.colorize("darkgrey", "Debug: do_preserve_metadata"))
  323. print("preserve_metadata:")
  324. print(dump(drops))
  325. print("oldmeta:")
  326. print(dump(oldmeta))
  327. stack = drops[1]
  328. smeta = stack:get_meta()
  329. smeta:set_string("bounty", oldmeta["bounty"])
  330. smeta:set_string("date", oldmeta["date"])
  331. smeta:set_string("capturedby", oldmeta["capturedby"])
  332. smeta:set_string("tombstone", oldmeta["tombstone"])
  333. smeta:set_string("infotext", oldmeta["infotext"])
  334. smeta:set_string("description", oldmeta["description"])
  335. end
  336. function bounty.reward.do_use(itemstack, user, pointed_thing)
  337. -- bounty reward
  338. local pname = user:get_player_name()
  339. local imeta = itemstack:get_meta()
  340. --local bountyname = imeta:get_string("bounty")
  341. --local tombstone = imeta:get_string("tombstone")
  342. -- minetest.chat_send_player(pname, "USING THIS THING!")
  343. local data = {
  344. bounty = imeta:get_string("bounty"),
  345. tombstone = imeta:get_string("tombstone"),
  346. date = imeta:get_string("date"),
  347. capturedby = imeta:get_string("capturedby")
  348. }
  349. local formspec = bounty.get_tombstone_formspec(data)
  350. minetest.show_formspec(pname, "bounty:tombstone", formspec)
  351. -- minetest.show_formspec(pname, "bounty:tombstone",
  352. -- bounty.get_tombstone_formspec(bountyname, tombstone))
  353. end
  354. function bounty.reward.do_rightclick(pos, node, clicker, pointed_thing)
  355. local pname = clicker:get_player_name()
  356. local meta = minetest.get_meta(pos)
  357. local data = {
  358. bounty = meta:get_string("bounty"),
  359. tombstone = meta:get_string("tombstone"),
  360. date = meta:get_string("date"),
  361. capturedby = meta:get_string("capturedby")
  362. }
  363. local formspec = bounty.get_tombstone_formspec(data, true)
  364. minetest.show_formspec(pname, "bounty:tombstonerw", formspec)
  365. end
  366. function bounty.do_player_receive_fields(player, formname, fields)
  367. if formname == "bounty:mark" then
  368. -- debug
  369. --local dbg = "formspec[bounty:mark]\nFields: " .. dump(fields)
  370. --dbg = minetest.colorize("magenta", dbg)
  371. --minetest.chat_send_player(player:get_player_name(), dbg)
  372. --
  373. local bountylevel = 0
  374. if fields.key_enter_field == "bountytarget" then
  375. if fields.bountytarget ~= "" then
  376. -- bounty.check_bounty(fields.bountytarget)
  377. end
  378. end
  379. -- check buttons
  380. if fields.dump then
  381. minetest.chat_send_player(player:get_player_name(),
  382. "Wanted list: " .. dump(bounty.wanted))
  383. elseif fields.place then
  384. local pname = player:get_player_name()
  385. local target = fields.bountytarget
  386. -- player exists?
  387. --if bountytarget and minetest.builtin_auth_handler.get_auth(bountytarget) ~= nil then
  388. -- minetest.chat_send_player(pname, "Player found!")
  389. --else
  390. -- minetest.chat_send_player(pname, "That player is not found.")
  391. --end
  392. if target ~= "" then
  393. -- place bounty
  394. if bounty.place_bounty(target, player) then
  395. -- use bounty tag
  396. wield = player:get_wielded_item()
  397. if wield:get_name() == "bounty:mark" then
  398. wield:take_item()
  399. player:set_wielded_item(wield)
  400. end
  401. end
  402. -- bounty.storage.
  403. end
  404. elseif fields.remove then
  405. local pname = player:get_player_name()
  406. local target = fields.bountytarget
  407. if target ~= "" then
  408. if bounty.remove_bounty(target, player) then
  409. -- DONE: use bounty tag
  410. wield = player:get_wielded_item()
  411. if wield:get_name() == "bounty:mark" then
  412. wield:take_item()
  413. player:set_wielded_item(wield)
  414. end
  415. end
  416. end
  417. end
  418. elseif formname == "bounty:tombstone" then
  419. if fields.tombstone then
  420. -- update tombstone text
  421. local pname = player:get_player_name()
  422. local wielditem = player:get_wielded_item()
  423. if not wielditem then
  424. return
  425. end
  426. -- debug
  427. print("Setting wielded item " .. fields.tombstone)
  428. local wieldmeta = wielditem:get_meta()
  429. wieldmeta:set_string("tombstone", fields.tombstone)
  430. player:set_wielded_item(wielditem)
  431. end
  432. end
  433. end
  434. function bounty.do_shutdown()
  435. bounty.store_save()
  436. end
  437. function bounty.store_load()
  438. bounty.wanted = minetest.deserialize(bounty.storage:get_string("wanted")) or {}
  439. end
  440. function bounty.store_save()
  441. bounty.storage:set_string("wanted", minetest.serialize(bounty.wanted))
  442. end
  443. -- ========== BONES OVERRIDE FUNCTIONS ==========
  444. function bounty.override_bones()
  445. local bones = minetest.registered_nodes["bones:bones"]
  446. bounty.oldbones_metadata_inventory_take = bones.on_metadata_inventory_take
  447. bones.on_metadata_inventory_take = function(...)
  448. return bounty.bones.do_metadata_inventory_take(...)
  449. end
  450. bounty.oldbones_on_punch = bones.on_punch
  451. bones.on_punch = function(...)
  452. return bounty.bones.do_punch(...)
  453. end
  454. bounty.oldbones_on_timer = bones.on_timer
  455. bones.on_timer = function(...)
  456. return bounty.bones.do_timer(...)
  457. end
  458. end
  459. function bounty.bones.do_metadata_inventory_take(pos, listname, index, stack, player)
  460. -- get bounty data
  461. local bountydata = bounty.get_bounty_data(pos)
  462. -- original bones callback
  463. local result = bounty.oldbones_metadata_inventory_take(pos, listname, index, stack, player)
  464. -- check if node was removed
  465. -- give bounty reward if applicable
  466. bounty.give_reward(bountydata, player)
  467. return result
  468. end
  469. function bounty.bones.do_punch(pos, node, player)
  470. -- get bounty data
  471. local bountydata = bounty.get_bounty_data(pos)
  472. -- original callback
  473. local result = bounty.oldbones_on_punch(pos, node, player)
  474. -- check if node was removed
  475. -- give bounty reward if applicable
  476. bounty.give_reward(bountydata, player)
  477. return result
  478. end
  479. function bounty.bones.do_timer(pos, elapsed)
  480. local meta = minetest.get_meta(pos)
  481. local owner = meta:get_string("owner")
  482. if owner ~= "" and bounty.get_count(owner)>0 then
  483. -- add "bounty" name to bones
  484. meta:set_string("bounty", meta:get_string("owner"))
  485. -- add "deathdate" to bones
  486. local deathdate = os.date("%Y-%m-%d %H:%M")
  487. meta:set_string("date", deathdate)
  488. end
  489. -- bones callback function
  490. local result = bounty.oldbones_on_timer(pos, elapsed)
  491. -- debug
  492. --local time = meta:get_int("time") + elapsed
  493. --if result then
  494. -- minetest.chat_send_all(minetest.colorize("orange",
  495. -- "Server: player bones still warm. (time: ".. time .. ")."))
  496. --else
  497. -- minetest.chat_send_all(minetest.colorize("cyan",
  498. -- "Server: player bones got cold!"))
  499. --end
  500. return result
  501. end
  502. -- ---------- END BONES OVERRIDE FUNCTIONS ----------
  503. -- initialization
  504. if not bounty.runonce then
  505. -- debug
  506. local share_bones_time = tonumber(minetest.settings:get("share_bones_time")) or 1200
  507. print("Debug: Share bones time: " .. share_bones_time)
  508. minetest.register_chatcommand("bounty", {
  509. params = "",
  510. description = "Shows bounty list.",
  511. privs = {},
  512. func = function(...)
  513. return bounty.command_list(...)
  514. end
  515. })
  516. minetest.register_on_dieplayer(function(...)
  517. bounty.do_dieplayer(...)
  518. end)
  519. minetest.register_on_player_receive_fields(function(...)
  520. bounty.do_player_receive_fields(...)
  521. end)
  522. minetest.register_on_shutdown(function(...)
  523. return bounty.do_shutdown(...)
  524. end)
  525. bounty.override_bones()
  526. bounty.store_load()
  527. if minetest.get_modpath("reload") then
  528. reload.register_file("bounty:init", bounty.modpath .. "/init.lua", false)
  529. end
  530. bounty.runonce = true
  531. end
  532. --<eof>