123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328 |
- -- Elemental mod for Minetest
- -- Copyright ©2019 Alex Yst <mailto:copyright@y.st>
- -- This program is free software; you can redistribute it and/or
- -- modify it under the terms of the GNU Lesser General Public
- -- License as published by the Free Software Foundation; either
- -- version 2.1 of the License, or (at your option) any later version.
- -- This software is distributed in the hope that it will be useful,
- -- but WITHOUT ANY WARRANTY; without even the implied warranty of
- -- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- -- Lesser General Public License for more details.
- -- You should have received a copy of the GNU Lesser General Public
- -- License along with this program. If not, see
- -- <https://www.gnu.org./licenses/>.
- local storage = minetest.get_mod_storage()
- -- Conversion tables
- local to_element, to_index, palette_colour = dofile(minetest.get_modpath("elemental").."/elements.lua")
- -- We need to keep track of which crystal a player is using, so the
- -- deposit/withdrawal formspec can be used properly.
- local using_crystal = {}
- -- Storing integer metadata in Minetest is sort of interesting. From
- -- what I can tell, Lua's floats are cast into integers (probably by
- -- C), then truncated by C into 32 bits (signed). From there, they get
- -- cast into strings. The process might not be exactly as I describe
- -- here, as I'm not sure where in the code it is happening, and only
- -- have the data I've gotten through experimenting. Anyway, the exact
- -- details of how and why it happen aren't important. What's important
- -- is *what actually ends up done, and the above description gets close
- -- enough to understand the implications of the process. Essentially,
- -- we're working with 32-bit signed integers even though we're not
- -- allowed to use integers directly. That means that if we add too
- -- much, we're going to go negative,due to integer overflow. *Way*
- -- negative. To avoid that, we either can't rely on Minetest's integer
- -- storage mechanism or we have to put a cap on the input so it never
- -- exceed the maximum 32-bit signed integer and overflows.
- --
- -- We could roll our own integer representation. Liblevelup has code
- -- for that as far as its own data, proving that it's more than
- -- feasible. However, for now, I think staying within the bounds of
- -- what is basically a 3-bit unsigned integer - as we never want to go
- -- negative - is just fine as well. Honestly, that still allows you to
- -- store more than you could in 1024 chests in the worst case and more
- -- than you could store in 677867 chests in the common case. Even with
- -- putting a cap on the amount we can store, trying to actually exceed
- -- that maximum value is an unrealistic goal. If for some reason you
- -- you think you should be allowed to store more than this in each
- -- elemental crystal, by all means, get in touch and let me know. I
- -- have two higher caps we can work with if the need should arise, and
- -- if those don't work for you either, as I mentioned before, I have a
- -- way to remove the cap entirely. It'll just take a little more
- -- processing power from the server. But until someone complains about
- -- the cap, I'm going to assume no one needs to store more than 31 bits
- -- worth of material.
- local max_storage = 2^31-1
- -- This function drops a stack into a player's inventory. It's used to
- -- give players elemental crystals, to withdraw items from crystals,
- -- and to prevent deletion of items in case of error.
- --
- -- The implementation of this function might need work. This isn't
- -- exactly the semantically correct way to do this.
- local function give_to_player(player, item)
- minetest.handle_node_drops(player:get_pos(), {
- item,
- }, player)
- end
- local function send_formspec(player_name)
- local meta = minetest.get_meta(using_crystal[player_name])
- formspec = "\
- size[8,9]\
- label[0,0;"..(meta:get_string("infotext") or "").."]\
- label[2,1;Currently in storage: "..meta:get_int("count").."]\
- list[current_player;main;0,4.85;8,1;]\
- list[current_player;main;0,6.08;8,3;8]\
- listring[current_player;main]\
- listring[detached:elemental;input]\
- button[1,1.5;3,1;withdraw;Withdraw maximum]\
- button[1,2.5;3,1;withdraw;Withdraw stack]\
- button[1,3.5;3,1;withdraw;Withdraw count]\
- item_image[5,2;1,1;"..to_element[minetest.get_node(using_crystal[player_name]).param2].."]\
- field[4.3,4;3,1;count;Count:;1]"
- ..default.get_hotbar_bg(0,4.85)
- minetest.show_formspec(player_name, "elemental:0", formspec)
- end
- -- We can't always fit a full withdrawal into the player's inventory,
- -- so this function does the work of fitting what it can, and only
- -- removing from storage what was actually given to the player.
- local function withdraw(player, attempted_count)
- local player_name = player:get_player_name()
- if using_crystal[player_name] then
- local node = minetest.get_node_or_nil(using_crystal[player_name])
- if node then
- local element = to_element[node.param2]
- local item_name = minetest.registered_items.unknown.description
- if minetest.registered_items[element]
- and minetest.registered_items[element].description then
- item_name = minetest.registered_items[element].description
- end
- local meta = minetest.get_meta(using_crystal[player_name])
- local count
- -- A special value to take it all.
- if attempted_count == "all" then
- attempted_count = meta:get_int("count")
- count = attempted_count
- -- A special value to take a stack.
- elseif attempted_count == "stack" then
- local stack_max
- if minetest.registered_items[element] then
- stack_max = minetest.registered_items[element].stack_max
- else
- stack_max = minetest.registered_items.unknown.stack_max
- end
- attempted_count = stack_max
- -- You can't take a full stack unless you have a full stack in storage.
- count = math.min(stack_max, meta:get_int("count"))
- elseif attempted_count > 0 then
- -- You can't take more than the crystal has.
- count = math.min(attempted_count, meta:get_int("count"))
- else
- -- Withdrawing negative amounts isn't implemented. I wasn't sure if
- -- this special case needed to be checked for or if the code would just
- -- do nothing when given a negative number, so I ran a test to find
- -- out. As it turns out, allowing negative numbers will add to the
- -- amount stored in the crystal while not taking anything from the
- -- player. The player can simply mint their own items, and withdraw
- -- them when they're done. To avoid this, we simply disallow negative
- -- values.
- minetest.chat_send_player(player_name, minetest.colorize(palette_colour[node.param2], 'Negative withdrawals are not supported. Please use deposits instead. To deposit, hold "shift" and click on a stack of '..item_name..'s in your inventory.'))
- return
- end
- local stack = ItemStack(element)
- local withdrawn = 0
- local inv = player:get_inventory()
- while count ~= 0 do
- if count < 65535 then
- stack:set_count(count)
- withdrawn = withdrawn + count
- count = 0
- else
- stack:set_count(65535)
- count = count - 65535
- withdrawn = withdrawn + 65535
- end
- remainder = inv:add_item("main", stack)
- if remainder:get_count() ~= 0 then
- withdrawn = withdrawn - remainder:get_count()
- count = 0
- end
- end
- meta:set_int("count", meta:get_int("count") - withdrawn)
- send_formspec(player_name)
- if withdrawn == attempted_count then
- minetest.chat_send_player(player_name, minetest.colorize(palette_colour[node.param2], "You withdrew "..withdrawn.." "..item_name.."s."))
- else
- minetest.chat_send_player(player_name, minetest.colorize(palette_colour[node.param2], "You tried to withdraw "..attempted_count.." "..item_name.."s, but were only able to withdraw "..withdrawn.."."))
- end
- end
- end
- end
- local magic_input_inv = minetest.create_detached_inventory("elemental", {
- allow_put = function(inv, listname, index, stack, player)
- local player_name = player:get_player_name()
- if not using_crystal[player_name] then
- return 0
- end
- local node = minetest.get_node_or_nil(using_crystal[player_name])
- local meta = minetest.get_meta(using_crystal[player_name])
- local infotext = meta:get_string("infotext")
- if not node then
- minetest.chat_send_player(player_name, "The crystal you're trying to deposit into is out of range.")
- elseif not infotext then
- minetest.chat_send_player(player_name, minetest.colorize(palette_colour[node.param2], "The crystal you're attempting to deposit into appears to be damaged, and is unable to accept items."))
- end
- local owner = infotext:split("'")[1]
- if owner ~= player_name then
- minetest.chat_send_player(player_name, minetest.colorize(palette_colour[node.param2], "This crystal doesn't belong to you."))
- return 0
- end
- local item_name = minetest.registered_items.unknown.description
- local element = to_element[node.param2]
- if minetest.registered_items[element]
- and minetest.registered_items[element].description then
- item_name = minetest.registered_items[element].description
- end
- if stack:get_name() ~= element then
- minetest.chat_send_player(player_name, minetest.colorize(palette_colour[node.param2], "Only "..item_name.."s can be deposited into this crystal."))
- return 0
- end
- local count = stack:get_count()
- if max_storage >= count + meta:get_int("count") then
- minetest.chat_send_player(player_name, minetest.colorize(palette_colour[node.param2], "You deposited "..count.." "..item_name.."s."))
- return -1
- else
- minetest.chat_send_player(player_name, minetest.colorize(palette_colour[node.param2], "The crystal is too full to accept that many "..item_name.."s."))
- return 0
- end
- end,
- allow_take = function()
- return 0
- end,
- on_put = function(inv, listname, index, stack, player)
- local player_name = player:get_player_name()
- if not using_crystal[player_name] then
- give_to_player(player, stack)
- end
- local meta = minetest.get_meta(using_crystal[player_name])
- meta:set_int("count", meta:get_int("count") + stack:get_count())
- send_formspec(player_name)
- end,
- })
- magic_input_inv:set_size("input", 1)
- minetest.register_node("elemental:0", {
- stack_max = 65535,
- drawtype = "mesh",
- mesh = "elemental-pillar.obj",
- description = "Elemental Crystal",
- paramtype = "light",
- tiles = {
- {
- name = "default_mossycobble.png",
- color = "#fff",
- },
- "elemental-crystal.png",
- },
- light_source = LIGHT_MAX,
- paramtype2 = "color",
- palette = "elemental-palette.png",
- groups = {cracky = 3},
- can_dig = function(pos, player)
- local meta = minetest.get_meta(pos)
- local infotext = meta:get_string("infotext")
- if not player then
- -- Non-players that ask permission to dig the node are denied it. This
- -- doesn't protect against all non-player situations though, as most
- -- non-players won't ask permission first.
- return false
- elseif infotext then
- local owner = infotext:split("'")[1]
- return owner == player:get_player_name() and meta:get_int("count") == 0
- else
- -- If a mod corrupts the pillar and removes the owner information, we
- -- allow all players to remove the pillar. Otherwise, no one would be
- -- allowed to remove it, and it'd be stuck on the map permanently.
- return meta:get_int("count") == 0
- end
- end,
- selection_box = {
- type = "fixed",
- fixed = {
- {-0.125, -0.5, -0.125, 0.125, 0, 0.125},
- },
- },
- collision_box = {
- type = "fixed",
- fixed = {
- {-0.125, -0.5, -0.125, 0.125, 0, 0.125},
- },
- },
- after_place_node = function(pos, placer, itemstack, pointed_thing)
- local element = to_element[minetest.get_node(pos).param2]
- local infotext
- if minetest.registered_items[element] and minetest.registered_items[element].description then
- infotext = placer:get_player_name().."'s "..minetest.registered_items[element].description.." Elemental Crystal"
- else
- infotext = placer:get_player_name().."' Elemental Pillar"
- end
- local meta = minetest.get_meta(pos)
- meta:set_string("infotext", infotext)
- end,
- on_rightclick = function(pos, node, clicker, itemstack, pointed_thing)
- if clicker then
- local owner = minetest.get_meta(pos):get_string("infotext"):split("'")[1]
- using_crystal[clicker:get_player_name()] = pos
- send_formspec(clicker:get_player_name())
- end
- end,
- on_blast = function() end,
- after_dig_node = function(pos, oldnode, oldmetadata, digger)
- if oldmetadata.fields.infotext then
- using_crystal[oldmetadata.fields.infotext:split("'")[1]] = nil
- end
- end,
- })
- liblevelup.register.update_function(function(digger, drops)
- for _, drop in next, drops do
- local element = ItemStack(drop):get_name()
- if to_index[element] and to_index[element] < 26
- and minetest.registered_items[element] then
- local player_name = digger:get_player_name()
- local key = player_name..";"..to_index[element]
- local stat = liblevelup.get.drop_stat(player_name, element)
- local next_at = (storage:get_int(key)+1)*41*minetest.registered_items[element].stack_max
- if stat >= next_at then
- storage:set_int(key, storage:get_int(key)+1)
- give_to_player(digger, minetest.itemstring_with_palette("elemental:0", to_index[element]))
- end
- end
- end
- end)
- minetest.register_on_player_receive_fields(function(player, formname, fields)
- if formname == "elemental:0" and fields.withdraw then
- if fields.withdraw == "Withdraw maximum" then
- withdraw(player, "all")
- elseif fields.withdraw == "Withdraw stack" then
- withdraw(player, "stack")
- else
- -- only integers are allowed.
- local count = tonumber(fields.count)
- if count then
- withdraw(player, math.floor(count))
- end
- end
- end
- end)
|