diff --git a/mods/CORE/mcl_util/init.lua b/mods/CORE/mcl_util/init.lua index b9c510390..56a4aaa3d 100644 --- a/mods/CORE/mcl_util/init.lua +++ b/mods/CORE/mcl_util/init.lua @@ -76,6 +76,15 @@ function mcl_util.mcl_log(message, module, bypass_default_logger) minetest.log(selected_module .. " " .. message) end end +function mcl_util.make_mcl_logger(label, option) + -- Return dummy function if debug option isn't set + if not minetest.settings:get_bool(option,false) then return function() end, false end + + local label_text = "["..tostring(label).."]" + return function(message) + mcl_util.mcl_log(message, label_text, true) + end, true +end local player_timers = {} @@ -231,13 +240,7 @@ function mcl_util.hopper_push(pos, dst_pos) return ok end --- Try pulling from source inventory to hopper inventory ----@param pos Vector ----@param src_pos Vector -function mcl_util.hopper_pull(pos, src_pos) - local hop_inv = minetest.get_meta(pos):get_inventory() - local hop_list = 'main' - +function mcl_util.hopper_pull_to_inventory(hop_inv, hop_list, src_pos, pos) -- Get node pos' for item transfer local src = minetest.get_node(src_pos) if not minetest.registered_nodes[src.name] then return end @@ -253,7 +256,7 @@ function mcl_util.hopper_pull(pos, src_pos) else local src_meta = minetest.get_meta(src_pos) src_inv = src_meta:get_inventory() - stack_id = mcl_util.select_stack(src_inv, src_list, hop_inv, hop_list, nil, 1) + stack_id = mcl_util.select_stack(src_inv, src_list, hop_inv, hop_list) end if stack_id ~= nil then @@ -263,6 +266,12 @@ function mcl_util.hopper_pull(pos, src_pos) end end end +-- Try pulling from source inventory to hopper inventory +---@param pos Vector +---@param src_pos Vector +function mcl_util.hopper_pull(pos, src_pos) + return mcl_util.hopper_pull_to_inventory(minetest.get_meta(pos):get_inventory(), "main", src_pos, pos) +end local function drop_item_stack(pos, stack) if not stack or stack:is_empty() then return end @@ -753,3 +762,78 @@ function mcl_util.remove_entity(luaentity) luaentity.object:remove() end +local function table_merge(base, overlay) + for k,v in pairs(overlay) do + if type(base[k]) == "table" and type(v) == "table" then + table_merge(base[k], v) + else + base[k] = v + end + end + return base +end +mcl_util.table_merge = table_merge + +function mcl_util.table_keys(t) + local keys = {} + for k,_ in pairs(t) do + keys[#keys + 1] = k + end + return keys +end + +local uuid_to_aoid_cache = {} +local function scan_active_objects() + -- Update active object ids for all active objects + for active_object_id,o in pairs(minetest.luaentities) do + o._active_object_id = active_object_id + if o._uuid then + uuid_to_aoid_cache[o._uuid] = active_object_id + end + end +end +function mcl_util.get_active_object_id(obj) + local le = obj:get_luaentity() + + -- If the active object id in the lua entity is correct, return that + if le._active_object_id and minetest.luaentities[le._active_object_id] == le then + return le._active_object_id + end + + scan_active_objects() + + return le._active_object_id +end +function mcl_util.get_active_object_id_from_uuid(uuid) + return uuid_to_aoid_cache[uuid] or scan_active_objects() or uuid_to_aoid_cache[uuid] +end +function mcl_util.get_luaentity_from_uuid(uuid) + return minetest.luaentities[ mcl_util.get_active_object_id_from_uuid(uuid) ] +end +function mcl_util.assign_uuid(obj) + assert(obj) + + local le = obj:get_luaentity() + if not le._uuid then + le._uuid = mcl_util.gen_uuid() + end + + -- Update the cache with this new id + local aoid = mcl_util.get_active_object_id(obj) + uuid_to_aoid_cache[le._uuid] = aoid + + return le._uuid +end +function mcl_util.metadata_last_act(meta, name, delay) + local last_act = meta:get_float(name) + local now = minetest.get_us_time() * 1e-6 + if last_act > now + 0.5 then + -- Last action was in the future, clock went backwards, so reset + elseif last_act >= now - delay then + return false + end + + meta:set_float(name, now) + return true +end + diff --git a/mods/CORE/vl_legacy/API.md b/mods/CORE/vl_legacy/API.md new file mode 100644 index 000000000..ceb11aad1 --- /dev/null +++ b/mods/CORE/vl_legacy/API.md @@ -0,0 +1,35 @@ +# Legacy Code Support Functions + +## `vl_legacy.deprecated(description, replacement)` + +Creates a wrapper that logs calls to deprecated function. + +Arguments: +* `description`: The text logged when the deprecated function is called. +* `replacement`: The function that should be called instead. This is invoked passing + along the parameters exactly as provided. + +## `vl_legacy.register_item_conversion` + +Allows automatic conversion of items. + +Arguments: +* `old`: Itemstring to be converted +* `new`: New item string + +## `vl_legacy.convert_node(pos, node)` + +Converts legacy nodes to newer versions. + +Arguments: +* `pos`: Position of the node to attempt conversion +* `node`: Node definition to convert. The node will be loaded from map data if `nil`. + +The node definition for the old node must contain the field `_vl_legacy_convert` with +a value that is either a `function(pos, node)` or `string` for this call to have any +affect. If a function is provided, the function is called with `pos` and `node` as +arguments. If a string is provided, a node name conversion will occur. + +This mod provides an LBM and ABM that will automatically call this function for nodes +with `group:legacy` set. + diff --git a/mods/CORE/vl_legacy/init.lua b/mods/CORE/vl_legacy/init.lua new file mode 100644 index 000000000..cb6c6389c --- /dev/null +++ b/mods/CORE/vl_legacy/init.lua @@ -0,0 +1,75 @@ +local mod = {} +vl_legacy = mod + +function mod.deprecated(description, func) + return function(...) + minetest.log("warning",description .. debug.traceback()) + return func(...) + end +end + +local item_conversions = {} +mod.registered_item_conversions = item_conversions + +function mod.register_item_conversion(old, new, func) + item_conversions[old] = {new, func} +end +function mod.convert_inventory_lists(lists) + for _,list in pairs(lists) do + for i = 1,#list do + local itemstack = list[i] + local conversion = itemstack and item_conversions[itemstack:get_name()] + if conversion then + local new_name,func = conversion[1],conversion[2] + if func then + func(itemstack) + else + itemstack:set_name(new_name) + end + end + end + end +end +function mod.convert_inventory(inv) + local lists = inv:get_lists() + mod.convert_inventory_lists(lists) + inv:set_lists(lists) +end +function mod.convert_node(pos, node) + local node = node or minetest.get_node(pos) + local node_def = minetest.registered_nodes[node.name] + local convert = node_def._vl_legacy_convert_node + if type(convert) == "function" then + convert(pos, node) + elseif type(convert) == "string" then + node.name = convert + minetest.swap_node(pos, node) + end +end + +minetest.register_on_joinplayer(function(player) + mod.convert_inventory(player:get_inventory()) +end) + +minetest.register_lbm({ + name = "vl_legacy:convert_container_inventories", + nodenames = "group:container", + run_at_every_load = true, + action = function(pos, node) + local meta = minetest.get_meta(pos) + mod.convert_inventory(meta:get_inventory()) + end +}) +minetest.register_lbm({ + name = "vl_legacy:convert_nodes", + nodenames = "group:legacy", + run_at_every_load = true, + action = mod.convert_node, +}) +minetest.register_abm({ + label = "Convert Legacy Nodes", + nodenames = "group:legacy", + interval = 5, + chance = 1, + action = mod.convert_node, +}) diff --git a/mods/CORE/vl_legacy/mod.conf b/mods/CORE/vl_legacy/mod.conf new file mode 100644 index 000000000..11b6cd01b --- /dev/null +++ b/mods/CORE/vl_legacy/mod.conf @@ -0,0 +1,3 @@ +name = vl_legacy +author = teknomunk +description = API to ease conversion of items, deprecated function logging and similar functions diff --git a/mods/ENTITIES/mcl_entity_invs/init.lua b/mods/ENTITIES/mcl_entity_invs/init.lua index 35af491e1..4bcffede4 100644 --- a/mods/ENTITIES/mcl_entity_invs/init.lua +++ b/mods/ENTITIES/mcl_entity_invs/init.lua @@ -27,28 +27,33 @@ local inv_callbacks = { } function mcl_entity_invs.load_inv(ent,size) - mcl_log("load_inv") if not ent._inv_id then return end - mcl_log("load_inv 2") local inv = minetest.get_inventory({type="detached", name=ent._inv_id}) if not inv then - mcl_log("load_inv 3") inv = minetest.create_detached_inventory(ent._inv_id, inv_callbacks) inv:set_size("main", size) - if ent._items then + if ent._mcl_entity_invs_load_items then + local lists = ent:_mcl_entity_invs_load_items() + vl_legacy.convert_inventory_lists(lists) + inv:set_list("main", lists) + elseif ent._items then + vl_legacy.convert_inventory_lists(ent._items) inv:set_list("main",ent._items) end - else - mcl_log("load_inv 4") end return inv end function mcl_entity_invs.save_inv(ent) if ent._inv then - ent._items = {} + local items = {} for i,it in ipairs(ent._inv:get_list("main")) do - ent._items[i] = it:to_string() + items[i] = it:to_string() + end + if ent._mcl_entity_invs_save_items then + ent:_mcl_entity_invs_save_items(items) + else + ent._items = items end minetest.remove_detached_inventory(ent._inv_id) ent._inv = nil @@ -108,7 +113,11 @@ function mcl_entity_invs.show_inv_form(ent,player,text) local playername = player:get_player_name() - minetest.show_formspec(playername, ent._inv_id, load_default_formspec (ent, text)) + -- Workaround: wait at least 50ms to ensure that the detached inventory exists before + -- the formspec attempts to use it. (See https://git.minetest.land/VoxeLibre/VoxeLibre/issues/4670#issuecomment-84875) + minetest.after(0.05, function() + minetest.show_formspec(playername, ent._inv_id, load_default_formspec (ent, text)) + end) end local function drop_inv(ent) diff --git a/mods/ENTITIES/mcl_entity_invs/mod.conf b/mods/ENTITIES/mcl_entity_invs/mod.conf index 8e94d6b1e..64f92aea9 100644 --- a/mods/ENTITIES/mcl_entity_invs/mod.conf +++ b/mods/ENTITIES/mcl_entity_invs/mod.conf @@ -1,3 +1,3 @@ name = mcl_entity_invs author = cora -depends = mcl_formspec +depends = mcl_formspec, vl_legacy diff --git a/mods/ENTITIES/mcl_item_entity/init.lua b/mods/ENTITIES/mcl_item_entity/init.lua index 7f5056617..2bc530397 100644 --- a/mods/ENTITIES/mcl_item_entity/init.lua +++ b/mods/ENTITIES/mcl_item_entity/init.lua @@ -845,6 +845,7 @@ minetest.register_entity(":__builtin:item", { _insta_collect = self._insta_collect, _flowing = self._flowing, _removed = self._removed, + _immortal = self._immortal, }) -- sfan5 guessed that the biggest serializable item -- entity would have a size of 65530 bytes. This has @@ -897,6 +898,7 @@ minetest.register_entity(":__builtin:item", { self._insta_collect = data._insta_collect self._flowing = data._flowing self._removed = data._removed + self._immortal = data._immortal end else self.itemstring = staticdata @@ -990,7 +992,7 @@ minetest.register_entity(":__builtin:item", { if self._collector_timer then self._collector_timer = self._collector_timer + dtime end - if time_to_live > 0 and self.age > time_to_live then + if time_to_live > 0 and ( self.age > time_to_live and not self._immortal ) then self._removed = true self.object:remove() return diff --git a/mods/ENTITIES/mcl_minecarts/API.md b/mods/ENTITIES/mcl_minecarts/API.md new file mode 100644 index 000000000..1f90c32f4 --- /dev/null +++ b/mods/ENTITIES/mcl_minecarts/API.md @@ -0,0 +1,284 @@ +# Table of Contents +1. [Useful Constants](#useful-constants) +2. [Rail](#rail) + 1. [Constants](#constants) + 2. [Functions](#functions) + 3. [Node Definition Options](#node-definition-options) +3. [Cart Functions](#cart-functions) +4. [Cart Data Functions](#cart-data-functions) +5. [Cart-Node Interactions](#cart-node-interactions) +6. [Train Functions](#train-functions) + +## Useful Constants + +- `mcl_minecarts.north` +- `mcl_minecarts.south` +- `mcl_minecarts.east` +- `mcl_minecarts.west` + +Human-readable names for the cardinal directions. + +- `mcl_minecarts.SPEED_MAX` + +Maximum speed that minecarts will be accelerated to with powered rails, in blocks per +second. Defined as 10 blocks/second. + +- `mcl_minecarts.CART_BLOCKS_SIZE` + +The size of blocks to use when searching for carts to respawn. Defined as is 64 blocks. + +- `mcl_minecarts.FRICTION` + +Rail friction. Defined as is 0.4 blocks/second^2. + +- `mcl_minecarts.MAX_TRAIN_LENGTH` + +The maximum number of carts that can be in a single train. Defined as 4 carts. + +- `mcl_minecarts.PASSENGER_ATTACH_POSITION` + +Where to attach passengers to the minecarts. + +## Rail + +### Constants + +`mcl_minecarts.HORIZONTAL_CURVES_RULES` +`mcl_minecarts.HORIZONTAL_STANDARD_RULES` + +Rail connection rules. Each rule is a table with the following indexes: + +1. `node_name_suffix` - The suffix added to a node's `_mcl_minecarts.base_name` to + get the name of the node to use for this connection. +2. `param2_value` - The value of the node's param2. Used to specify rotation. + +and the following named options: + +- `mask` - Directional connections mask +- `score` - priority of the rule. If more than one rule matches, the one with the + highest store is selected. +- `can_slope` - true if the result of this rule can be converted into a slope. + +`mcl_minecarts.RAIL_GROUPS.STANDARD` +`mcl_minecarts.RAIL_GROUPS.CURVES` + +These constants are used to specify a rail node's `group.rail` value. + +### Functions + +`mcl_minecarts.get_rail_connections(node_position, options)` + +Calculate the rail adjacency information for rail placement. Arguments are: + +- `node_position` - the location of the node to calculate adjacency for. +- `options` - A table containing any of these options: + - `legacy`- if true, don't check that a connection proceeds out in a direction + a cart can travel. Used for converting legacy rail to newer equivalents. + - `ignore_neightbor_connections` - if true, don't check that a cart could leave + the neighboring node from this direction. + +`mcl_minecarts.is_rail(position, railtype)` + +Determines if the node at `position` is a rail. If `railtype` is provided, +determine if the node at `position` is that type of rail. + +`mcl_minecarts.register_rail(itemstring, node_definition)` + +Registers a rail with a few sensible defaults and if a craft recipe was specified, +register that as well. + +`mcl_minecarts.register_straight_rail(base_name, tiles, node_definition)` + +Registers a rail with only straight and sloped variants. + +`mcl_minecarts.register_curves_rail(base_name, tiles, node_definition)` + +Registers a rail with straight, sloped, curved, tee and cross variants. + +`mcl_minecarts.update_rail_connections(node_position, options)` + +Converts the rail at `node_position`, if possible, another variant (curve, etc.) +and rotates the node as needed so that rails connect together. `options` is +passed thru to `mcl_minecarts.get_rail_connections()` + +`mcl_minecarts.get_rail_direction(rail_position, cart_direction)` + +Returns the next direction a cart traveling in the direction specified in `cart_direction` +will travel from the rail located at `rail_position`. + +### Node Definition Options + +`_mcl_minecarts.railtype` + +This declares the variant type of the rail. This will be one of the following: + +- "straight" - two connections opposite each other and no vertical change. +- "sloped" - two connections opposite each other with one of these connections + one block higher. +- "corner" - two connections at 90 degrees from each other. +- "tee" - three connections +- "cross" - four connections allowing only straight-thru movement + +#### Hooks +`_mcl_minecarts.get_next_dir = function(node_position, current_direction, node)` + +Called to get the next direction a cart will travel after passing thru this node. + +## Cart Functions + +`mcl_minecarts.attach_driver(cart, player)` + +This attaches (ObjectRef) `player` to the (LuaEntity) `cart`. + +`mcl_minecarts.detach_minecart(cart_data)` + +This detaches a minecart from any rail it is attached to and makes it start moving +as an entity affected by gravity. It will keep moving in the same direction and +at the same speed it was moving at before it detaches. + +`mcl_minecarts.get_cart_position(cart_data)` + +Compute the location of a minecart from its cart data. This works even when the entity +is unloaded. + +`mcl_minecarts.kill_cart(cart_data)` + +Kills a cart and drops it as an item, even if the cart entity is unloaded. + +`mcl_minecarts.place_minecart(itemstack, pointed_thing, placer)` + +Places a minecart at the location specified by `pointed_thing` + +`mcl_minecarts.register_minecart(minecart_definition)` + +Registers a minecart. `minecart_definition` defines the entity. All the options supported by +normal minetest entities are supported, with a few additions: + +- `craft` - Crafting recipe for this cart. +- `drop` - List of items to drop when the cart is killed. (required) +- `entity_id` - The entity id of the cart. (required) +- `itemstring` - This is the itemstring to use for this entity. (required) + +`mcl_minecarts.reverse_cart_direction(cart_data)` + +Force a minecart to start moving in the opposite direction of its current direction. + +`mcl_minecarts.snap_direction(direction_vector)` + +Returns a valid cart movement direction that has the smallest angle between it and `direction_vector`. + +`mcl_minecarts.update_cart_orientation(cart)` + +Updates the rotation of a cart entity to match the cart's data. + +## Cart Data Functions + +`mcl_minecarts.destroy_cart_data(uuid)` + +Destroys the data for the cart with the identitfier in `uuid`. + +`mcl_minecarts.find_carts_by_block_map(block_map)` + +Returns a list of cart data for carts located in the blocks specified in `block_map`. Used +to respawn carts entering areas around players. + +`mcl_minecarts.add_blocks_to_map(block_map, min_pos, max_pos)` + +Add blocks that fully contain `min_pos` and `max_pos` to `block_map` for use by + `mcl_minecarts.find_cart_by_block_map`. + +`mcl_minecarts.get_cart_data(uuid)` + +Loads the data for the cart with the identitfier in `uuid`. + +`mcl_minecarts.save_cart_data(uuid)` + +Saves the data for the cart with the identifier in `uuid`. + +`mcl_minecart.update_cart_data(data)` + +Replaces the cart data for the cart with the identifier in `data.uuid`, then saves +the data. + +## Cart-Node Interactions + +As the cart moves thru the environment, it can interact with the surrounding blocks +thru a number of handlers in the block definitions. All these handlers are defined +as: + +`function(node_position, cart_luaentity, cart_direction, cart_position)` + +Arguments: +- `node_position` - position of the node the cart is interacting with +- `cart_luaentity` - The luaentity of the cart that is entering this block. Will + be nil for minecarts moving thru unloaded blocks +- `cart_direction` - The direction the cart is moving +- `cart_position` - The location of the cart +- `cart_data` - Information about the cart. This will always be defined. + +There are several variants of this handler: +- `_mcl_minecarts_on_enter` - The cart enters this block +- `_mcl_minecarts_on_enter_below` - The cart enters above this block +- `_mcl_minecarts_on_enter_above` - The cart enters below this block +- `_mcl_minecarts_on_enter_side` - The cart enters beside this block + +Mods can also define global handlers that are called for every node. These +handlers are defined as: + +`function(node_position, cart_luaentity, cart_direction, node_definition, cart_data)` + +Arguments: +- `node_position` - position of the node the cart is interacting with +- `cart_luaentity` - The luaentity of the cart that is entering this block. Will + be nil for minecarts moving thru unloaded blocks +- `cart_direction` - The direction the cart is moving +- `cart_position` - The location of the cart +- `cart_data` - Information about the cart. This will always be defined. +- `node_definition` - The definition of the node at `node_position` + +The available hooks are: +- `_mcl_minecarts.on_enter` - The cart enters this block +- `_mcl_minecarts.on_enter_below` - The cart enters above this block +- `_mcl_minecarts.on_enter_above` - The cart enters below this block +- `_mcl_minecarts.on_enter_side` - The cart enters beside this block + +Only a single function can be installed in each of these handlers. Before installing, +preserve the existing handler and call it from inside your handler if not `nil`. + +## Train Functions + +`mcl_minecarts.break_train_at(cart_data)` + +Splits a train apart at the specified cart. + +`mcl_minecarts.distance_between_cars(cart1_data, cart2_data)` + +Returns the distance between two carts even if both entities are unloaded, or nil if either +cart is not on a rail. + +`mcl_minecarts.is_in_same_train(cart1_data, cart2_data)` + +Returns true if cart1 and cart2 are a part of the same train and false otherwise. + +`mcl_minecarts.link_cart_ahead(cart_data, cart_ahead_data)` + +Given two carts, link them together into a train, with the second cart ahead of the first. + +`mcl_minecarts.train_cars(cart_data)` + +Use to iterate over all carts in a train. Expected usage: + +`for cart in mcl_minecarts.train_cars(cart) do --[[ code ]] end` + +`mcl_minecarts.reverse_train(cart)` + +Make all carts in a train reverse and start moving in the opposite direction. + +`mcl_minecarts.train_length(cart_data)` + +Compute the current length of the train containing the cart whose data is `cart_data`. + +`mcl_minecarts.update_train(cart_data)` + +When provided with the rear-most cart of a tain, update speeds of all carts in the train +so that it holds together and moves as a unit. diff --git a/mods/ENTITIES/mcl_minecarts/DOC.md b/mods/ENTITIES/mcl_minecarts/DOC.md new file mode 100644 index 000000000..00f799e51 --- /dev/null +++ b/mods/ENTITIES/mcl_minecarts/DOC.md @@ -0,0 +1,120 @@ + +## Organization +- [ init.lua](./init.lua) - module entrypoint. The other files are included from here + and several constants are defined here + +- [carts.lua](./carts/lua) - This file contains code related to cart entities, cart + type registration, creation, estruction and updating. The global step function + responsible for updating attached carts is in this file. The various carts are + referenced from this file but actually reside in the subdirectory [carts/](./carts/). + +- [functions.lua](./functions.lua) - This file contains various minecart and rail + utility functions used by the rest of the code. + +- [movement.lua](./movement.lua) - This file contains the code related to cart + movement physics. + +- [rails.lua](./rails.lua) - This file contains code related to rail registation, + placement, connection rules and cart direction selection. This contains the rail + behaviors and the LBM code for updating legacy rail nodes to the new versions + that don't use the raillike draw type. + +- [storage.lua](./storage.lua) - This file contains the code than manages minecart + state data to allow processing minecarts while entities are unloaded. + +- [train.lua](./train.lua) - This file contains code related to multi-car trains. + +## Rail Nodes + +Previous versions of mcl\_minecarts used one node type for each rail type (standard, +powered, detector and activator) using the raillike draw type that minetest provides. +This version does not use the raillike draw type and instead uses a 1/16th of a block +high nodebox and uses an additional node definition for each variant. The variants +present are: + +- straight +- sloped +- corner +- tee +- cross + +Of the rail types provided by this module, standard has all of these variants. The +remaining types only have straight and sloped variants. + +Unlike the old rail type, this version will only update connections when placed, and +will only place a variant that already has connections into the space the rail is +being placed. Here is how to create the various varients: + +- Straight rail is placed when with zero or one adjacent rail nodes. If no rails + are adjacent, the rail is placed in line with the direction the player is facing. + If there is exactly one adjacent rail present, the straight rail will always rotate + to connect to it. + +- Sloped rail is placed when there are two rails in a straight line, with one being + one block higher. When rail is placed adjacent to a straight rail one block lower + and the rail is facing the block the rail is being placed on, the lower rail will + convert into a slope. + +- A corner rail is placed when there are exactly two adjacent rails that are not in + a line and lead into the space the rail is being placed. The corner will be rotated + to connect these two rails. + +- A tee rail is placed where there are exactly three rails adjact and those existing + rails lead into the the space the new rail is being placed. + +- A rail cross is placed when there is rail in all four adjacent blocks and they all + have a path into the space the new rail is being placed. + +The tee variant will interact with redstone and mesecons to switch the curved section. + +## On-rail Minecart Movement + +Minecart movement is handled in two distinct regimes: on a rail and off. The +off-rail movement is handled with minetest's builtin entity movement handling. +The on-rail movement is handled with a custom algorithm. This section details +the latter. + +The data for on-rail minecart movement is stored entirely inside mod storage +and indexed by a hex-encoded 128-bit universally-unique identifier (uuid). Minecart +entities store this uuid and a sequence identifier. The code for handling this +storage is in [storage.lua](./storage.lua). This was done so that minecarts can +still move while no players are connected or when out of range of players. Inspiration +for this was the [Advanced Trains mod](http://advtrains.de/). This is a behavior difference +when compared to minecraft, as carts there will stop movement when out of range of +players. + +Processing for minecart movement is as follows: +1. In a globalstep handler in [carts.lua](./carts.lua), determine which carts are + moving. +2. Call `do_movement` in [movement.lua](./movement.lua) to update + each cart's location and handle interactions with the environment. + 1. Each movement is broken up into one or more steps that are completely + contained inside a block. This prevents carts from ever jumping from + one rail to another over a gap or thru solid blocks because of server + lag. Each step is processed with `do_movement_step` + 2. Each step uses physically accurate, timestep-independent physics + to move the cart. Calculating the acceleration to apply to a cart + is broken out into its own function (`calculate_acceperation`). + 3. As the cart enters and leaves blocks, handlers in nearby blocks are called + to allow the cart to efficiently interact with the environment. Handled by + the functions `handle_cart_enter` and `handle_cart_leave` + 4. The cart checks for nearby carts and collides elastically with these. The + calculations for these collisions are in the function `handle_cart_collision` + 5. If the cart enters a new block, determine the new direction the cart will + move with `mcl_minecarts:get_rail_direction` in [functions.lua](./functions.lua). + The rail nodes provide a hook `_mcl_minecarts.get_next_direction` that + provides this information based on the previous movement direction. +3. If an entity exists for a given cart, the entity will update its position + while loaded in. + +Cart movement when on a rail occurs regarless of whether an entity for that +cart exists or is loaded into memory. As a consequence of this movement, it +is possible for carts with unloaded entities to enter range of a player. +To handle this, periodic checks are performed around players and carts that +are within range but don't have a cart have a new entity spawned. + +Every time a cart has a new entity spawned, it increases a sequence number in +the cart data to allow removing old entities from the minetest engine. Any cart +entity that does not have the current sequence number for a minecart gets removed +once processing for that entity resumes. + diff --git a/mods/ENTITIES/mcl_minecarts/README.txt b/mods/ENTITIES/mcl_minecarts/README.txt index 112cbd308..f0f8123ee 100644 --- a/mods/ENTITIES/mcl_minecarts/README.txt +++ b/mods/ENTITIES/mcl_minecarts/README.txt @@ -10,6 +10,7 @@ MIT License Copyright (C) 2012-2016 PilzAdam Copyright (C) 2014-2016 SmallJoker Copyright (C) 2012-2016 Various Minetest developers and contributors +Copyright (C) 2024 teknomunk Authors/licenses of media files: ----------------------- diff --git a/mods/ENTITIES/mcl_minecarts/carts.lua b/mods/ENTITIES/mcl_minecarts/carts.lua new file mode 100644 index 000000000..ecf9eca7e --- /dev/null +++ b/mods/ENTITIES/mcl_minecarts/carts.lua @@ -0,0 +1,695 @@ +local modname = minetest.get_current_modname() +local modpath = minetest.get_modpath(modname) +local mod = mcl_minecarts +local S = minetest.get_translator(modname) + +local mcl_log,DEBUG = mcl_util.make_mcl_logger("mcl_logging_minecarts", "Minecarts") + +-- Imports +local CART_BLOCK_SIZE = mod.CART_BLOCK_SIZE +local table_merge = mcl_util.table_merge +local get_cart_data = mod.get_cart_data +local save_cart_data = mod.save_cart_data +local update_cart_data = mod.update_cart_data +local destroy_cart_data = mod.destroy_cart_data +local find_carts_by_block_map = mod.find_carts_by_block_map +local movement = dofile(modpath.."/movement.lua") +assert(movement.do_movement) +assert(movement.do_detached_movement) +assert(movement.handle_cart_enter) + +-- Constants +local max_step_distance = 0.5 +local MINECART_MAX_HP = 4 +local TWO_OVER_PI = 2 / math.pi + +local function detach_driver(self) + local staticdata = self._staticdata + + if not self._driver then + return + end + + -- Update player infomation + local driver_name = self._driver + local playerinfo = mcl_playerinfo[driver_name] + if playerinfo then + playerinfo.attached_to = nil + end + mcl_player.player_attached[driver_name] = nil + + minetest.log("action", driver_name.." left a minecart") + + -- Update cart informatino + self._driver = nil + self._start_pos = nil + local player_meta = mcl_playerinfo.get_mod_meta(driver_name, modname) + player_meta.attached_to = nil + + -- Detatch the player object from the minecart + local player = minetest.get_player_by_name(driver_name) + if player then + local dir = staticdata.dir or vector.new(1,0,0) + local cart_pos = mod.get_cart_position(staticdata) or self.object:get_pos() + local new_pos = vector.offset(cart_pos, -dir.z, 0, dir.x) + player:set_detach() + --print("placing player at "..tostring(new_pos).." from cart at "..tostring(cart_pos)..", old_pos="..tostring(player:get_pos()).."dir="..tostring(dir)) + + -- There needs to be a delay here or the player's position won't update + minetest.after(0.1,function(driver_name,new_pos) + local player = minetest.get_player_by_name(driver_name) + player:moveto(new_pos, false) + end, driver_name, new_pos) + + player:set_eye_offset(vector.zero(),vector.zero()) + mcl_player.player_set_animation(player, "stand" , 30) + --else + --print("No player object found for "..driver_name) + end +end +mod.detach_driver = detach_driver + +function mod.kill_cart(staticdata, killer) + local pos + mcl_log("cart #"..staticdata.uuid.." was killed") + + -- Leave nodes + if staticdata.attached_at then + handle_cart_leave(self, staticdata.attached_at, staticdata.dir ) + else + --mcl_log("TODO: handle detatched minecart death") + end + + -- Handle entity-related items + local le = mcl_util.get_luaentity_from_uuid(staticdata.uuid) + if le then + pos = le.object:get_pos() + + detach_driver(le) + + -- Detach passenger + if le._passenger then + local mob = le._passenger.object + mob:set_detach() + end + + -- Remove the entity + le.object:remove() + else + pos = mod.get_cart_position(staticdata) + end + + -- Drop items + if not staticdata.dropped then + -- Try to drop the cart + local entity_def = minetest.registered_entities[staticdata.cart_type] + if entity_def then + local drop_cart = true + if killer and minetest.is_creative_enabled(killer:get_player_name()) then + drop_cart = false + end + + if drop_cart then + local drop = entity_def.drop + for d=1, #drop do + minetest.add_item(pos, drop[d]) + end + end + end + + -- Drop any items in the inventory + local inventory = staticdata.inventory + if inventory then + for i=1,#inventory do + minetest.add_item(pos, inventory[i]) + end + end + + -- Prevent item duplication + staticdata.dropped = true + end + + -- Remove data + destroy_cart_data(staticdata.uuid) +end +local kill_cart = mod.kill_cart + + +-- Table for item-to-entity mapping. Keys: itemstring, Values: Corresponding entity ID +local entity_mapping = {} + +local function make_staticdata( _, connected_at, dir ) + return { + connected_at = connected_at, + distance = 0, + velocity = 0, + dir = vector.new(dir), + mass = 1, + seq = 1, + } +end + +local DEFAULT_CART_DEF = { + initial_properties = { + physical = true, + collisionbox = {-10/16., -0.5, -10/16, 10/16, 0.25, 10/16}, + visual = "mesh", + visual_size = {x=1, y=1}, + }, + + hp_max = MINECART_MAX_HP, + + groups = { + minecart = 1, + }, + + _driver = nil, -- player who sits in and controls the minecart (only for minecart!) + _passenger = nil, -- for mobs + _start_pos = nil, -- Used to calculate distance for “On A Rail” achievement + _last_float_check = nil, -- timestamp of last time the cart was checked to be still on a rail + _boomtimer = nil, -- how many seconds are left before exploding + _blinktimer = nil, -- how many seconds are left before TNT blinking + _blink = false, -- is TNT blink texture active? + _old_pos = nil, + _staticdata = nil, +} +function DEFAULT_CART_DEF:on_activate(staticdata, dtime_s) + -- Transfer older data + local data = minetest.deserialize(staticdata) or {} + if not data.uuid then + data.uuid = mcl_util.assign_uuid(self.object) + + if data._items then + data.inventory = data._items + data._items = nil + data._inv_id = nil + data._inv_size = nil + end + end + self._seq = data.seq or 1 + + local cd = get_cart_data(data.uuid) + if not cd then + update_cart_data(data) + else + if not cd.seq then cd.seq = 1 end + data = cd + end + + -- Fix up types + data.dir = vector.new(data.dir) + + -- Fix mass + data.mass = data.mass or 1 + + -- Make sure all carts have an ID to isolate them + self._uuid = data.uuid + self._staticdata = data + + -- Activate cart if on powered activator rail + if self.on_activate_by_rail then + local pos = self.object:get_pos() + local node = minetest.get_node(vector.floor(pos)) + if node.name == "mcl_minecarts:activator_rail_on" then + self:on_activate_by_rail() + end + end +end +function DEFAULT_CART_DEF:get_staticdata() + save_cart_data(self._staticdata.uuid) + return minetest.serialize({uuid = self._staticdata.uuid, seq=self._seq}) +end + +function DEFAULT_CART_DEF:_mcl_entity_invs_load_items() + local staticdata = self._staticdata + return staticdata.inventory or {} +end +function DEFAULT_CART_DEF:_mcl_entity_invs_save_items(items) + local staticdata = self._staticdata + staticdata.inventory = table.copy(items) +end + +function DEFAULT_CART_DEF:add_node_watch(pos) + local staticdata = self._staticdata + local watches = staticdata.node_watches or {} + + for i=1,#watches do + if watches[i] == pos then return end + end + + watches[#watches+1] = pos + staticdata.node_watches = watches +end +function DEFAULT_CART_DEF:remove_node_watch(pos) + local staticdata = self._staticdata + local watches = staticdata.node_watches or {} + + local new_watches = {} + for i=1,#watches do + local node_pos = watches[i] + if node_pos ~= pos then + new_watches[#new_watches + 1] = node_pos + end + end + staticdata.node_watches = new_watches +end +function DEFAULT_CART_DEF:get_cart_position() + local staticdata = self._staticdata + + if staticdata.connected_at then + return staticdata.connected_at + staticdata.dir * staticdata.distance + else + return self.object:get_pos() + end +end +function DEFAULT_CART_DEF:on_punch(puncher, time_from_last_punch, tool_capabilities, dir, damage) + if puncher == self._driver then return end + + local staticdata = self._staticdata + + if puncher:get_player_control().sneak then + mod.kill_cart(staticdata, puncher) + return + end + + local controls = staticdata.controls or {} + dir.y = 0 + dir = vector.normalize(dir) + local impulse = vector.dot(staticdata.dir, vector.multiply(dir, damage * 4)) + if impulse < 0 and staticdata.velocity == 0 then + mod.reverse_direction(staticdata) + impulse = -impulse + end + + controls.impulse = impulse + staticdata.controls = controls +end +function DEFAULT_CART_DEF:on_step(dtime) + local staticdata = self._staticdata + if not staticdata then + staticdata = make_staticdata() + self._staticdata = staticdata + end + if self._items then + self._items = nil + end + + -- Update entity position + local pos = mod.get_cart_position(staticdata) + if pos then self.object:move_to(pos) end + + -- Repair cart_type + if not staticdata.cart_type then + staticdata.cart_type = self.name + end + + -- Remove superceded entities + if staticdata.seq and (self._seq or -1) < staticdata.seq then + if not self._seq then + core.log("warning", "Removing minecart entity missing sequence number") + end + --print("removing cart #"..staticdata.uuid.." with sequence number mismatch") + self.object:remove() + self._removed = true + return + end + + -- Regen + local hp = self.object:get_hp() + local time_now = minetest.get_gametime() + if hp < MINECART_MAX_HP and (staticdata.last_regen or 0) <= time_now - 1 then + staticdata.last_regen = time_now + hp = hp + 1 + self.object:set_hp(hp) + end + + -- Cart specific behaviors + local hook = self._mcl_minecarts_on_step + if hook then hook(self,dtime) end + + if (staticdata.hopper_delay or 0) > 0 then + staticdata.hopper_delay = staticdata.hopper_delay - dtime + end + + -- Controls + local ctrl, player = nil, nil + if self._driver then + player = minetest.get_player_by_name(self._driver) + if player then + ctrl = player:get_player_control() + -- player detach + if ctrl.sneak then + detach_driver(self) + return + end + + -- Experimental controls + local now_time = minetest.get_gametime() + local controls = {} + if ctrl.up then controls.forward = now_time end + if ctrl.down then controls.brake = now_time end + controls.look = math.round(player:get_look_horizontal() * TWO_OVER_PI) % 4 + staticdata.controls = controls + end + + -- Give achievement when player reached a distance of 1000 nodes from the start position + if pos and vector.distance(self._start_pos, pos) >= 1000 then + awards.unlock(self._driver, "mcl:onARail") + end + end + + + if not staticdata.connected_at then + movement.do_detached_movement(self, dtime) + else + mod.update_cart_orientation(self) + end +end +function DEFAULT_CART_DEF:on_death(killer) + kill_cart(self._staticdata, killer) +end + +-- Create a minecart +function mod.create_minecart(entity_id, pos, dir) + -- Setup cart data + local uuid = mcl_util.gen_uuid() + local data = make_staticdata( nil, pos, dir ) + data.uuid = uuid + data.cart_type = entity_id + update_cart_data(data) + save_cart_data(uuid) + + return uuid +end +local create_minecart = mod.create_minecart + +-- Place a minecart at pointed_thing +function mod.place_minecart(itemstack, pointed_thing, placer) + if not pointed_thing.type == "node" then + return + end + + local look_4dir = math.round(placer:get_look_horizontal() * TWO_OVER_PI) % 4 + local look_dir = core.fourdir_to_dir(look_4dir) + look_dir.x = -look_dir.x + + local spawn_pos = pointed_thing.above + local cart_dir = look_dir + + local railpos, node + if mcl_minecarts.is_rail(pointed_thing.under) then + railpos = pointed_thing.under + elseif mcl_minecarts.is_rail(pointed_thing.above) then + railpos = pointed_thing.above + end + if railpos then + spawn_pos = railpos + node = minetest.get_node(railpos) + + -- Try two orientations, and select the second if the first is at an angle + cart_dir1 = mcl_minecarts.get_rail_direction(railpos, look_dir) + cart_dir2 = mcl_minecarts.get_rail_direction(railpos, -look_dir) + if vector.length(cart_dir1) <= 1 then + cart_dir = cart_dir1 + else + cart_dir = cart_dir2 + end + end + + -- Make sure to always go down slopes + if cart_dir.y > 0 then cart_dir = -cart_dir end + + local entity_id = entity_mapping[itemstack:get_name()] + + local uuid = create_minecart(entity_id, railpos, cart_dir) + + -- Create the entity with the staticdata already setup + local sd = minetest.serialize({ uuid=uuid, seq=1 }) + local cart = minetest.add_entity(spawn_pos, entity_id, sd) + local staticdata = get_cart_data(uuid) + + cart:set_yaw(minetest.dir_to_yaw(cart_dir)) + + -- Call placer + local le = cart:get_luaentity() + if le._mcl_minecarts_on_place then + le._mcl_minecarts_on_place(le, placer) + end + + if railpos then + movement.handle_cart_enter(staticdata, railpos) + end + + local pname = placer and placer:get_player_name() or "" + if not minetest.is_creative_enabled(pname) then + itemstack:take_item() + end + return itemstack +end + +local function dropper_place_minecart(dropitem, pos) + -- Don't try to place the minecart if pos isn't a rail + local node = minetest.get_node(pos) + if minetest.get_item_group(node.name, "rail") == 0 then return false end + + mod.place_minecart(dropitem, { + above = pos, + under = vector.offset(pos,0,-1,0) + }) + return true +end + +local function register_minecart_craftitem(itemstring, def) + local groups = { minecart = 1, transport = 1 } + if def.creative == false then + groups.not_in_creative_inventory = 1 + end + local item_def = { + stack_max = 1, + _mcl_dropper_on_drop = dropper_place_minecart, + on_place = function(itemstack, placer, pointed_thing) + if not pointed_thing.type == "node" then + return + end + + -- Call on_rightclick if the pointed node defines it + local node = minetest.get_node(pointed_thing.under) + if placer and not placer:get_player_control().sneak then + if minetest.registered_nodes[node.name] and minetest.registered_nodes[node.name].on_rightclick then + return minetest.registered_nodes[node.name].on_rightclick(pointed_thing.under, node, placer, itemstack) or itemstack + end + end + + return mod.place_minecart(itemstack, pointed_thing, placer) + end, + _on_dispense = function(stack, pos, droppos, dropnode, dropdir) + -- Place minecart as entity on rail. If there's no rail, just drop it. + local placed + if minetest.get_item_group(dropnode.name, "rail") ~= 0 then + -- FIXME: This places minecarts even if the spot is already occupied + local pointed_thing = { under = droppos, above = vector.new( droppos.x, droppos.y+1, droppos.z ) } + placed = mod.place_minecart(stack, pointed_thing) + end + if placed == nil then + -- Drop item + minetest.add_item(droppos, stack) + end + end, + groups = groups, + } + item_def.description = def.description + item_def._tt_help = def.tt_help + item_def._doc_items_longdesc = def.longdesc + item_def._doc_items_usagehelp = def.usagehelp + item_def.inventory_image = def.icon + item_def.wield_image = def.icon + minetest.register_craftitem(itemstring, item_def) +end + +--[[ +Register a minecart +* itemstring: Itemstring of minecart item +* entity_id: ID of minecart entity +* description: Item name / description +* longdesc: Long help text +* usagehelp: Usage help text +* mesh: Minecart mesh +* textures: Minecart textures table +* icon: Item icon +* drop: Dropped items after destroying minecart +* on_rightclick: Called after rightclick +* on_activate_by_rail: Called when above activator rail +* creative: If false, don't show in Creative Inventory +]] +function mod.register_minecart(def) + -- Make sure all required parameters are present + for _,name in pairs({"drop","itemstring","entity_id"}) do + assert( def[name], "def."..name..", a required parameter, is missing") + end + + local entity_id = def.entity_id; def.entity_id = nil + local craft = def.craft; def.craft = nil + local itemstring = def.itemstring; def.itemstring = nil + + -- Build cart definition + local cart = table.copy(DEFAULT_CART_DEF) + table_merge(cart, def) + minetest.register_entity(entity_id, cart) + + -- Register item to entity mapping + entity_mapping[itemstring] = entity_id + + register_minecart_craftitem(itemstring, def) + if minetest.get_modpath("doc_identifier") then + doc.sub.identifier.register_object(entity_id, "craftitems", itemstring) + end + + if craft then + minetest.register_craft(craft) + end +end +local register_minecart = mod.register_minecart + +dofile(modpath.."/carts/minecart.lua") +dofile(modpath.."/carts/with_chest.lua") +dofile(modpath.."/carts/with_commandblock.lua") +dofile(modpath.."/carts/with_hopper.lua") +dofile(modpath.."/carts/with_furnace.lua") +dofile(modpath.."/carts/with_tnt.lua") + +if minetest.get_modpath("mcl_wip") then + mcl_wip.register_wip_item("mcl_minecarts:chest_minecart") + mcl_wip.register_wip_item("mcl_minecarts:furnace_minecart") + mcl_wip.register_wip_item("mcl_minecarts:command_block_minecart") +end + +local function respawn_cart(cart) + local cart_type = cart.cart_type or "mcl_minecarts:minecart" + local pos = mod.get_cart_position(cart) + + local players = minetest.get_connected_players() + local distance = nil + for _,player in pairs(players) do + local d = vector.distance(player:get_pos(), pos) + if not distance or d < distance then distance = d end + end + if not distance or distance > 90 then return end + + mcl_log("Respawning cart #"..cart.uuid.." at "..tostring(pos)..",distance="..distance..",node="..minetest.get_node(pos).name) + + -- Update sequence so that old cart entities get removed + cart.seq = (cart.seq or 1) + 1 + save_cart_data(cart.uuid) + + -- Create the new entity and refresh caches + local sd = minetest.serialize({ uuid=cart.uuid, seq=cart.seq }) + local entity = minetest.add_entity(pos, cart_type, sd) + local le = entity:get_luaentity() + le._staticdata = cart + mcl_util.assign_uuid(entity) + + -- We intentionally don't call the normal hooks because this minecart was already there +end + +-- Try to respawn cart entities for carts that have moved into range of a player +local function try_respawn_carts() + -- Build a map of blocks near players + local block_map = {} + local players = minetest.get_connected_players() + for _,player in pairs(players) do + local pos = player:get_pos() + mod.add_blocks_to_map( + block_map, + vector.offset(pos,-CART_BLOCK_SIZE,-CART_BLOCK_SIZE,-CART_BLOCK_SIZE), + vector.offset(pos, CART_BLOCK_SIZE, CART_BLOCK_SIZE, CART_BLOCK_SIZE) + ) + end + + -- Find all cart data that are in these blocks + local carts = find_carts_by_block_map(block_map) + + -- Check to see if any of these don't have an entity + for _,cart in pairs(carts) do + local le = mcl_util.get_luaentity_from_uuid(cart.uuid) + if not le then + respawn_cart(cart) + end + end +end + +local timer = 0 +minetest.register_globalstep(function(dtime) + + -- Periodically respawn carts that come into range of a player + timer = timer - dtime + if timer <= 0 then + local start_time = minetest.get_us_time() + try_respawn_carts() + local stop_time = minetest.get_us_time() + local duration = (stop_time - start_time) / 1e6 + timer = duration / 250e-6 -- Schedule 50us per second + if timer > 5 then timer = 5 end + end + + -- Handle periodically updating out-of-range carts + -- TODO: change how often cart positions are updated based on velocity + local start_time + if DEBUG then start_time = minetest.get_us_time() end + + for uuid,staticdata in mod.carts() do + local pos = mod.get_cart_position(staticdata) + --[[ + local le = mcl_util.get_luaentity_from_uuid(staticdata.uuid) + print("cart# "..uuid.. + ",velocity="..tostring(staticdata.velocity).. + ",pos="..tostring(pos).. + ",le="..tostring(le).. + ",connected_at="..tostring(staticdata.connected_at) + )]] + + --- Non-entity code + if staticdata.connected_at then + movement.do_movement(staticdata, dtime) + end + end + + if DEBUG then + local stop_time = minetest.get_us_time() + print("Update took "..((stop_time-start_time)*1e-6).." seconds") + end +end) + +minetest.register_on_joinplayer(function(player) + -- Try cart reattachment + local player_name = player:get_player_name() + local player_meta = mcl_playerinfo.get_mod_meta(player_name, modname) + local cart_uuid = player_meta.attached_to + if cart_uuid then + local cartdata = get_cart_data(cart_uuid) + + -- Can't get into a cart that was destroyed + if not cartdata then + return + end + + -- Don't reattach players if someone else got in the cart + if cartdata.last_player ~= player_name then + return + end + + minetest.after(0.2,function(player_name, cart_uuid) + local player = minetest.get_player_by_name(player_name) + if not player then + return + end + + local cart = mcl_util.get_luaentity_from_uuid(cart_uuid) + if not cart then + return + end + + mod.attach_driver(cart, player) + end, player_name, cart_uuid) + end +end) + diff --git a/mods/ENTITIES/mcl_minecarts/carts/minecart.lua b/mods/ENTITIES/mcl_minecarts/carts/minecart.lua new file mode 100644 index 000000000..a155c94b5 --- /dev/null +++ b/mods/ENTITIES/mcl_minecarts/carts/minecart.lua @@ -0,0 +1,106 @@ +local modname = minetest.get_current_modname() +local S = minetest.get_translator(modname) +local mcl_log = mcl_util.make_mcl_logger("mcl_logging_minecarts", "Minecarts") +local mod = mcl_minecarts + +-- Imports +local PASSENGER_ATTACH_POSITION = mod.PASSENGER_ATTACH_POSITION + +local function activate_normal_minecart(self) + mod.detach_driver(self) + + -- Detach passenger + if self._passenger then + local mob = self._passenger.object + mob:set_detach() + self._passenger = nil + end +end + +function mod.attach_driver(cart, player) + local staticdata = cart._staticdata + + -- Make sure we have a player + if not player or not player:is_player() then return end + + -- Prevent more than one player getting in the cart + local player_name = player:get_player_name() + if cart._driver or player:get_player_control().sneak then return end + + -- Prevent getting into a cart that already has a passenger + if cart._passenger then return end + + -- Update cart information + cart._driver = player_name + cart._start_pos = cart.object:get_pos() + + -- Keep track of player attachment + local player_meta = mcl_playerinfo.get_mod_meta(player_name, modname) + player_meta.attached_to = cart._uuid + staticdata.last_player = player_name + + -- Update player information + local uuid = staticdata.uuid + mcl_player.player_attached[player_name] = true + --minetest.log("action", player_name.." entered minecart #"..tostring(uuid).." at "..tostring(cart._start_pos)) + + -- Attach the player object to the minecart + player:set_attach(cart.object, "", vector.new(1,-1.75,-2), vector.new(0,0,0)) + minetest.after(0.2, function(name) + local player = minetest.get_player_by_name(name) + if player then + mcl_player.player_set_animation(player, "sit" , 30) + player:set_eye_offset(vector.new(0,-5.5,0), vector.new(0,-4,0)) + mcl_title.set(player, "actionbar", {text=S("Sneak to dismount"), color="white", stay=60}) + end + end, player_name) +end + +mod.register_minecart({ + itemstring = "mcl_minecarts:minecart", + craft = { + output = "mcl_minecarts:minecart", + recipe = { + {"mcl_core:iron_ingot", "", "mcl_core:iron_ingot"}, + {"mcl_core:iron_ingot", "mcl_core:iron_ingot", "mcl_core:iron_ingot"}, + }, + }, + entity_id = "mcl_minecarts:minecart", + description = S("Minecart"), + tt_helop = S("Vehicle for fast travel on rails"), + long_descp = S("Minecarts can be used for a quick transportion on rails.") .. "\n" .. + S("Minecarts only ride on rails and always follow the tracks. At a T-junction with no straight way ahead, they turn left. The speed is affected by the rail type."), + S("You can place the minecart on rails. Right-click it to enter it. Punch it to get it moving.") .. "\n" .. + S("To obtain the minecart, punch it while holding down the sneak key.") .. "\n" .. + S("If it moves over a powered activator rail, you'll get ejected."), + initial_properties = { + mesh = "mcl_minecarts_minecart.b3d", + textures = {"mcl_minecarts_minecart.png"}, + }, + icon = "mcl_minecarts_minecart_normal.png", + drop = {"mcl_minecarts:minecart"}, + on_rightclick = mod.attach_driver, + on_activate_by_rail = activate_normal_minecart, + _mcl_minecarts_on_step = function(self, dtime) + -- Grab mob + if math.random(1,20) > 15 and not self._passenger then + local mobsnear = minetest.get_objects_inside_radius(self.object:get_pos(), 1.3) + for n=1, #mobsnear do + local mob = mobsnear[n] + if mob and not mob:get_attach() then + local entity = mob:get_luaentity() + if entity and entity.is_mob then + self._passenger = entity + mob:set_attach(self.object, "", PASSENGER_ATTACH_POSITION, vector.zero()) + break + end + end + end + elseif self._passenger then + local passenger_pos = self._passenger.object:get_pos() + if not passenger_pos then + self._passenger = nil + end + end + end +}) diff --git a/mods/ENTITIES/mcl_minecarts/carts/with_chest.lua b/mods/ENTITIES/mcl_minecarts/carts/with_chest.lua new file mode 100644 index 000000000..1f141eade --- /dev/null +++ b/mods/ENTITIES/mcl_minecarts/carts/with_chest.lua @@ -0,0 +1,35 @@ +local modname = minetest.get_current_modname() +local modpath = minetest.get_modpath(modname) +local mod = mcl_minecarts +local S = minetest.get_translator(modname) + +-- Minecart with Chest +mcl_minecarts.register_minecart({ + itemstring = "mcl_minecarts:chest_minecart", + craft = { + output = "mcl_minecarts:chest_minecart", + recipe = { + {"mcl_chests:chest"}, + {"mcl_minecarts:minecart"}, + }, + }, + entity_id = "mcl_minecarts:chest_minecart", + description = S("Minecart with Chest"), + tt_help = nil, + longdesc = nil, + usagehelp = nil, + initial_properties = { + mesh = "mcl_minecarts_minecart_chest.b3d", + textures = { + "mcl_chests_normal.png", + "mcl_minecarts_minecart.png" + }, + }, + icon = "mcl_minecarts_minecart_chest.png", + drop = {"mcl_minecarts:minecart", "mcl_chests:chest"}, + groups = { container = 1 }, + on_rightclick = nil, + on_activate_by_rail = nil, + creative = true +}) +mcl_entity_invs.register_inv("mcl_minecarts:chest_minecart","Minecart",27,false,true) diff --git a/mods/ENTITIES/mcl_minecarts/carts/with_commandblock.lua b/mods/ENTITIES/mcl_minecarts/carts/with_commandblock.lua new file mode 100644 index 000000000..0dc05d490 --- /dev/null +++ b/mods/ENTITIES/mcl_minecarts/carts/with_commandblock.lua @@ -0,0 +1,64 @@ +local modname = minetest.get_current_modname() +local modpath = minetest.get_modpath(modname) +local mod = mcl_minecarts +local S = minetest.get_translator(modname) + +function table_metadata(table) + return { + table = table, + set_string = function(self, key, value) + --print("set_string("..tostring(key)..", "..tostring(value)..")") + self.table[key] = tostring(value) + end, + get_string = function(self, key) + if self.table[key] then + return tostring(self.table[key]) + end + end + } +end + +-- Minecart with Command Block +mod.register_minecart({ + itemstring = "mcl_minecarts:command_block_minecart", + entity_id = "mcl_minecarts:command_block_minecart", + description = S("Minecart with Command Block"), + tt_help = nil, + loncdesc = nil, + usagehelp = nil, + initial_properties = { + mesh = "mcl_minecarts_minecart_block.b3d", + textures = { + "jeija_commandblock_off.png^[verticalframe:2:0", + "jeija_commandblock_off.png^[verticalframe:2:0", + "jeija_commandblock_off.png^[verticalframe:2:0", + "jeija_commandblock_off.png^[verticalframe:2:0", + "jeija_commandblock_off.png^[verticalframe:2:0", + "jeija_commandblock_off.png^[verticalframe:2:0", + "mcl_minecarts_minecart.png", + }, + }, + icon = "mcl_minecarts_minecart_command_block.png", + drop = {"mcl_minecarts:minecart"}, + on_rightclick = function(self, clicker) + self._staticdata.meta = self._staticdata.meta or {} + local meta = table_metadata(self._staticdata.meta) + + mesecon.commandblock.handle_rightclick(meta, clicker) + end, + _mcl_minecarts_on_place = function(self, placer) + -- Create a fake metadata object that stores into the cart's staticdata + self._staticdata.meta = self._staticdata.meta or {} + local meta = table_metadata(self._staticdata.meta) + + mesecon.commandblock.initialize(meta) + mesecon.commandblock.place(meta, placer) + end, + on_activate_by_rail = function(self, timer) + self._staticdata.meta = self._staticdata.meta or {} + local meta = table_metadata(self._staticdata.meta) + + mesecon.commandblock.action_on(meta, self.object:get_pos()) + end, + creative = true +}) diff --git a/mods/ENTITIES/mcl_minecarts/carts/with_furnace.lua b/mods/ENTITIES/mcl_minecarts/carts/with_furnace.lua new file mode 100644 index 000000000..9bc09774f --- /dev/null +++ b/mods/ENTITIES/mcl_minecarts/carts/with_furnace.lua @@ -0,0 +1,100 @@ +local modname = minetest.get_current_modname() +local S = minetest.get_translator(modname) + +local FURNACE_CART_SPEED = tonumber(minetest.settings:get("mcl_minecarts_furnace_speed")) or 4 + +-- Minecart with Furnace +mcl_minecarts.register_minecart({ + itemstring = "mcl_minecarts:furnace_minecart", + craft = { + output = "mcl_minecarts:furnace_minecart", + recipe = { + {"mcl_furnaces:furnace"}, + {"mcl_minecarts:minecart"}, + }, + }, + entity_id = "mcl_minecarts:furnace_minecart", + description = S("Minecart with Furnace"), + tt_help = nil, + longdesc = S("A minecart with furnace is a vehicle that travels on rails. It can propel itself with fuel."), + usagehelp = S("Place it on rails. If you give it some coal, the furnace will start burning for a long time and the minecart will be able to move itself. Punch it to get it moving.") .. "\n" .. + S("To obtain the minecart and furnace, punch them while holding down the sneak key."), + + initial_properties = { + mesh = "mcl_minecarts_minecart_block.b3d", + textures = { + "default_furnace_top.png", + "default_furnace_top.png", + "default_furnace_front.png", + "default_furnace_side.png", + "default_furnace_side.png", + "default_furnace_side.png", + "mcl_minecarts_minecart.png", + }, + }, + icon = "mcl_minecarts_minecart_furnace.png", + drop = {"mcl_minecarts:minecart", "mcl_furnaces:furnace"}, + on_rightclick = function(self, clicker) + local staticdata = self._staticdata + + -- Feed furnace with coal + if not clicker or not clicker:is_player() then + return + end + local held = clicker:get_wielded_item() + if minetest.get_item_group(held:get_name(), "coal") == 1 then + staticdata.fueltime = (staticdata.fueltime or 0) + 180 + + -- Trucate to 27 minutes (9 uses) + if staticdata.fueltime > 27*60 then + staticdata.fuel_time = 27*60 + end + + if not minetest.is_creative_enabled(clicker:get_player_name()) then + held:take_item() + local index = clicker:get_wield_index() + local inv = clicker:get_inventory() + inv:set_stack("main", index, held) + end + self.object:set_properties({textures = + { + "default_furnace_top.png", + "default_furnace_top.png", + "default_furnace_front_active.png", + "default_furnace_side.png", + "default_furnace_side.png", + "default_furnace_side.png", + "mcl_minecarts_minecart.png", + }}) + end + end, + on_activate_by_rail = nil, + creative = true, + _mcl_minecarts_on_step = function(self, dtime) + local staticdata = self._staticdata + + -- Update furnace stuff + if (staticdata.fueltime or 0) > 0 then + for car in mcl_minecarts.train_cars(staticdata) do + if car.velocity < FURNACE_CART_SPEED - 0.1 then -- Slightly less to allow train cars to maintain spacing + car.velocity = FURNACE_CART_SPEED + end + end + + staticdata.fueltime = (staticdata.fueltime or dtime) - dtime + if staticdata.fueltime <= 0 then + self.object:set_properties({textures = + { + "default_furnace_top.png", + "default_furnace_top.png", + "default_furnace_front.png", + "default_furnace_side.png", + "default_furnace_side.png", + "default_furnace_side.png", + "mcl_minecarts_minecart.png", + }}) + staticdata.fueltime = 0 + end + end + end, +}) diff --git a/mods/ENTITIES/mcl_minecarts/carts/with_hopper.lua b/mods/ENTITIES/mcl_minecarts/carts/with_hopper.lua new file mode 100644 index 000000000..9e6defdad --- /dev/null +++ b/mods/ENTITIES/mcl_minecarts/carts/with_hopper.lua @@ -0,0 +1,178 @@ +local modname = minetest.get_current_modname() +local modpath = minetest.get_modpath(modname) +local mod = mcl_minecarts +local S = minetest.get_translator(modname) + +local LOGGING_ON = {minetest.settings:get_bool("mcl_logging_minecarts", false)} +local function mcl_log(message) + if LOGGING_ON[1] then + mcl_util.mcl_log(message, "[Minecarts]", true) + end +end + +local function hopper_take_item(self, dtime) + local pos = self.object:get_pos() + if not pos then return end + + if not self or self.name ~= "mcl_minecarts:hopper_minecart" then return end + + if mcl_util.check_dtime_timer(self, dtime, "hoppermc_take", 0.15) then + --minetest.log("The check timer was triggered: " .. dump(pos) .. ", name:" .. self.name) + else + --minetest.log("The check timer was not triggered") + return + end + + + local above_pos = vector.offset(pos, 0, 0.9, 0) + local objs = minetest.get_objects_inside_radius(above_pos, 1.25) + + if objs then + mcl_log("there is an itemstring. Number of objs: ".. #objs) + + for k, v in pairs(objs) do + local ent = v:get_luaentity() + + if ent and not ent._removed and ent.itemstring and ent.itemstring ~= "" then + local taken_items = false + + mcl_log("ent.name: " .. tostring(ent.name)) + mcl_log("ent pos: " .. tostring(ent.object:get_pos())) + + local inv = mcl_entity_invs.load_inv(self, 5) + if not inv then return false end + + local current_itemstack = ItemStack(ent.itemstring) + + mcl_log("inv. size: " .. self._inv_size) + if inv:room_for_item("main", current_itemstack) then + mcl_log("Room") + inv:add_item("main", current_itemstack) + ent.object:get_luaentity().itemstring = "" + ent.object:remove() + taken_items = true + else + mcl_log("no Room") + end + + if not taken_items then + local items_remaining = current_itemstack:get_count() + + -- This will take part of a floating item stack if no slot can hold the full amount + for i = 1, self._inv_size, 1 do + local stack = inv:get_stack("main", i) + + mcl_log("i: " .. tostring(i)) + mcl_log("Items remaining: " .. items_remaining) + mcl_log("Name: " .. tostring(stack:get_name())) + + if current_itemstack:get_name() == stack:get_name() then + mcl_log("We have a match. Name: " .. tostring(stack:get_name())) + + local room_for = stack:get_stack_max() - stack:get_count() + mcl_log("Room for: " .. tostring(room_for)) + + if room_for == 0 then + -- Do nothing + mcl_log("No room") + elseif room_for < items_remaining then + mcl_log("We have more items remaining than space") + + items_remaining = items_remaining - room_for + stack:set_count(stack:get_stack_max()) + inv:set_stack("main", i, stack) + taken_items = true + else + local new_stack_size = stack:get_count() + items_remaining + stack:set_count(new_stack_size) + mcl_log("We have more than enough space. Now holds: " .. new_stack_size) + + inv:set_stack("main", i, stack) + items_remaining = 0 + + ent.object:get_luaentity().itemstring = "" + ent.object:remove() + + taken_items = true + break + end + + mcl_log("Count: " .. tostring(stack:get_count())) + mcl_log("stack max: " .. tostring(stack:get_stack_max())) + --mcl_log("Is it empty: " .. stack:to_string()) + end + + if i == self._inv_size and taken_items then + mcl_log("We are on last item and still have items left. Set final stack size: " .. items_remaining) + current_itemstack:set_count(items_remaining) + --mcl_log("Itemstack2: " .. current_itemstack:to_string()) + ent.itemstring = current_itemstack:to_string() + end + end + end + + --Add in, and delete + if taken_items then + mcl_log("Saving") + mcl_entity_invs.save_inv(ent) + return taken_items + else + mcl_log("No need to save") + end + + end + end + end + + return false +end + +-- Minecart with Hopper +mod.register_minecart({ + itemstring = "mcl_minecarts:hopper_minecart", + craft = { + output = "mcl_minecarts:hopper_minecart", + recipe = { + {"mcl_hoppers:hopper"}, + {"mcl_minecarts:minecart"}, + }, + }, + entity_id = "mcl_minecarts:hopper_minecart", + description = S("Minecart with Hopper"), + tt_help = nil, + longdesc = nil, + usagehelp = nil, + initial_properties = { + mesh = "mcl_minecarts_minecart_hopper.b3d", + textures = { + "mcl_hoppers_hopper_inside.png", + "mcl_minecarts_minecart.png", + "mcl_hoppers_hopper_outside.png", + "mcl_hoppers_hopper_top.png", + }, + }, + icon = "mcl_minecarts_minecart_hopper.png", + drop = {"mcl_minecarts:minecart", "mcl_hoppers:hopper"}, + groups = { container = 1 }, + on_rightclick = nil, + on_activate_by_rail = nil, + _mcl_minecarts_on_enter = function(self, pos, staticdata) + if (staticdata.hopper_delay or 0) > 0 then + return + end + + -- try to pull from containers into our inventory + if not self then return end + local inv = mcl_entity_invs.load_inv(self,5) + local above_pos = vector.offset(pos,0,1,0) + mcl_util.hopper_pull_to_inventory(inv, 'main', above_pos, pos) + + staticdata.hopper_delay = (staticdata.hopper_delay or 0) + (1/20) + end, + _mcl_minecarts_on_step = function(self, dtime) + hopper_take_item(self, dtime) + end, + creative = true +}) +mcl_entity_invs.register_inv("mcl_minecarts:hopper_minecart", "Hopper Minecart", 5, false, true) + diff --git a/mods/ENTITIES/mcl_minecarts/carts/with_tnt.lua b/mods/ENTITIES/mcl_minecarts/carts/with_tnt.lua new file mode 100644 index 000000000..8b0c7d869 --- /dev/null +++ b/mods/ENTITIES/mcl_minecarts/carts/with_tnt.lua @@ -0,0 +1,139 @@ +local modname = minetest.get_current_modname() +local modpath = minetest.get_modpath(modname) +local mod = mcl_minecarts +local S = minetest.get_translator(modname) + +local function detonate_tnt_minecart(self) + local pos = self.object:get_pos() + self.object:remove() + mcl_explosions.explode(pos, 6, { drop_chance = 1.0 }) +end + +local function activate_tnt_minecart(self, timer) + if self._boomtimer then + return + end + if timer then + self._boomtimer = timer + else + self._boomtimer = tnt.BOOMTIMER + end + self.object:set_properties({ + textures = { + "mcl_tnt_blink.png", + "mcl_tnt_blink.png", + "mcl_tnt_blink.png", + "mcl_tnt_blink.png", + "mcl_tnt_blink.png", + "mcl_tnt_blink.png", + "mcl_minecarts_minecart.png", + }, + glow = 15, + }) + self._blinktimer = tnt.BLINKTIMER + minetest.sound_play("tnt_ignite", {pos = self.object:get_pos(), gain = 1.0, max_hear_distance = 15}, true) +end +mod.register_minecart({ + itemstring = "mcl_minecarts:tnt_minecart", + craft = { + output = "mcl_minecarts:tnt_minecart", + recipe = { + {"mcl_tnt:tnt"}, + {"mcl_minecarts:minecart"}, + }, + }, + entity_id = "mcl_minecarts:tnt_minecart", + description = S("Minecart with TNT"), + tt_help = S("Vehicle for fast travel on rails").."\n"..S("Can be ignited by tools or powered activator rail"), + longdesc = S("A minecart with TNT is an explosive vehicle that travels on rail."), + usagehelp = S("Place it on rails. Punch it to move it. The TNT is ignited with a flint and steel or when the minecart is on an powered activator rail.") .. "\n" .. + S("To obtain the minecart and TNT, punch them while holding down the sneak key. You can't do this if the TNT was ignited."), + initial_properties = { + mesh = "mcl_minecarts_minecart_block.b3d", + textures = { + "default_tnt_top.png", + "default_tnt_bottom.png", + "default_tnt_side.png", + "default_tnt_side.png", + "default_tnt_side.png", + "default_tnt_side.png", + "mcl_minecarts_minecart.png", + }, + }, + icon = "mcl_minecarts_minecart_tnt.png", + drop = {"mcl_minecarts:minecart", "mcl_tnt:tnt"}, + on_rightclick = function(self, clicker) + -- Ingite + if not clicker or not clicker:is_player() then + return + end + if self._boomtimer then + return + end + local held = clicker:get_wielded_item() + if held:get_name() == "mcl_fire:flint_and_steel" then + if not minetest.is_creative_enabled(clicker:get_player_name()) then + held:add_wear(65535/65) -- 65 uses + local index = clicker:get_wield_index() + local inv = clicker:get_inventory() + inv:set_stack("main", index, held) + end + activate_tnt_minecart(self) + end + end, + on_activate_by_rail = activate_tnt_minecart, + creative = true, + _mcl_minecarts_on_step = function(self, dtime) + -- Impacts reduce the speed greatly. Use this to trigger explosions + local current_speed = vector.length(self.object:get_velocity()) + if current_speed < (self._old_speed or 0) - 6 then + detonate_tnt_minecart(self) + end + self._old_speed = current_speed + + if self._boomtimer then + -- Explode + self._boomtimer = self._boomtimer - dtime + if self._boomtimer <= 0 then + detonate_tnt_minecart(self) + return + else + local pos = mod.get_cart_position(self._staticdata) or self.object:get_pos() + if pos then + tnt.smoke_step(pos) + end + end + end + + if self._blinktimer then + self._blinktimer = self._blinktimer - dtime + if self._blinktimer <= 0 then + self._blink = not self._blink + if self._blink then + self.object:set_properties({textures = + { + "default_tnt_top.png", + "default_tnt_bottom.png", + "default_tnt_side.png", + "default_tnt_side.png", + "default_tnt_side.png", + "default_tnt_side.png", + "mcl_minecarts_minecart.png", + }}) + else + self.object:set_properties({textures = + { + "mcl_tnt_blink.png", + "mcl_tnt_blink.png", + "mcl_tnt_blink.png", + "mcl_tnt_blink.png", + "mcl_tnt_blink.png", + "mcl_tnt_blink.png", + "mcl_minecarts_minecart.png", + }}) + end + self._blinktimer = tnt.BLINKTIMER + end + end + end, +}) diff --git a/mods/ENTITIES/mcl_minecarts/functions.lua b/mods/ENTITIES/mcl_minecarts/functions.lua index 1792e9252..db981981f 100644 --- a/mods/ENTITIES/mcl_minecarts/functions.lua +++ b/mods/ENTITIES/mcl_minecarts/functions.lua @@ -1,4 +1,36 @@ local vector = vector +local mod = mcl_minecarts +local table_merge = mcl_util.table_merge + +local function get_path(base, first, ...) + if not first then return base end + if not base then return end + return get_path(base[first], ...) +end +local function force_get_node(pos) + local node = minetest.get_node(pos) + if node.name ~= "ignore" then return node end + + --local time_start = minetest.get_us_time() + local vm = minetest.get_voxel_manip() + local emin, emax = vm:read_from_map(pos, pos) + local area = VoxelArea:new{ + MinEdge = emin, + MaxEdge = emax, + } + local data = vm:get_data() + local param_data = vm:get_light_data() + local param2_data = vm:get_param2_data() + + local vi = area:indexp(pos) + --minetest.log("force_get_node() voxel_manip section took "..((minetest.get_us_time()-time_start)*1e-6).." seconds") + return { + name = minetest.get_name_from_content_id(data[vi]), + param = param_data[vi], + param2 = param2_data[vi] + } + +end function mcl_minecarts:get_sign(z) if z == 0 then @@ -10,158 +42,485 @@ end function mcl_minecarts:velocity_to_dir(v) if math.abs(v.x) > math.abs(v.z) then - return {x=mcl_minecarts:get_sign(v.x), y=mcl_minecarts:get_sign(v.y), z=0} + return vector.new( + mcl_minecarts:get_sign(v.x), + mcl_minecarts:get_sign(v.y), + 0 + ) else - return {x=0, y=mcl_minecarts:get_sign(v.y), z=mcl_minecarts:get_sign(v.z)} + return vector.new( + 0, + mcl_minecarts:get_sign(v.y), + mcl_minecarts:get_sign(v.z) + ) end end -function mcl_minecarts:is_rail(pos, railtype) - local node = minetest.get_node(pos).name - if node == "ignore" then - local vm = minetest.get_voxel_manip() - local emin, emax = vm:read_from_map(pos, pos) - local area = VoxelArea:new{ - MinEdge = emin, - MaxEdge = emax, - } - local data = vm:get_data() - local vi = area:indexp(pos) - node = minetest.get_name_from_content_id(data[vi]) +function mcl_minecarts.is_rail(self, pos, railtype) + -- Compatibility with mcl_minecarts:is_rail() usage + if self ~= mcl_minecarts then + railtype = pos + pos = self end - if minetest.get_item_group(node, "rail") == 0 then + + local node_name = force_get_node(pos).name + + if minetest.get_item_group(node_name, "rail") == 0 then return false end if not railtype then return true end - return minetest.get_item_group(node, "connect_to_raillike") == railtype + return minetest.get_item_group(node_name, "connect_to_raillike") == railtype end -function mcl_minecarts:check_front_up_down(pos, dir_, check_down, railtype) - local dir = vector.new(dir_) - -- Front - dir.y = 0 - local cur = vector.add(pos, dir) - if mcl_minecarts:is_rail(cur, railtype) then - return dir - end - -- Up - if check_down then - dir.y = 1 - cur = vector.add(pos, dir) - if mcl_minecarts:is_rail(cur, railtype) then - return dir +-- Directional constants +local north = vector.new( 0, 0, 1); local N = 1 -- 4dir = 0 +local east = vector.new( 1, 0, 0); local E = 4 -- 4dir = 1 +local south = vector.new( 0, 0,-1); local S = 2 -- 4dir = 2 +local west = vector.new(-1, 0, 0); local W = 8 -- 4dir = 3 + +-- Share. Consider moving this to some shared location +mod.north = north +mod.south = south +mod.east = east +mod.west = west + +--[[ + mcl_minecarts.snap_direction(dir) + + returns a valid cart direction that has the smallest angle difference to `dir' +]] +local VALID_DIRECTIONS = { + north, vector.offset(north, 0, 1, 0), vector.offset(north, 0, -1, 0), + south, vector.offset(south, 0, 1, 0), vector.offset(south, 0, -1, 0), + east, vector.offset(east, 0, 1, 0), vector.offset(east, 0, -1, 0), + west, vector.offset(west, 0, 1, 0), vector.offset(west, 0, -1, 0), +} +function mod.snap_direction(dir) + dir = vector.normalize(dir) + local best = nil + local diff = -1 + for _,d in pairs(VALID_DIRECTIONS) do + local dot = vector.dot(dir,d) + if dot > diff then + best = d + diff = dot end end - -- Down - dir.y = -1 - cur = vector.add(pos, dir) - if mcl_minecarts:is_rail(cur, railtype) then - return dir - end - return nil + return best end -function mcl_minecarts:get_rail_direction(pos_, dir, ctrl, old_switch, railtype) - local pos = vector.round(pos_) - local cur - local left_check, right_check = true, true +local CONNECTIONS = { north, south, east, west } +local HORIZONTAL_STANDARD_RULES = { + [N] = { "", 0, mask = N, score = 1, can_slope = true }, + [S] = { "", 0, mask = S, score = 1, can_slope = true }, + [N+S] = { "", 0, mask = N+S, score = 2, can_slope = true }, - -- Check left and right - local left = {x=0, y=0, z=0} - local right = {x=0, y=0, z=0} - if dir.z ~= 0 and dir.x == 0 then - left.x = -dir.z - right.x = dir.z - elseif dir.x ~= 0 and dir.z == 0 then - left.z = dir.x - right.z = -dir.x - end + [E] = { "", 1, mask = E, score = 1, can_slope = true }, + [W] = { "", 1, mask = W, score = 1, can_slope = true }, + [E+W] = { "", 1, mask = E+W, score = 2, can_slope = true }, +} +mod.HORIZONTAL_STANDARD_RULES = HORIZONTAL_STANDARD_RULES - if ctrl then - if old_switch == 1 then - left_check = false - elseif old_switch == 2 then - right_check = false - end - if ctrl.left and left_check then - cur = mcl_minecarts:check_front_up_down(pos, left, false, railtype) - if cur then - return cur, 1 - end - left_check = false - end - if ctrl.right and right_check then - cur = mcl_minecarts:check_front_up_down(pos, right, false, railtype) - if cur then - return cur, 2 - end - right_check = true - end - end +local HORIZONTAL_CURVES_RULES = { + [N+E] = { "_corner", 3, name = "ne corner", mask = N+E, score = 3 }, + [N+W] = { "_corner", 2, name = "nw corner", mask = N+W, score = 3 }, + [S+E] = { "_corner", 0, name = "se corner", mask = S+E, score = 3 }, + [S+W] = { "_corner", 1, name = "sw corner", mask = S+W, score = 3 }, - -- Normal - cur = mcl_minecarts:check_front_up_down(pos, dir, true, railtype) - if cur then - return cur - end + [N+E+W] = { "_tee_off", 3, mask = N+E+W, score = 4 }, + [S+E+W] = { "_tee_off", 1, mask = S+E+W, score = 4 }, + [N+S+E] = { "_tee_off", 0, mask = N+S+E, score = 4 }, + [N+S+W] = { "_tee_off", 2, mask = N+S+W, score = 4 }, - -- Left, if not already checked - if left_check then - cur = mcl_minecarts:check_front_up_down(pos, left, false, railtype) - if cur then - return cur - end - end + [N+S+E+W] = { "_cross", 0, mask = N+S+E+W, score = 5 }, +} +table_merge(HORIZONTAL_CURVES_RULES, HORIZONTAL_STANDARD_RULES) +mod.HORIZONTAL_CURVES_RULES = HORIZONTAL_CURVES_RULES - -- Right, if not already checked - if right_check then - cur = mcl_minecarts:check_front_up_down(pos, right, false, railtype) - if cur then - return cur - end - end - -- Backwards - if not old_switch then - cur = mcl_minecarts:check_front_up_down(pos, { - x = -dir.x, - y = dir.y, - z = -dir.z - }, true, railtype) - if cur then - return cur - end - end - return {x=0, y=0, z=0} -end - -local plane_adjacents = { - vector.new(-1,0,0), - vector.new(1,0,0), - vector.new(0,0,-1), - vector.new(0,0,1), +local HORIZONTAL_RULES_BY_RAIL_GROUP = { + [1] = HORIZONTAL_STANDARD_RULES, + [2] = HORIZONTAL_CURVES_RULES, } -function mcl_minecarts:get_start_direction(pos) - local dir - local i = 0 - while (not dir and i < #plane_adjacents) do - i = i+1 - local node = minetest.get_node_or_nil(vector.add(pos, plane_adjacents[i])) - if node ~= nil - and minetest.get_item_group(node.name, "rail") == 0 - and minetest.get_item_group(node.name, "solid") == 1 - and minetest.get_item_group(node.name, "opaque") == 1 - then - dir = mcl_minecarts:check_front_up_down(pos, vector.multiply(plane_adjacents[i], -1), true) - end +local function check_connection_rule(pos, connections, rule) + -- All bits in the mask must be set for the connection to be possible + if bit.band(rule.mask,connections) ~= rule.mask then + --print("Mask mismatch ("..tostring(rule.mask)..","..tostring(connections)..")") + return false end - return dir + + -- If there is an allow filter, that mush also return true + if rule.allow and rule.allow(rule, connections, pos) then + return false + end + + return true end -function mcl_minecarts:set_velocity(obj, dir, factor) - obj._velocity = vector.multiply(dir, factor or 3) - obj._old_pos = nil - obj._punched = true +local function make_sloped_if_straight(pos, dir) + local node = minetest.get_node(pos) + local nodedef = minetest.registered_nodes[node.name] + + local param2 = 0 + if dir == east then + param2 = 3 + elseif dir == west then + param2 = 1 + elseif dir == north then + param2 = 2 + elseif dir == south then + param2 = 0 + end + + if get_path( nodedef, "_mcl_minecarts", "railtype" ) == "straight" then + minetest.swap_node(pos, {name = nodedef._mcl_minecarts.base_name .. "_sloped", param2 = param2}) + end end + +local function is_connection(pos, dir) + local node = force_get_node(pos) + local nodedef = minetest.registered_nodes[node.name] + + local get_next_dir = get_path(nodedef, "_mcl_minecarts", "get_next_dir") + if not get_next_dir then return end + + local next_dir = get_next_dir(pos, dir, node) + next_dir.y = 0 + return vector.equals(next_dir, dir) +end + +local function get_rail_connections(pos, opt) + local legacy = opt and opt.legacy + local ignore_neighbor_connections = opt and opt.ignore_neighbor_connections + + local connections = 0 + for i = 1,#CONNECTIONS do + local dir = CONNECTIONS[i] + local neighbor = vector.add(pos, dir) + local node = force_get_node(neighbor) + local nodedef = minetest.registered_nodes[node.name] + + -- Only allow connections to the open ends of rails, as decribed by get_next_dir + if mcl_minecarts.is_rail(neighbor) and ( legacy or get_path(nodedef, "_mcl_minecarts", "get_next_dir" ) ) then + local rev_dir = vector.direction(dir,vector.zero()) + if ignore_neighbor_connections or is_connection(neighbor, rev_dir) then + connections = bit.bor(connections, bit.lshift(1,i - 1)) + end + end + + -- Check for sloped rail one block down + local below_neighbor = vector.offset(neighbor, 0, -1, 0) + local node = force_get_node(below_neighbor) + local nodedef = minetest.registered_nodes[node.name] + if mcl_minecarts.is_rail(below_neighbor) and ( legacy or get_path(nodedef, "_mcl_minecarts", "get_next_dir" ) ) then + local rev_dir = vector.direction(dir, vector.zero()) + if ignore_neighbor_connections or is_connection(below_neighbor, rev_dir) then + connections = bit.bor(connections, bit.lshift(1,i - 1)) + end + end + end + return connections +end +mod.get_rail_connections = get_rail_connections + +local function apply_connection_rules(node, nodedef, pos, rules, connections) + -- Select the best allowed connection + local rule = nil + local score = 0 + for k,r in pairs(rules) do + if check_connection_rule(pos, connections, r) then + if r.score > score then + --print("Best rule so far is "..dump(r)) + score = r.score + rule = r + end + end + end + + if rule then + -- Apply the mapping + local new_name = nodedef._mcl_minecarts.base_name..rule[1] + if new_name ~= node.name or node.param2 ~= rule[2] then + --print("swapping "..node.name.." for "..new_name..","..tostring(rule[2]).." at "..tostring(pos)) + node.name = new_name + node.param2 = rule[2] + minetest.swap_node(pos, node) + end + + if rule.after then + rule.after(rule, pos, connections) + end + end +end + +local function is_rail_end_connected(pos, dir) + -- Handle new track types that have track-specific direction handler + local node = force_get_node(pos) + local get_next_dir = get_path(minetest.registered_nodes,node.name,"_mcl_minecarts","get_next_dir") + if not get_next_dir then return false end + + return get_next_dir(pos, dir, node) == dir +end + +local function bend_straight_rail(pos, towards) + local node = force_get_node(pos) + local nodedef = minetest.registered_nodes[node.name] + + -- Only bend rails + local rail_type = minetest.get_item_group(node.name, "rail") + if rail_type == 0 then return end + + -- Only bend unbent rails + if not nodedef._mcl_minecarts then return end + if node.name ~= nodedef._mcl_minecarts.base_name then return end + + -- only bend rails that have at least one free end + local dir1 = minetest.fourdir_to_dir(node.param2) + local dir2 = minetest.fourdir_to_dir((node.param2+2)%4) + local dir1_connected = is_rail_end_connected(pos + dir1, dir2) + local dir2_connected = is_rail_end_connected(pos + dir2, dir1) + if dir1_connected and dir2_connected then return end + + local connections = { + vector.direction(pos, towards), + } + if dir1_connected then + connections[#connections+1] = dir1 + end + if dir2_connected then + connections[#connections+1] = dir2 + end + local connections_mask = 0 + for i = 1,#CONNECTIONS do + for j = 1,#connections do + if CONNECTIONS[i] == connections[j] then + connections_mask = bit.bor(connections_mask, bit.lshift(1, i -1)) + end + end + end + + local rules = HORIZONTAL_RULES_BY_RAIL_GROUP[nodedef.groups.rail] + apply_connection_rules(node, nodedef, pos, rules, connections_mask) +end + +local function update_rail_connections(pos, opt) + local node = minetest.get_node(pos) + local nodedef = minetest.registered_nodes[node.name] + if not nodedef or not nodedef._mcl_minecarts then return end + + -- Get the mappings to use + local rules = HORIZONTAL_RULES_BY_RAIL_GROUP[nodedef.groups.rail] + if nodedef._mcl_minecarts and nodedef._mcl_minecarts.connection_rules then -- Custom connection rules + rules = nodedef._mcl_minecarts.connection_rules + end + if not rules then return end + + if not (opt and opt.no_bend_straights) then + for i = 1,#CONNECTIONS do + bend_straight_rail(vector.add(pos, CONNECTIONS[i]), pos) + end + end + + -- Horizontal rules, Check for rails on each neighbor + local connections = get_rail_connections(pos, opt) + + -- Check for rasing rails to slopes + for i = 1,#CONNECTIONS do + local dir = CONNECTIONS[i] + local neighbor = vector.add(pos, dir) + make_sloped_if_straight(vector.offset(neighbor, 0, -1, 0), dir) + end + + apply_connection_rules(node, nodedef, pos, rules, connections) + + local node_def = minetest.registered_nodes[node.name] + if get_path(node_def, "_mcl_minecarts", "can_slope") then + for i=1,#CONNECTIONS do + local dir = CONNECTIONS[i] + local higher_rail_pos = vector.offset(pos,dir.x,1,dir.z) + local rev_dir = vector.direction(dir,vector.zero()) + if mcl_minecarts.is_rail(higher_rail_pos) and is_connection(higher_rail_pos, rev_dir) then + make_sloped_if_straight(pos, rev_dir) + end + end + end + + -- Recursion guard + if opt and opt.convert_neighbors == false then return end + + -- Check if the open end of this rail runs into a corner or a tee and convert that node into a tee or a cross + local neighbors = {} + for i=1,#CONNECTIONS do + local dir = CONNECTIONS[i] + if is_connection(pos, dir) then + local other_pos = pos - dir + local other_node = core.get_node(other_pos) + local other_node_def = core.registered_nodes[other_node.name] + local railtype = get_path(other_node_def, "_mcl_minecarts","railtype") + if railtype == "corner" or railtype == "tee" then + update_rail_connections(other_pos, {convert_neighbors = false}) + end + end + end +end +mod.update_rail_connections = update_rail_connections + +local north = vector.new(0,0,1) +local south = vector.new(0,0,-1) +local east = vector.new(1,0,0) +local west = vector.new(-1,0,0) + +local function is_ahead_slope(pos, dir) + local ahead = vector.add(pos,dir) + if mcl_minecarts.is_rail(ahead) then return false end + + local below = vector.offset(ahead,0,-1,0) + if not mcl_minecarts.is_rail(below) then return false end + + local node_name = force_get_node(below).name + return minetest.get_item_group(node_name, "rail_slope") ~= 0 +end + +local function get_rail_direction_inner(pos, dir) + -- Handle new track types that have track-specific direction handler + local node = minetest.get_node(pos) + local get_next_dir = get_path(minetest.registered_nodes,node.name,"_mcl_minecarts","get_next_dir") + if not get_next_dir then return dir end + + dir = get_next_dir(pos, dir, node) + + -- Handle reversing if there is a solid block in the next position + local next_pos = vector.add(pos, dir) + local next_node = minetest.get_node(next_pos) + local node_def = minetest.registered_nodes[next_node.name] + if node_def and node_def.groups and ( node_def.groups.solid or node_def.groups.stair ) then + -- Reverse the direction without giving -0 members + dir = vector.direction(next_pos, pos) + end + + -- Handle going downhill + if is_ahead_slope(pos,dir) then + dir = vector.offset(dir,0,-1,0) + end + + return dir +end +function mcl_minecarts.get_rail_direction(self, pos_, dir) + -- Compatibility with mcl_minecarts:get_rail_direction() usage + if self ~= mcl_minecarts then + dir = pos_ + pos_ = self + end + + local pos = vector.round(pos_) + + -- diagonal direction handling + if dir.x ~= 0 and dir.z ~= 0 then + -- Check both possible diagonal movements + local dir_a = vector.new(dir.x,0,0) + local dir_b = vector.new(0,0,dir.z) + local new_dir_a = mcl_minecarts.get_rail_direction(pos, dir_a) + local new_dir_b = mcl_minecarts.get_rail_direction(pos, dir_b) + + -- If either is the same diagonal direction, continue as you were + if vector.equals(dir,new_dir_a) or vector.equals(dir,new_dir_b) then + return dir + + -- Otherwise, if either would try to move in the same direction as + -- what tried, move that direction + elseif vector.equals(dir_a, new_dir_a) then + return new_dir_a + elseif vector.equals(dir_b, new_dir_b) then + return new_dir_b + end + + -- And if none of these were true, fall thru into standard behavior + end + + local new_dir = get_rail_direction_inner(pos, dir) + + if new_dir.y ~= 0 then return new_dir end + + -- Check four 45 degree movement + local next_rails_dir = get_rail_direction_inner(vector.add(pos, new_dir), new_dir) + if next_rails_dir.y == 0 and vector.equals(next_rails_dir, dir) and not vector.equals(new_dir, next_rails_dir) then + return vector.add(new_dir, next_rails_dir) + end + + return new_dir +end + +local _2_pi = math.pi * 2 +local _half_pi = math.pi * 0.5 +local _quart_pi = math.pi * 0.25 +local pi = math.pi +local rot_debug = {} +function mod.update_cart_orientation(self) + local staticdata = self._staticdata + local dir = staticdata.dir + + -- Calculate an angle from the x,z direction components + local rot_y = math.atan2( dir.z, dir.x ) + ( staticdata.rot_adjust or 0 ) + if rot_y < 0 then + rot_y = rot_y + _2_pi + end + + -- Check if the rotation is a 180 flip and don't change if so + local rot = self.object:get_rotation() + local old_rot = vector.new(rot) + rot.y = (rot.y - _half_pi + _2_pi) % _2_pi + if not rot then return end + + local diff = math.abs((rot_y - ( rot.y + pi ) % _2_pi) ) + if diff < 0.001 or diff > _2_pi - 0.001 then + -- Update rotation adjust + staticdata.rot_adjust = ( ( staticdata.rot_adjust or 0 ) + pi ) % _2_pi + else + rot.y = rot_y + end + + -- Forward/backwards tilt (pitch) + if dir.y > 0 then + rot.x = _quart_pi + elseif dir.y < 0 then + rot.x = -_quart_pi + else + rot.x = 0 + end + + if ( staticdata.rot_adjust or 0 ) < 0.01 then + rot.x = -rot.x + end + + rot.y = (rot.y + _half_pi) % _2_pi + self.object:set_rotation(rot) +end + +function mod.get_cart_position(cart_staticdata) + local data = cart_staticdata + if not data then return nil end + if not data.connected_at then return nil end + + return vector.add(data.connected_at, vector.multiply(data.dir or vector.zero(), data.distance or 0)) +end + +function mod.reverse_cart_direction(staticdata) + if staticdata.distance == 0 then + staticdata.dir = -staticdata.dir + return + end + + -- Complete moving thru this block into the next, reverse direction, and put us back at the same position we were at + local next_dir = -staticdata.dir + if not staticdata.connected_at then return end + + staticdata.connected_at = staticdata.connected_at + staticdata.dir + staticdata.distance = 1 - (staticdata.distance or 0) + + -- recalculate direction + local next_dir,_ = mod:get_rail_direction(staticdata.connected_at, next_dir) + staticdata.dir = next_dir +end + diff --git a/mods/ENTITIES/mcl_minecarts/init.lua b/mods/ENTITIES/mcl_minecarts/init.lua index 89ae44349..55960e296 100644 --- a/mods/ENTITIES/mcl_minecarts/init.lua +++ b/mods/ENTITIES/mcl_minecarts/init.lua @@ -1,1026 +1,17 @@ local modname = minetest.get_current_modname() -local S = minetest.get_translator(modname) - -local has_mcl_wip = minetest.get_modpath("mcl_wip") - +local modpath = minetest.get_modpath(modname) mcl_minecarts = {} -mcl_minecarts.modpath = minetest.get_modpath(modname) -mcl_minecarts.speed_max = 10 -mcl_minecarts.check_float_time = 15 +local mod = mcl_minecarts +mcl_minecarts.modpath = modpath -dofile(mcl_minecarts.modpath.."/functions.lua") -dofile(mcl_minecarts.modpath.."/rails.lua") +-- Constants +mod.SPEED_MAX = 10 +mod.FRICTION = 0.4 +mod.OFF_RAIL_FRICTION = 1.2 +mod.MAX_TRAIN_LENGTH = 4 +mod.CART_BLOCK_SIZE = 64 +mod.PASSENGER_ATTACH_POSITION = vector.new(0, -1.75, 0) -local LOGGING_ON = minetest.settings:get_bool("mcl_logging_minecarts", false) -local function mcl_log(message) - if LOGGING_ON then - mcl_util.mcl_log(message, "[Minecarts]", true) - end -end - - -local function detach_driver(self) - if not self._driver then - return - end - mcl_player.player_attached[self._driver] = nil - local player = minetest.get_player_by_name(self._driver) - self._driver = nil - self._start_pos = nil - if player then - player:set_detach() - player:set_eye_offset({x=0, y=0, z=0},{x=0, y=0, z=0}) - mcl_player.player_set_animation(player, "stand" , 30) - end -end - -local function activate_tnt_minecart(self, timer) - if self._boomtimer then - return - end - self.object:set_armor_groups({immortal=1}) - if timer then - self._boomtimer = timer - else - self._boomtimer = tnt.BOOMTIMER - end - self.object:set_properties({textures = { - "mcl_tnt_blink.png", - "mcl_tnt_blink.png", - "mcl_tnt_blink.png", - "mcl_tnt_blink.png", - "mcl_tnt_blink.png", - "mcl_tnt_blink.png", - "mcl_minecarts_minecart.png", - }}) - self._blinktimer = tnt.BLINKTIMER - minetest.sound_play("tnt_ignite", {pos = self.object:get_pos(), gain = 1.0, max_hear_distance = 15}, true) -end - -local activate_normal_minecart = detach_driver - -local function hopper_take_item(self, dtime) - local pos = self.object:get_pos() - if not pos then return end - - if not self or self.name ~= "mcl_minecarts:hopper_minecart" then return end - - if mcl_util.check_dtime_timer(self, dtime, "hoppermc_take", 0.15) then - --minetest.log("The check timer was triggered: " .. dump(pos) .. ", name:" .. self.name) - else - --minetest.log("The check timer was not triggered") - return - end - - --mcl_log("self.itemstring: ".. self.itemstring) - - local above_pos = vector.offset(pos, 0, 0.9, 0) - --mcl_log("self.itemstring: ".. minetest.pos_to_string(above_pos)) - local objs = minetest.get_objects_inside_radius(above_pos, 1.25) - - if objs then - - mcl_log("there is an itemstring. Number of objs: ".. #objs) - - for k, v in pairs(objs) do - local ent = v:get_luaentity() - - if ent and not ent._removed and ent.itemstring and ent.itemstring ~= "" then - local taken_items = false - - mcl_log("ent.name: " .. tostring(ent.name)) - mcl_log("ent pos: " .. tostring(ent.object:get_pos())) - - local inv = mcl_entity_invs.load_inv(self, 5) - if not inv then return false end - - local current_itemstack = ItemStack(ent.itemstring) - - mcl_log("inv. size: " .. self._inv_size) - if inv:room_for_item("main", current_itemstack) then - mcl_log("Room") - inv:add_item("main", current_itemstack) - ent.object:get_luaentity().itemstring = "" - ent.object:remove() - taken_items = true - else - mcl_log("no Room") - end - - if not taken_items then - local items_remaining = current_itemstack:get_count() - - -- This will take part of a floating item stack if no slot can hold the full amount - for i = 1, self._inv_size, 1 do - local stack = inv:get_stack("main", i) - - mcl_log("i: " .. tostring(i)) - mcl_log("Items remaining: " .. items_remaining) - mcl_log("Name: " .. tostring(stack:get_name())) - - if current_itemstack:get_name() == stack:get_name() then - mcl_log("We have a match. Name: " .. tostring(stack:get_name())) - - local room_for = stack:get_stack_max() - stack:get_count() - mcl_log("Room for: " .. tostring(room_for)) - - if room_for == 0 then - -- Do nothing - mcl_log("No room") - elseif room_for < items_remaining then - mcl_log("We have more items remaining than space") - - items_remaining = items_remaining - room_for - stack:set_count(stack:get_stack_max()) - inv:set_stack("main", i, stack) - taken_items = true - else - local new_stack_size = stack:get_count() + items_remaining - stack:set_count(new_stack_size) - mcl_log("We have more than enough space. Now holds: " .. new_stack_size) - - inv:set_stack("main", i, stack) - items_remaining = 0 - - ent.object:get_luaentity().itemstring = "" - ent.object:remove() - - taken_items = true - break - end - - mcl_log("Count: " .. tostring(stack:get_count())) - mcl_log("stack max: " .. tostring(stack:get_stack_max())) - --mcl_log("Is it empty: " .. stack:to_string()) - end - - if i == self._inv_size and taken_items then - mcl_log("We are on last item and still have items left. Set final stack size: " .. items_remaining) - current_itemstack:set_count(items_remaining) - --mcl_log("Itemstack2: " .. current_itemstack:to_string()) - ent.itemstring = current_itemstack:to_string() - end - end - end - - --Add in, and delete - if taken_items then - mcl_log("Saving") - mcl_entity_invs.save_inv(ent) - return taken_items - else - mcl_log("No need to save") - end - - end - end - end - - return false -end - --- Table for item-to-entity mapping. Keys: itemstring, Values: Corresponding entity ID -local entity_mapping = {} - -local function register_entity(entity_id, mesh, textures, drop, on_rightclick, on_activate_by_rail) - local cart = { - physical = false, - collisionbox = {-10/16., -0.5, -10/16, 10/16, 0.25, 10/16}, - visual = "mesh", - mesh = mesh, - visual_size = {x=1, y=1}, - textures = textures, - - on_rightclick = on_rightclick, - - _driver = nil, -- player who sits in and controls the minecart (only for minecart!) - _passenger = nil, -- for mobs - _punched = false, -- used to re-send _velocity and position - _velocity = {x=0, y=0, z=0}, -- only used on punch - _start_pos = nil, -- Used to calculate distance for “On A Rail” achievement - _last_float_check = nil, -- timestamp of last time the cart was checked to be still on a rail - _fueltime = nil, -- how many seconds worth of fuel is left. Only used by minecart with furnace - _boomtimer = nil, -- how many seconds are left before exploding - _blinktimer = nil, -- how many seconds are left before TNT blinking - _blink = false, -- is TNT blink texture active? - _old_dir = {x=0, y=0, z=0}, - _old_pos = nil, - _old_vel = {x=0, y=0, z=0}, - _old_switch = 0, - _railtype = nil, - } - - function cart:on_activate(staticdata, dtime_s) - -- Initialize - local data = minetest.deserialize(staticdata) - if type(data) == "table" then - self._railtype = data._railtype - self._passenger = data._passenger - end - self.object:set_armor_groups({immortal=1}) - - -- Activate cart if on activator rail - if self.on_activate_by_rail then - local pos = self.object:get_pos() - local node = minetest.get_node(vector.floor(pos)) - if node.name == "mcl_minecarts:activator_rail_on" then - self:on_activate_by_rail() - end - end - end - - function cart:on_punch(puncher, time_from_last_punch, tool_capabilities, direction) - local pos = self.object:get_pos() - if not self._railtype then - local node = minetest.get_node(vector.floor(pos)).name - self._railtype = minetest.get_item_group(node, "connect_to_raillike") - end - - if not puncher or not puncher:is_player() then - local cart_dir = mcl_minecarts:get_rail_direction(pos, {x=1, y=0, z=0}, nil, nil, self._railtype) - if vector.equals(cart_dir, {x=0, y=0, z=0}) then - return - end - mcl_minecarts:set_velocity(self, cart_dir) - return - end - - -- Punch+sneak: Pick up minecart (unless TNT was ignited) - if puncher:get_player_control().sneak and not self._boomtimer then - if self._driver then - if self._old_pos then - self.object:set_pos(self._old_pos) - end - detach_driver(self) - end - - -- Disable detector rail - local rou_pos = vector.round(pos) - local node = minetest.get_node(rou_pos) - if node.name == "mcl_minecarts:detector_rail_on" then - local newnode = {name="mcl_minecarts:detector_rail", param2 = node.param2} - minetest.swap_node(rou_pos, newnode) - mesecon.receptor_off(rou_pos) - end - - -- Drop items and remove cart entity - if not minetest.is_creative_enabled(puncher:get_player_name()) then - for d=1, #drop do - minetest.add_item(self.object:get_pos(), drop[d]) - end - elseif puncher and puncher:is_player() then - local inv = puncher:get_inventory() - for d=1, #drop do - if not inv:contains_item("main", drop[d]) then - inv:add_item("main", drop[d]) - end - end - end - - self.object:remove() - return - end - - local vel = self.object:get_velocity() - if puncher:get_player_name() == self._driver then - if math.abs(vel.x + vel.z) > 7 then - return - end - end - - local punch_dir = mcl_minecarts:velocity_to_dir(puncher:get_look_dir()) - punch_dir.y = 0 - local cart_dir = mcl_minecarts:get_rail_direction(pos, punch_dir, nil, nil, self._railtype) - if vector.equals(cart_dir, {x=0, y=0, z=0}) then - return - end - - time_from_last_punch = math.min(time_from_last_punch, tool_capabilities.full_punch_interval) - local f = 3 * (time_from_last_punch / tool_capabilities.full_punch_interval) - - mcl_minecarts:set_velocity(self, cart_dir, f) - end - - cart.on_activate_by_rail = on_activate_by_rail - - local passenger_attach_position = vector.new(0, -1.75, 0) - - function cart:on_step(dtime) - hopper_take_item(self, dtime) - - local ctrl, player = nil, nil - if self._driver then - player = minetest.get_player_by_name(self._driver) - if player then - ctrl = player:get_player_control() - -- player detach - if ctrl.sneak then - detach_driver(self) - return - end - end - end - - local vel = self.object:get_velocity() - local update = {} - if self._last_float_check == nil then - self._last_float_check = 0 - else - self._last_float_check = self._last_float_check + dtime - end - - local pos, rou_pos, node = self.object:get_pos() - local r = 0.6 - for _, node_pos in pairs({{r, 0}, {0, r}, {-r, 0}, {0, -r}}) do - if minetest.get_node(vector.offset(pos, node_pos[1], 0, node_pos[2])).name == "mcl_core:cactus" then - detach_driver(self) - for d = 1, #drop do - minetest.add_item(pos, drop[d]) - end - self.object:remove() - return - end - end - - -- Grab mob - if math.random(1,20) > 15 and not self._passenger then - if self.name == "mcl_minecarts:minecart" then - local mobsnear = minetest.get_objects_inside_radius(self.object:get_pos(), 1.3) - for n=1, #mobsnear do - local mob = mobsnear[n] - if mob then - local entity = mob:get_luaentity() - if entity and entity.is_mob then - self._passenger = entity - mob:set_attach(self.object, "", passenger_attach_position, vector.zero()) - break - end - end - end - end - elseif self._passenger then - local passenger_pos = self._passenger.object:get_pos() - if not passenger_pos then - self._passenger = nil - end - end - - -- Drop minecart if it isn't on a rail anymore - if self._last_float_check >= mcl_minecarts.check_float_time then - pos = self.object:get_pos() - rou_pos = vector.round(pos) - node = minetest.get_node(rou_pos) - local g = minetest.get_item_group(node.name, "connect_to_raillike") - if g ~= self._railtype and self._railtype then - -- Detach driver - if player then - if self._old_pos then - self.object:set_pos(self._old_pos) - end - mcl_player.player_attached[self._driver] = nil - player:set_detach() - player:set_eye_offset({x=0, y=0, z=0},{x=0, y=0, z=0}) - end - - -- Explode if already ignited - if self._boomtimer then - self.object:remove() - mcl_explosions.explode(pos, 4, { drop_chance = 1.0 }) - return - end - - -- Do not drop minecart. It goes off the rails too frequently, and anyone using them for farms won't - -- notice and lose their iron and not bother. Not cool until fixed. - end - self._last_float_check = 0 - end - - -- Update furnace stuff - if self._fueltime and self._fueltime > 0 then - self._fueltime = self._fueltime - dtime - if self._fueltime <= 0 then - self.object:set_properties({textures = - { - "default_furnace_top.png", - "default_furnace_top.png", - "default_furnace_front.png", - "default_furnace_side.png", - "default_furnace_side.png", - "default_furnace_side.png", - "mcl_minecarts_minecart.png", - }}) - self._fueltime = 0 - end - end - local has_fuel = self._fueltime and self._fueltime > 0 - - -- Update TNT stuff - if self._boomtimer then - -- Explode - self._boomtimer = self._boomtimer - dtime - local pos = self.object:get_pos() - if self._boomtimer <= 0 then - self.object:remove() - mcl_explosions.explode(pos, 4, { drop_chance = 1.0 }) - return - else - tnt.smoke_step(pos) - end - end - if self._blinktimer then - self._blinktimer = self._blinktimer - dtime - if self._blinktimer <= 0 then - self._blink = not self._blink - if self._blink then - self.object:set_properties({textures = - { - "default_tnt_top.png", - "default_tnt_bottom.png", - "default_tnt_side.png", - "default_tnt_side.png", - "default_tnt_side.png", - "default_tnt_side.png", - "mcl_minecarts_minecart.png", - }}) - else - self.object:set_properties({textures = - { - "mcl_tnt_blink.png", - "mcl_tnt_blink.png", - "mcl_tnt_blink.png", - "mcl_tnt_blink.png", - "mcl_tnt_blink.png", - "mcl_tnt_blink.png", - "mcl_minecarts_minecart.png", - }}) - end - self._blinktimer = tnt.BLINKTIMER - end - end - - if self._punched then - vel = vector.add(vel, self._velocity) - self.object:set_velocity(vel) - self._old_dir.y = 0 - elseif vector.equals(vel, {x=0, y=0, z=0}) and (not has_fuel) then - return - end - - local dir, last_switch, restart_pos = nil, nil, nil - if not pos then - pos = self.object:get_pos() - end - if self._old_pos and not self._punched then - local flo_pos = vector.floor(pos) - local flo_old = vector.floor(self._old_pos) - if vector.equals(flo_pos, flo_old) and (not has_fuel) then - return - -- Prevent querying the same node over and over again - end - - if not rou_pos then - rou_pos = vector.round(pos) - end - local rou_old = vector.round(self._old_pos) - if not node then - node = minetest.get_node(rou_pos) - end - local node_old = minetest.get_node(rou_old) - - -- Update detector rails - if node.name == "mcl_minecarts:detector_rail" then - local newnode = {name="mcl_minecarts:detector_rail_on", param2 = node.param2} - minetest.swap_node(rou_pos, newnode) - mesecon.receptor_on(rou_pos) - end - if node.name == "mcl_minecarts:golden_rail_on" then - restart_pos = rou_pos - end - if node_old.name == "mcl_minecarts:detector_rail_on" then - local newnode = {name="mcl_minecarts:detector_rail", param2 = node_old.param2} - minetest.swap_node(rou_old, newnode) - mesecon.receptor_off(rou_old) - end - -- Activate minecart if on activator rail - if node_old.name == "mcl_minecarts:activator_rail_on" and self.on_activate_by_rail then - self:on_activate_by_rail() - end - end - - -- Stop cart if velocity vector flips - if self._old_vel and self._old_vel.y == 0 and - (self._old_vel.x * vel.x < 0 or self._old_vel.z * vel.z < 0) then - self._old_vel = {x = 0, y = 0, z = 0} - self._old_pos = pos - self.object:set_velocity(vector.new()) - self.object:set_acceleration(vector.new()) - return - end - self._old_vel = vector.new(vel) - - if self._old_pos then - local diff = vector.subtract(self._old_pos, pos) - for _,v in ipairs({"x","y","z"}) do - if math.abs(diff[v]) > 1.1 then - local expected_pos = vector.add(self._old_pos, self._old_dir) - dir, last_switch = mcl_minecarts:get_rail_direction(pos, self._old_dir, ctrl, self._old_switch, self._railtype) - if vector.equals(dir, {x=0, y=0, z=0}) then - dir = false - pos = vector.new(expected_pos) - update.pos = true - end - break - end - end - end - - if vel.y == 0 then - for _,v in ipairs({"x", "z"}) do - if vel[v] ~= 0 and math.abs(vel[v]) < 0.9 then - vel[v] = 0 - update.vel = true - end - end - end - - local cart_dir = mcl_minecarts:velocity_to_dir(vel) - local max_vel = mcl_minecarts.speed_max - if not dir then - dir, last_switch = mcl_minecarts:get_rail_direction(pos, cart_dir, ctrl, self._old_switch, self._railtype) - end - - local new_acc = {x=0, y=0, z=0} - if vector.equals(dir, {x=0, y=0, z=0}) and not has_fuel then - vel = {x=0, y=0, z=0} - update.vel = true - else - -- If the direction changed - if dir.x ~= 0 and self._old_dir.z ~= 0 then - vel.x = dir.x * math.abs(vel.z) - vel.z = 0 - pos.z = math.floor(pos.z + 0.5) - update.pos = true - end - if dir.z ~= 0 and self._old_dir.x ~= 0 then - vel.z = dir.z * math.abs(vel.x) - vel.x = 0 - pos.x = math.floor(pos.x + 0.5) - update.pos = true - end - -- Up, down? - if dir.y ~= self._old_dir.y then - vel.y = dir.y * math.abs(vel.x + vel.z) - pos = vector.round(pos) - update.pos = true - end - - -- Slow down or speed up - local acc = dir.y * -1.8 - local friction = 0.4 - local ndef = minetest.registered_nodes[minetest.get_node(pos).name] - local speed_mod = ndef and ndef._rail_acceleration - - acc = acc - friction - - if has_fuel then - acc = acc + 0.6 - end - - if speed_mod and speed_mod ~= 0 then - acc = acc + speed_mod + friction - end - - new_acc = vector.multiply(dir, acc) - end - - self.object:set_acceleration(new_acc) - self._old_pos = vector.new(pos) - self._old_dir = vector.new(dir) - self._old_switch = last_switch - - -- Limits - for _,v in ipairs({"x","y","z"}) do - if math.abs(vel[v]) > max_vel then - vel[v] = mcl_minecarts:get_sign(vel[v]) * max_vel - new_acc[v] = 0 - update.vel = true - end - end - - -- Give achievement when player reached a distance of 1000 nodes from the start position - if self._driver and (vector.distance(self._start_pos, pos) >= 1000) then - awards.unlock(self._driver, "mcl:onARail") - end - - - if update.pos or self._punched then - local yaw = 0 - if dir.x < 0 then - yaw = 0.5 - elseif dir.x > 0 then - yaw = 1.5 - elseif dir.z < 0 then - yaw = 1 - end - self.object:set_yaw(yaw * math.pi) - end - - if self._punched then - self._punched = false - end - - if not (update.vel or update.pos) then - return - end - - - local anim = {x=0, y=0} - if dir.y == -1 then - anim = {x=1, y=1} - elseif dir.y == 1 then - anim = {x=2, y=2} - end - self.object:set_animation(anim, 1, 0) - - self.object:set_velocity(vel) - if update.pos then - self.object:set_pos(pos) - end - - -- stopped on "mcl_minecarts:golden_rail_on" - if vector.equals(vel, {x=0, y=0, z=0}) and restart_pos then - local dir = mcl_minecarts:get_start_direction(restart_pos) - if dir then - mcl_minecarts:set_velocity(self, dir) - end - end - end - - function cart:get_staticdata() - return minetest.serialize({_railtype = self._railtype}) - end - - minetest.register_entity(entity_id, cart) -end - --- Place a minecart at pointed_thing -function mcl_minecarts.place_minecart(itemstack, pointed_thing, placer) - if not pointed_thing.type == "node" then - return - end - - local railpos, node - if mcl_minecarts:is_rail(pointed_thing.under) then - railpos = pointed_thing.under - node = minetest.get_node(pointed_thing.under) - elseif mcl_minecarts:is_rail(pointed_thing.above) then - railpos = pointed_thing.above - node = minetest.get_node(pointed_thing.above) - else - return - end - - -- Activate detector rail - if node.name == "mcl_minecarts:detector_rail" then - local newnode = {name="mcl_minecarts:detector_rail_on", param2 = node.param2} - minetest.swap_node(railpos, newnode) - mesecon.receptor_on(railpos) - end - - local entity_id = entity_mapping[itemstack:get_name()] - local cart = minetest.add_entity(railpos, entity_id) - local railtype = minetest.get_item_group(node.name, "connect_to_raillike") - local le = cart:get_luaentity() - if le then - le._railtype = railtype - end - local cart_dir - if node.name == "mcl_minecarts:golden_rail_on" then - cart_dir = mcl_minecarts:get_start_direction(railpos) - end - if cart_dir then - mcl_minecarts:set_velocity(le, cart_dir) - else - cart_dir = mcl_minecarts:get_rail_direction(railpos, {x=1, y=0, z=0}, nil, nil, railtype) - end - cart:set_yaw(minetest.dir_to_yaw(cart_dir)) - - local pname = "" - if placer then - pname = placer:get_player_name() - end - if not minetest.is_creative_enabled(pname) then - itemstack:take_item() - end - return itemstack -end - - -local function register_craftitem(itemstring, entity_id, description, tt_help, longdesc, usagehelp, icon, creative) - entity_mapping[itemstring] = entity_id - - local groups = { minecart = 1, transport = 1 } - if creative == false then - groups.not_in_creative_inventory = 1 - end - local def = { - stack_max = 1, - on_place = function(itemstack, placer, pointed_thing) - if not pointed_thing.type == "node" then - return - end - - -- Call on_rightclick if the pointed node defines it - local node = minetest.get_node(pointed_thing.under) - if placer and not placer:get_player_control().sneak then - if minetest.registered_nodes[node.name] and minetest.registered_nodes[node.name].on_rightclick then - return minetest.registered_nodes[node.name].on_rightclick(pointed_thing.under, node, placer, itemstack) or itemstack - end - end - - return mcl_minecarts.place_minecart(itemstack, pointed_thing, placer) - end, - _on_dispense = function(stack, pos, droppos, dropnode, dropdir) - -- Place minecart as entity on rail. If there's no rail, just drop it. - local placed - if minetest.get_item_group(dropnode.name, "rail") ~= 0 then - -- FIXME: This places minecarts even if the spot is already occupied - local pointed_thing = { under = droppos, above = { x=droppos.x, y=droppos.y+1, z=droppos.z } } - placed = mcl_minecarts.place_minecart(stack, pointed_thing) - end - if placed == nil then - -- Drop item - minetest.add_item(droppos, stack) - end - end, - groups = groups, - } - def.description = description - def._tt_help = tt_help - def._doc_items_longdesc = longdesc - def._doc_items_usagehelp = usagehelp - def.inventory_image = icon - def.wield_image = icon - minetest.register_craftitem(itemstring, def) -end - ---[[ -Register a minecart -* itemstring: Itemstring of minecart item -* entity_id: ID of minecart entity -* description: Item name / description -* longdesc: Long help text -* usagehelp: Usage help text -* mesh: Minecart mesh -* textures: Minecart textures table -* icon: Item icon -* drop: Dropped items after destroying minecart -* on_rightclick: Called after rightclick -* on_activate_by_rail: Called when above activator rail -* creative: If false, don't show in Creative Inventory -]] -local function register_minecart(itemstring, entity_id, description, tt_help, longdesc, usagehelp, mesh, textures, icon, drop, on_rightclick, on_activate_by_rail, creative) - register_entity(entity_id, mesh, textures, drop, on_rightclick, on_activate_by_rail) - register_craftitem(itemstring, entity_id, description, tt_help, longdesc, usagehelp, icon, creative) - if minetest.get_modpath("doc_identifier") then - doc.sub.identifier.register_object(entity_id, "craftitems", itemstring) - end -end - --- Minecart -register_minecart( - "mcl_minecarts:minecart", - "mcl_minecarts:minecart", - S("Minecart"), - S("Vehicle for fast travel on rails"), - S("Minecarts can be used for a quick transportion on rails.") .. "\n" .. - S("Minecarts only ride on rails and always follow the tracks. At a T-junction with no straight way ahead, they turn left. The speed is affected by the rail type."), - S("You can place the minecart on rails. Right-click it to enter it. Punch it to get it moving.") .. "\n" .. - S("To obtain the minecart, punch it while holding down the sneak key.") .. "\n" .. - S("If it moves over a powered activator rail, you'll get ejected."), - "mcl_minecarts_minecart.b3d", - {"mcl_minecarts_minecart.png"}, - "mcl_minecarts_minecart_normal.png", - {"mcl_minecarts:minecart"}, - function(self, clicker) - local name = clicker:get_player_name() - if not clicker or not clicker:is_player() then - return - end - local player_name = clicker:get_player_name() - if self._driver and player_name == self._driver then - detach_driver(self) - elseif not self._driver then - self._driver = player_name - self._start_pos = self.object:get_pos() - mcl_player.player_attached[player_name] = true - clicker:set_attach(self.object, "", {x=0, y=-1.75, z=-2}, {x=0, y=0, z=0}) - mcl_player.player_attached[name] = true - minetest.after(0.2, function(name) - local player = minetest.get_player_by_name(name) - if player then - mcl_player.player_set_animation(player, "sit" , 30) - player:set_eye_offset({x=0, y=-5.5, z=0},{x=0, y=-4, z=0}) - mcl_title.set(clicker, "actionbar", {text=S("Sneak to dismount"), color="white", stay=60}) - end - end, name) - end - end, activate_normal_minecart -) - --- Minecart with Chest -register_minecart( - "mcl_minecarts:chest_minecart", - "mcl_minecarts:chest_minecart", - S("Minecart with Chest"), - nil, nil, nil, - "mcl_minecarts_minecart_chest.b3d", - { "mcl_chests_normal.png", "mcl_minecarts_minecart.png" }, - "mcl_minecarts_minecart_chest.png", - {"mcl_minecarts:minecart", "mcl_chests:chest"}, - nil, nil, true) -mcl_entity_invs.register_inv("mcl_minecarts:chest_minecart","Minecart",27,false,true) - --- Minecart with Furnace -register_minecart( - "mcl_minecarts:furnace_minecart", - "mcl_minecarts:furnace_minecart", - S("Minecart with Furnace"), - nil, - S("A minecart with furnace is a vehicle that travels on rails. It can propel itself with fuel."), - S("Place it on rails. If you give it some coal, the furnace will start burning for a long time and the minecart will be able to move itself. Punch it to get it moving.") .. "\n" .. - S("To obtain the minecart and furnace, punch them while holding down the sneak key."), - - "mcl_minecarts_minecart_block.b3d", - { - "default_furnace_top.png", - "default_furnace_top.png", - "default_furnace_front.png", - "default_furnace_side.png", - "default_furnace_side.png", - "default_furnace_side.png", - "mcl_minecarts_minecart.png", - }, - "mcl_minecarts_minecart_furnace.png", - {"mcl_minecarts:minecart", "mcl_furnaces:furnace"}, - -- Feed furnace with coal - function(self, clicker) - if not clicker or not clicker:is_player() then - return - end - if not self._fueltime then - self._fueltime = 0 - end - local held = clicker:get_wielded_item() - if minetest.get_item_group(held:get_name(), "coal") == 1 then - self._fueltime = self._fueltime + 180 - - if not minetest.is_creative_enabled(clicker:get_player_name()) then - held:take_item() - local index = clicker:get_wield_index() - local inv = clicker:get_inventory() - inv:set_stack("main", index, held) - end - self.object:set_properties({textures = - { - "default_furnace_top.png", - "default_furnace_top.png", - "default_furnace_front_active.png", - "default_furnace_side.png", - "default_furnace_side.png", - "default_furnace_side.png", - "mcl_minecarts_minecart.png", - }}) - end - end, nil, true -) - --- Minecart with Command Block -register_minecart( - "mcl_minecarts:command_block_minecart", - "mcl_minecarts:command_block_minecart", - S("Minecart with Command Block"), - nil, nil, nil, - "mcl_minecarts_minecart_block.b3d", - { - "jeija_commandblock_off.png^[verticalframe:2:0", - "jeija_commandblock_off.png^[verticalframe:2:0", - "jeija_commandblock_off.png^[verticalframe:2:0", - "jeija_commandblock_off.png^[verticalframe:2:0", - "jeija_commandblock_off.png^[verticalframe:2:0", - "jeija_commandblock_off.png^[verticalframe:2:0", - "mcl_minecarts_minecart.png", - }, - "mcl_minecarts_minecart_command_block.png", - {"mcl_minecarts:minecart"}, - nil, nil, false -) - --- Minecart with Hopper -register_minecart( - "mcl_minecarts:hopper_minecart", - "mcl_minecarts:hopper_minecart", - S("Minecart with Hopper"), - nil, nil, nil, - "mcl_minecarts_minecart_hopper.b3d", - { - "mcl_hoppers_hopper_inside.png", - "mcl_minecarts_minecart.png", - "mcl_hoppers_hopper_outside.png", - "mcl_hoppers_hopper_top.png", - }, - "mcl_minecarts_minecart_hopper.png", - {"mcl_minecarts:minecart", "mcl_hoppers:hopper"}, - nil, nil, true -) -mcl_entity_invs.register_inv("mcl_minecarts:hopper_minecart", "Hopper Minecart", 5, false, true) - --- Minecart with TNT -register_minecart( - "mcl_minecarts:tnt_minecart", - "mcl_minecarts:tnt_minecart", - S("Minecart with TNT"), - S("Vehicle for fast travel on rails").."\n"..S("Can be ignited by tools or powered activator rail"), - S("A minecart with TNT is an explosive vehicle that travels on rail."), - S("Place it on rails. Punch it to move it. The TNT is ignited with a flint and steel or when the minecart is on an powered activator rail.") .. "\n" .. - S("To obtain the minecart and TNT, punch them while holding down the sneak key. You can't do this if the TNT was ignited."), - "mcl_minecarts_minecart_block.b3d", - { - "default_tnt_top.png", - "default_tnt_bottom.png", - "default_tnt_side.png", - "default_tnt_side.png", - "default_tnt_side.png", - "default_tnt_side.png", - "mcl_minecarts_minecart.png", - }, - "mcl_minecarts_minecart_tnt.png", - {"mcl_minecarts:minecart", "mcl_tnt:tnt"}, - -- Ingite - function(self, clicker) - if not clicker or not clicker:is_player() then - return - end - if self._boomtimer then - return - end - local held = clicker:get_wielded_item() - if held:get_name() == "mcl_fire:flint_and_steel" then - if not minetest.is_creative_enabled(clicker:get_player_name()) then - held:add_wear(65535/65) -- 65 uses - local index = clicker:get_wield_index() - local inv = clicker:get_inventory() - inv:set_stack("main", index, held) - end - activate_tnt_minecart(self) - end - end, activate_tnt_minecart) - - -minetest.register_craft({ - output = "mcl_minecarts:minecart", - recipe = { - {"mcl_core:iron_ingot", "", "mcl_core:iron_ingot"}, - {"mcl_core:iron_ingot", "mcl_core:iron_ingot", "mcl_core:iron_ingot"}, - }, -}) - -minetest.register_craft({ - output = "mcl_minecarts:tnt_minecart", - recipe = { - {"mcl_tnt:tnt"}, - {"mcl_minecarts:minecart"}, - }, -}) - -minetest.register_craft({ - output = "mcl_minecarts:furnace_minecart", - recipe = { - {"mcl_furnaces:furnace"}, - {"mcl_minecarts:minecart"}, - }, -}) - -minetest.register_craft({ - output = "mcl_minecarts:hopper_minecart", - recipe = { - {"mcl_hoppers:hopper"}, - {"mcl_minecarts:minecart"}, - }, -}) - - -minetest.register_craft({ - output = "mcl_minecarts:chest_minecart", - recipe = { - {"mcl_chests:chest"}, - {"mcl_minecarts:minecart"}, - }, -}) - - -if has_mcl_wip then - mcl_wip.register_wip_item("mcl_minecarts:chest_minecart") - mcl_wip.register_wip_item("mcl_minecarts:furnace_minecart") - mcl_wip.register_wip_item("mcl_minecarts:command_block_minecart") +for _,filename in pairs({"storage","functions","rails","train","carts"}) do + dofile(modpath.."/"..filename..".lua") end diff --git a/mods/ENTITIES/mcl_minecarts/mod.conf b/mods/ENTITIES/mcl_minecarts/mod.conf index b810c2b6a..df0f02bff 100644 --- a/mods/ENTITIES/mcl_minecarts/mod.conf +++ b/mods/ENTITIES/mcl_minecarts/mod.conf @@ -1,5 +1,5 @@ name = mcl_minecarts author = Krock description = Minecarts are vehicles to move players quickly on rails. -depends = mcl_title, mcl_explosions, mcl_core, mcl_sounds, mcl_player, mcl_achievements, mcl_chests, mcl_furnaces, mesecons_commandblock, mcl_hoppers, mcl_tnt, mesecons, mcl_entity_invs -optional_depends = doc_identifier, mcl_wip +depends = mcl_title, mcl_explosions, mcl_core, mcl_util, mcl_sounds, mcl_player, mcl_playerinfo, mcl_achievements, mcl_chests, mcl_furnaces, mesecons_commandblock, mcl_hoppers, mcl_tnt, mesecons, mcl_entity_invs, vl_legacy +optional_depends = doc_identifier, mcl_wip, mcl_physics, vl_physics diff --git a/mods/ENTITIES/mcl_minecarts/models/flat_track.obj b/mods/ENTITIES/mcl_minecarts/models/flat_track.obj new file mode 100644 index 000000000..4df51c509 --- /dev/null +++ b/mods/ENTITIES/mcl_minecarts/models/flat_track.obj @@ -0,0 +1,15 @@ +# hand-made Wavefront .OBJ file for sloped rail +mtllib mcl_minecarts_rail.mtl +o flat_track.001 +v -0.500000 -0.437500 -0.500000 +v -0.500000 -0.437500 0.500000 +v 0.500000 -0.437500 0.500000 +v 0.500000 -0.437500 -0.500000 +vt 1.000000 0.000000 +vt 1.000000 1.000000 +vt 0.000000 1.000000 +vt 0.000000 0.000000 +vn 0.000000 1.000000 0.000000 +usemtl None +s off +f 1/1/1 2/2/1 3/3/1 4/4/1 diff --git a/mods/ENTITIES/mcl_minecarts/models/sloped_track.obj b/mods/ENTITIES/mcl_minecarts/models/sloped_track.obj new file mode 100644 index 000000000..b5c106278 --- /dev/null +++ b/mods/ENTITIES/mcl_minecarts/models/sloped_track.obj @@ -0,0 +1,15 @@ +# hand-made Wavefront .OBJ file for sloped rail +mtllib mcl_minecarts_rail.mtl +o sloped_rail.001 +v -0.500000 -0.437500 -0.500000 +v -0.500000 0.562500 0.500000 +v 0.500000 0.562500 0.500000 +v 0.500000 -0.437500 -0.500000 +vt 1.000000 0.000000 +vt 1.000000 1.000000 +vt 0.000000 1.000000 +vt 0.000000 0.000000 +vn 0.707106 0.707106 0.000000 +usemtl None +s off +f 1/1/1 2/2/1 3/3/1 4/4/1 diff --git a/mods/ENTITIES/mcl_minecarts/movement.lua b/mods/ENTITIES/mcl_minecarts/movement.lua new file mode 100644 index 000000000..ce449a89b --- /dev/null +++ b/mods/ENTITIES/mcl_minecarts/movement.lua @@ -0,0 +1,632 @@ +local modname = minetest.get_current_modname() +local modpath = minetest.get_modpath(modname) +local mod = mcl_minecarts +local S = minetest.get_translator(modname) +local submod = {} +local ENABLE_TRAINS = core.settings:get_bool("mcl_minecarts_enable_trains",true) + +-- Constants +local mcl_debug,DEBUG = mcl_util.make_mcl_logger("mcl_logging_minecart_debug", "Minecart Debug") +--DEBUG = false +--mcl_debug,DEBUG = function(msg) print(msg) end,true + +-- Imports +local env_physics +if minetest.get_modpath("mcl_physics") then + env_physics = mcl_physics +elseif minetest.get_modpath("vl_physics") then + env_physics = vl_physics +end +local FRICTION = mod.FRICTION +local OFF_RAIL_FRICTION = mod.OFF_RAIL_FRICTION +local MAX_TRAIN_LENGTH = mod.MAX_TRAIN_LENGTH +local SPEED_MAX = mod.SPEED_MAX +local train_length = mod.train_length +local update_train = mod.update_train +local reverse_train = mod.reverse_train +local link_cart_ahead = mod.link_cart_ahead +local update_cart_orientation = mod.update_cart_orientation +local get_cart_data = mod.get_cart_data +local get_cart_position = mod.get_cart_position + + +local function reverse_direction(staticdata) + if staticdata.behind or staticdata.ahead then + reverse_train(staticdata) + return + end + + mod.reverse_cart_direction(staticdata) +end +mod.reverse_direction = reverse_direction + + +--[[ + Array of hooks { {u,v,w}, name } + Actual position is pos + u * dir + v * right + w * up +]] +local enter_exit_checks = { + { 0, 0, 0, "" }, + { 0, 0, 1, "_above" }, + { 0, 0,-1, "_below" }, + { 0, 1, 0, "_side" }, + { 0,-1, 0, "_side" }, +} + +local function handle_cart_enter_exit(staticdata, pos, next_dir, event) + local luaentity = mcl_util.get_luaentity_from_uuid(staticdata.uuid) + local dir = staticdata.dir + local right = vector.new( dir.z, dir.y, -dir.x) + local up = vector.new(0,1,0) + for i=1,#enter_exit_checks do + local check = enter_exit_checks[i] + + local check_pos = pos + dir * check[1] + right * check[2] + up * check[3] + local node = minetest.get_node(check_pos) + local node_def = minetest.registered_nodes[node.name] + if node_def then + -- node-specific hook + local hook_name = "_mcl_minecarts_"..event..check[4] + local hook = node_def[hook_name] + if hook then hook(check_pos, luaentity, next_dir, pos, staticdata) end + + -- global minecart hook + hook = mcl_minecarts[event..check[4]] + if hook then hook(check_pos, luaentity, next_dir, pos, staticdata, node_def) end + end + end + + -- Handle cart-specific behaviors + if luaentity then + local hook = luaentity["_mcl_minecarts_"..event] + if hook then hook(luaentity, pos, staticdata) end + else + --minetest.log("warning", "TODO: change _mcl_minecarts_"..event.." calling so it is not dependent on the existence of a luaentity") + end +end +local function set_metadata_cart_status(pos, uuid, state) + local meta = minetest.get_meta(pos) + local carts = minetest.deserialize(meta:get_string("_mcl_minecarts_carts")) or {} + carts[uuid] = state + meta:set_string("_mcl_minecarts_carts", minetest.serialize(carts)) +end +local function handle_cart_enter(staticdata, pos, next_dir) + --print("entering "..tostring(pos)) + set_metadata_cart_status(pos, staticdata.uuid, 1) + handle_cart_enter_exit(staticdata, pos, next_dir, "on_enter" ) +end +submod.handle_cart_enter = handle_cart_enter +local function handle_cart_leave(staticdata, pos, next_dir) + --print("leaving "..tostring(pos)) + set_metadata_cart_status(pos, staticdata.uuid, nil) + handle_cart_enter_exit(staticdata, pos, next_dir, "on_leave" ) +end +local function handle_cart_node_watches(staticdata, dtime) + local watches = staticdata.node_watches or {} + local new_watches = {} + local luaentity = mcl_util.get_luaentity_from_uuid(staticdata.uuid) + for i=1,#watches do + local node_pos = watches[i] + local node = minetest.get_node(node_pos) + local node_def = minetest.registered_nodes[node.name] + if node_def then + local hook = node_def._mcl_minecarts_node_on_step + if hook and hook(node_pos, luaentity, dtime, staticdata) then + new_watches[#new_watches+1] = node_pos + end + end + end + + staticdata.node_watches = new_watches +end + +local function detach_minecart(staticdata) + handle_cart_leave(staticdata, staticdata.connected_at, staticdata.dir) + staticdata.connected_at = nil + mod.break_train_at(staticdata) + + local luaentity = mcl_util.get_luaentity_from_uuid(staticdata.uuid) + if luaentity then + luaentity.object:set_velocity(staticdata.dir * staticdata.velocity) + end +end +mod.detach_minecart = detach_minecart + +local function try_detach_minecart(staticdata) + if not staticdata or not staticdata.connected_at then return end + if not mod:is_rail(staticdata.connected_at) then + mcl_debug("Detaching minecart #"..tostring(staticdata.uuid)) + detach_minecart(staticdata) + end +end + +local function handle_cart_collision(cart1_staticdata, prev_pos, next_dir) + if not cart1_staticdata then return end + + -- Look ahead one block + local pos = vector.add(prev_pos, next_dir) + + local meta = minetest.get_meta(pos) + local carts = minetest.deserialize(meta:get_string("_mcl_minecarts_carts")) or {} + local cart_uuid = nil + local dirty = false + for uuid,v in pairs(carts) do + -- Clean up dead carts + local data = get_cart_data(uuid) + if not data or not data.connected_at then + carts[uuid] = nil + dirty = true + uuid = nil + end + + if uuid and uuid ~= cart1_staticdata.uuid then cart_uuid = uuid end + end + if dirty then + meta:set_string("_mcl_minecarts_carts",minetest.serialize(carts)) + end + + local meta = minetest.get_meta(vector.add(pos,next_dir)) + if not cart_uuid then return end + + -- Don't collide with the train car in front of you + if cart1_staticdata.ahead == cart_uuid then return end + + --minetest.log("action","cart #"..cart1_staticdata.uuid.." collided with cart #"..cart_uuid.." at "..tostring(pos)) + + -- Standard Collision Handling + local cart2_staticdata = get_cart_data(cart_uuid) + + local u1 = cart1_staticdata.velocity + local u2 = cart2_staticdata.velocity + local m1 = cart1_staticdata.mass + local m2 = cart2_staticdata.mass + + if ENABLE_TRAINS and u2 == 0 and u1 < 4 and train_length(cart1_staticdata) < MAX_TRAIN_LENGTH then + link_cart_ahead(cart1_staticdata, cart2_staticdata) + cart2_staticdata.dir = mcl_minecarts.get_rail_direction(cart2_staticdata.connected_at, cart1_staticdata.dir) + cart2_staticdata.velocity = cart1_staticdata.velocity + return + end + + -- Reverse direction of the second cart if it is pointing in the wrong direction for this collision + local rel = vector.direction(cart1_staticdata.connected_at, cart2_staticdata.connected_at) + local dir2 = cart2_staticdata.dir + local col_dir = vector.dot(rel, dir2) + if col_dir < 0 then + cart2_staticdata.dir = -dir2 + u2 = -u2 + end + + -- Calculate new velocities according to https://en.wikipedia.org/wiki/Elastic_collision#One-dimensional_Newtonian + local c1 = m1 + m2 + local d = m1 - m2 + local v1 = ( d * u1 + 2 * m2 * u2 ) / c1 + local v2 = ( 2 * m1 * u1 + d * u2 ) / c1 + + cart1_staticdata.velocity = v1 + cart2_staticdata.velocity = v2 +end + +local function vector_away_from_players(cart, staticdata) + local function player_repel(obj) + -- Only repel from players + local player_name = obj:get_player_name() + if not player_name or player_name == "" then return false end + + -- Don't repel away from players in minecarts + local player_meta = mcl_playerinfo.get_mod_meta(player_name, modname) + if player_meta.attached_to then return false end + + return true + end + + -- Get the cart position + local cart_pos = mod.get_cart_position(staticdata) + if cart then cart_pos = cart.object:get_pos() end + if not cart_pos then return nil end + + for _,obj in pairs(minetest.get_objects_inside_radius(cart_pos, 1.1)) do + if player_repel(obj) then + return obj:get_pos() - cart_pos + end + end + + return nil +end + +local function direction_away_from_players(staticdata) + local diff = vector_away_from_players(nil,staticdata) + if not diff then return 0 end + + local length = vector.distance(vector.zero(),diff) + local vec = diff / length + local force = vector.dot( vec, vector.normalize(staticdata.dir) ) + + -- Check if this would push past the end of the track and don't move it it would + -- This prevents an oscillation that would otherwise occur + local dir = staticdata.dir + if force > 0 then + dir = -dir + end + if mcl_minecarts.is_rail( staticdata.connected_at + dir ) then + if force > 0.5 then + return -length * 4 + elseif force < -0.5 then + return length * 4 + end + end + return 0 +end + +local look_directions = { + [0] = mod.north, + mod.west, + mod.south, + mod.east, +} +local function calculate_acceleration(staticdata) + local acceleration = 0 + + -- Fix up movement data + staticdata.velocity = staticdata.velocity or 0 + + -- Apply friction if moving + if staticdata.velocity > 0 then + acceleration = -FRICTION + end + + local pos = staticdata.connected_at + local node_name = minetest.get_node(pos).name + local node_def = minetest.registered_nodes[node_name] + + local ctrl = staticdata.controls or {} + local time_active = minetest.get_gametime() - 0.25 + + if (ctrl.forward or 0) > time_active then + if staticdata.velocity <= 0.05 then + local look_dir = look_directions[ctrl.look or 0] or mod.north + local dot = vector.dot(staticdata.dir, look_dir) + if dot < 0 then + reverse_direction(staticdata) + end + end + acceleration = 4 + elseif (ctrl.brake or 0) > time_active then + acceleration = -1.5 + elseif (staticdata.fueltime or 0) > 0 and staticdata.velocity <= 4 then + acceleration = 0.6 + elseif staticdata.velocity >= ( node_def._max_acceleration_velocity or SPEED_MAX ) then + -- Standard friction + elseif node_def and node_def._rail_acceleration then + local rail_accel = node_def._rail_acceleration + if type(rail_accel) == "function" then + acceleration = (rail_accel(pos, staticdata) or 0) * 4 + else + acceleration = rail_accel * 4 + end + end + + -- Factor in gravity after everything else + local gravity_strength = 2.45 --friction * 5 + if staticdata.dir.y < 0 then + acceleration = gravity_strength - FRICTION + elseif staticdata.dir.y > 0 then + acceleration = -gravity_strength + FRICTION + end + + return acceleration +end + +local function do_movement_step(staticdata, dtime) + if not staticdata.connected_at then return 0 end + + -- Calculate timestep remaiing in this block + local x_0 = staticdata.distance or 0 + local remaining_in_block = 1 - x_0 + + -- Apply velocity impulse + local v_0 = staticdata.velocity or 0 + local ctrl = staticdata.controls or {} + if ctrl.impulse then + local impulse = ctrl.impulse + ctrl.impulse = nil + + local old_v_0 = v_0 + local new_v_0 = v_0 + impulse + if new_v_0 > SPEED_MAX then + new_v_0 = SPEED_MAX + elseif new_v_0 < 0.025 then + new_v_0 = 0 + end + v_0 = new_v_0 + end + + -- Calculate acceleration + local a = 0 + if staticdata.ahead or staticdata.behind then + -- Calculate acceleration of the entire train + local count = 0 + for cart in mod.train_cars(staticdata) do + count = count + 1 + if cart.behind then + a = a + calculate_acceleration(cart) + end + end + a = a / count + else + a = calculate_acceleration(staticdata) + end + + -- Repel minecarts + local away = direction_away_from_players(staticdata) + if away > 0 then + v_0 = away + elseif away < 0 then + reverse_direction(staticdata) + v_0 = -away + end + + if DEBUG and ( v_0 > 0 or a ~= 0 ) then + mcl_debug(" cart "..tostring(staticdata.uuid).. + ": a="..tostring(a).. + ",v_0="..tostring(v_0).. + ",x_0="..tostring(x_0).. + ",dtime="..tostring(dtime).. + ",dir="..tostring(staticdata.dir).. + ",connected_at="..tostring(staticdata.connected_at).. + ",distance="..tostring(staticdata.distance) + ) + end + + -- Not moving + if a == 0 and v_0 == 0 then return 0 end + + -- Prevent movement into solid blocks + if staticdata.distance == 0 then + local next_node = core.get_node(staticdata.connected_at + staticdata.dir) + local next_node_def = core.registered_nodes[next_node.name] + if not next_node_def or next_node_def.groups and (next_node_def.groups.solid or next_node_def.groups.stair) then + reverse_direction(staticdata) + return 0 + end + end + + -- Movement equation with acceleration: x_1 = x_0 + v_0 * t + 0.5 * a * t*t + local timestep + local stops_in_block = false + local inside = v_0 * v_0 + 2 * a * remaining_in_block + if inside < 0 then + -- Would stop or reverse direction inside this block, calculate time to v_1 = 0 + timestep = -v_0 / a + stops_in_block = true + if timestep <= 0.01 then + reverse_direction(staticdata) + end + elseif a ~= 0 then + -- Setting x_1 = x_0 + remaining_in_block, and solving for t gives: + timestep = ( math.sqrt( v_0 * v_0 + 2 * a * remaining_in_block) - v_0 ) / a + else + timestep = remaining_in_block / v_0 + end + + -- Truncate timestep to remaining time delta + if timestep > dtime then + timestep = dtime + end + + -- Truncate timestep to prevent v_1 from being larger that speed_max + if (v_0 < SPEED_MAX) and ( v_0 + a * timestep > SPEED_MAX) then + timestep = ( SPEED_MAX - v_0 ) / a + end + + -- Prevent infinite loops + if timestep <= 0 then return 0 end + + -- Calculate v_1 taking SPEED_MAX into account + local v_1 = v_0 + a * timestep + if v_1 > SPEED_MAX then + v_1 = SPEED_MAX + elseif v_1 < 0.025 then + v_1 = 0 + end + + -- Calculate x_1 + local x_1 = x_0 + (timestep * v_0 + 0.5 * a * timestep * timestep) / vector.length(staticdata.dir) + + -- Update position and velocity of the minecart + staticdata.velocity = v_1 + staticdata.distance = x_1 + + if DEBUG and ( v_0 > 0 or a ~= 0 ) then + mcl_debug( "- cart #"..tostring(staticdata.uuid).. + ": a="..tostring(a).. + ",v_0="..tostring(v_0).. + ",v_1="..tostring(v_1).. + ",x_0="..tostring(x_0).. + ",x_1="..tostring(x_1).. + ",timestep="..tostring(timestep).. + ",dir="..tostring(staticdata.dir).. + ",connected_at="..tostring(staticdata.connected_at).. + ",distance="..tostring(staticdata.distance) + ) + end + + -- Entity movement + local pos = staticdata.connected_at + + -- Handle movement to next block, account for loss of precision in calculations + if x_1 >= 0.99 then + staticdata.distance = 0 + + -- Anchor at the next node + local old_pos = pos + pos = pos + staticdata.dir + staticdata.connected_at = pos + + -- Get the next direction + local next_dir,_ = mcl_minecarts.get_rail_direction(pos, staticdata.dir, nil, nil, staticdata.railtype) + if DEBUG and next_dir ~= staticdata.dir then + mcl_debug( "Changing direction from "..tostring(staticdata.dir).." to "..tostring(next_dir)) + end + + -- Handle cart collisions + handle_cart_collision(staticdata, pos, next_dir) + + -- Leave the old node + handle_cart_leave(staticdata, old_pos, next_dir ) + + -- Enter the new node + handle_cart_enter(staticdata, pos, next_dir) + + -- Handle end of track + if next_dir == staticdata.dir * -1 and next_dir.y == 0 then + if DEBUG then mcl_debug("Stopping cart at end of track at "..tostring(pos)) end + staticdata.velocity = 0 + end + + -- Update cart direction + staticdata.dir = next_dir + elseif stops_in_block and v_1 < (FRICTION/5) and a <= 0 and staticdata.dir.y > 0 then + -- Handle direction flip due to gravity + if DEBUG then mcl_debug("Gravity flipped direction") end + + -- Velocity should be zero at this point + staticdata.velocity = 0 + + reverse_direction(staticdata) + + -- Intermediate movement + pos = staticdata.connected_at + staticdata.dir * staticdata.distance + else + -- Intermediate movement + pos = pos + staticdata.dir * staticdata.distance + end + + -- Debug reporting + if DEBUG and ( v_0 > 0 or v_1 > 0 ) then + mcl_debug( " cart #"..tostring(staticdata.uuid).. + ": a="..tostring(a).. + ",v_0="..tostring(v_0).. + ",v_1="..tostring(v_1).. + ",x_0="..tostring(x_0).. + ",x_1="..tostring(x_1).. + ",timestep="..tostring(timestep).. + ",dir="..tostring(staticdata.dir).. + ",pos="..tostring(pos).. + ",connected_at="..tostring(staticdata.connected_at).. + ",distance="..tostring(staticdata.distance) + ) + end + + -- Report the amount of time processed + return dtime - timestep +end + +function submod.do_movement( staticdata, dtime ) + assert(staticdata) + + -- Break long movements at block boundaries to make it + -- it impossible to jump across gaps due to server lag + -- causing large timesteps + while dtime > 0 do + local new_dtime = do_movement_step(staticdata, dtime) + try_detach_minecart(staticdata) + + update_train(staticdata) + + -- Handle node watches here in steps to prevent server lag from changing behavior + handle_cart_node_watches(staticdata, dtime - new_dtime) + + dtime = new_dtime + end +end + +local _half_pi = math.pi * 0.5 +function submod.do_detached_movement(self, dtime) + local staticdata = self._staticdata + + -- Make sure the object is still valid before trying to move it + local velocity = self.object:get_velocity() + if not self.object or not velocity then return end + + -- Apply physics + if env_physics then + env_physics.apply_entity_environmental_physics(self) + else + -- Simple physics + local friction = velocity or vector.zero() + friction.y = 0 + + local accel = vector.new(0,-9.81,0) -- gravity + + -- Don't apply friction in the air + local pos_rounded = vector.round(self.object:get_pos()) + if minetest.get_node(vector.offset(pos_rounded,0,-1,0)).name ~= "air" then + accel = vector.add(accel, vector.multiply(friction,-OFF_RAIL_FRICTION)) + end + + self.object:set_acceleration(accel) + end + + -- Shake the cart (also resets pitch) + local rot = self.object:get_rotation() + local shake_amount = 0.05 * vector.length(velocity) + rot.x = (math.random() - 0.5) * shake_amount + rot.z = (math.random() - 0.5) * shake_amount + self.object:set_rotation(rot) + + local away = vector_away_from_players(self, staticdata) + if away then + local v = self.object:get_velocity() + self.object:set_velocity((v - away)*0.65) + + -- Boost the minecart vertically a bit to get over the edge of rails and things like carpets + local boost = vector.offset(vector.multiply(vector.normalize(away), 0.1), 0, 0.07, 0) -- 1/16th + 0.0075 + local pos = self.object:get_pos() + if pos.y - math.floor(pos.y) < boost.y then + self.object:set_pos(vector.add(pos,boost)) + end + end + + -- Try to reconnect to rail + local pos = self.object:get_pos() + local yaw = self.object:get_yaw() + local yaw_dir = minetest.yaw_to_dir(yaw) + local test_positions = { + pos, + vector.offset(vector.add(pos, vector.multiply(yaw_dir, 0.5)),0,-0.55,0), + vector.offset(vector.add(pos, vector.multiply(yaw_dir,-0.5)),0,-0.55,0), + } + + for i=1,#test_positions do + local test_pos = test_positions[i] + local pos_r = vector.round(test_pos) + local node = minetest.get_node(pos_r) + if minetest.get_item_group(node.name, "rail") ~= 0 then + staticdata.connected_at = pos_r + staticdata.railtype = node.name + + local freebody_velocity = self.object:get_velocity() + staticdata.dir = mod:get_rail_direction(pos_r, mod.snap_direction(freebody_velocity)) + + -- Use vector projection to only keep the velocity in the new direction of movement on the rail + -- https://en.wikipedia.org/wiki/Vector_projection + staticdata.velocity = vector.dot(staticdata.dir,freebody_velocity) + --print("Reattached velocity="..tostring(staticdata.velocity)..", freebody_velocity="..tostring(freebody_velocity)) + + -- Clear freebody movement + self.object:set_velocity(vector.zero()) + self.object:set_acceleration(vector.zero()) + return + end + end + + -- Reset pitch if still not attached + local rot = self.object:get_rotation() + rot.x = 0 + self.object:set_rotation(rot) +end + +--return do_movement, do_detatched_movement +return submod + diff --git a/mods/ENTITIES/mcl_minecarts/rails.lua b/mods/ENTITIES/mcl_minecarts/rails.lua index 17c854e68..2356c7b5f 100644 --- a/mods/ENTITIES/mcl_minecarts/rails.lua +++ b/mods/ENTITIES/mcl_minecarts/rails.lua @@ -1,53 +1,406 @@ -local S = minetest.get_translator(minetest.get_current_modname()) +local modname = minetest.get_current_modname() +local modpath = minetest.get_modpath(modname) +local mod = mcl_minecarts +local S = minetest.get_translator(modname) +mod.RAIL_GROUPS = { + STANDARD = 1, + CURVES = 2, +} --- Template rail function -local function register_rail(itemstring, tiles, def_extras, creative) - local groups = {handy=1,pickaxey=1, attached_node=1,rail=1,connect_to_raillike=minetest.raillike_group("rail"),dig_by_water=0,destroy_by_lava_flow=0, transport=1} - if creative == false then - groups.not_in_creative_inventory = 1 - end - local ndef = { - drawtype = "raillike", - tiles = tiles, - is_ground_content = false, - inventory_image = tiles[1], - wield_image = tiles[1], - paramtype = "light", - walkable = false, - selection_box = { - type = "fixed", - fixed = {-1/2, -1/2, -1/2, 1/2, -1/2+1/16, 1/2}, - }, - stack_max = 64, - groups = groups, - sounds = mcl_sounds.node_sound_metal_defaults(), - _mcl_blast_resistance = 0.7, - _mcl_hardness = 0.7, - after_destruct = function(pos) - -- Scan for minecarts in this pos and force them to execute their "floating" check. - -- Normally, this will make them drop. - local objs = minetest.get_objects_inside_radius(pos, 1) - for o=1, #objs do - local le = objs[o]:get_luaentity() - if le then - -- All entities in this mod are minecarts, so this works - if string.sub(le.name, 1, 14) == "mcl_minecarts:" then - le._last_float_check = mcl_minecarts.check_float_time - end - end - end - end, - } - if def_extras then - for k,v in pairs(def_extras) do - ndef[k] = v +-- Inport functions and constants from elsewhere +local table_merge = mcl_util.table_merge +local check_connection_rules = mod.check_connection_rules +local update_rail_connections = mod.update_rail_connections +local minetest_fourdir_to_dir = minetest.fourdir_to_dir +local minetest_dir_to_fourdir = minetest.dir_to_fourdir +local vector_offset = vector.offset +local vector_equals = vector.equals +local north = mod.north +local south = mod.south +local east = mod.east +local west = mod.west + +--- Rail direction Handlers +local function rail_dir_straight(pos, dir, node) + dir = vector.new(dir) + dir.y = 0 + + if node.param2 == 0 or node.param2 == 2 then + if vector_equals(dir, north) then + return north + else + return south + end + else + if vector_equals(dir,east) then + return east + else + return west end end +end +local function rail_dir_sloped(pos, dir, node) + local uphill = minetest_fourdir_to_dir(node.param2) + local downhill = minetest_fourdir_to_dir((node.param2+2)%4) + local up_uphill = vector_offset(uphill,0,1,0) + + if vector_equals(dir, uphill) or vector_equals(dir, up_uphill) then + return up_uphill + else + return downhill + end +end +-- Fourdir to cardinal direction +-- 0 = north +-- 1 = east +-- 2 = south +-- 3 = west + +-- This takes a table `dirs` that has one element for each cardinal direction +-- and which specifies the direction for a cart to continue in when entering +-- a rail node in the direction of the cardinal. This function takes node +-- rotations into account. +local function rail_dir_from_table(pos, dir, node, dirs) + dir = vector.new(dir) + dir.y = 0 + local dir_fourdir = (minetest_dir_to_fourdir(dir) - node.param2 + 4) % 4 + local new_fourdir = (dirs[dir_fourdir] + node.param2) % 4 + return minetest_fourdir_to_dir(new_fourdir) +end + +local CURVE_RAIL_DIRS = { [0] = 1, 1, 2, 2, } +local function rail_dir_curve(pos, dir, node) + return rail_dir_from_table(pos, dir, node, CURVE_RAIL_DIRS) +end +local function rail_dir_tee_off(pos, dir, node) + return rail_dir_from_table(pos, dir, node, CURVE_RAIL_DIRS) +end + +local TEE_RAIL_ON_DIRS = { [0] = 0, 1, 1, 0 } +local function rail_dir_tee_on(pos, dir, node) + return rail_dir_from_table(pos, dir, node, TEE_RAIL_ON_DIRS) +end + +local function rail_dir_cross(pos, dir, node) + dir = vector.new(dir) + dir.y = 0 + + -- Always continue in the same direction. No direction changes allowed + return dir +end + +-- Setup shared text +local railuse = S( + "Place them on the ground to build your railway, the rails will automatically connect to each other and will".. + " turn into curves, T-junctions, crossings and slopes as needed." +) +mod.text = mod.text or {} +mod.text.railuse = railuse +local BASE_DEF = { + drawtype = "mesh", + mesh = "flat_track.obj", + paramtype = "light", + paramtype2 = "4dir", + stack_max = 64, + sounds = mcl_sounds.node_sound_metal_defaults(), + is_ground_content = true, + paramtype = "light", + use_texture_alpha = "clip", + collision_box = { + type = "fixed", + fixed = { -8/16, -8/16, -8/16, 8/16, -7/16, 8/15 } + }, + selection_box = { + type = "fixed", + fixed = {-1/2, -1/2, -1/2, 1/2, -1/2+1/16, 1/2}, + }, + groups = { + handy=1, pickaxey=1, + attached_node=1, + rail=1, + connect_to_raillike=minetest.raillike_group("rail"), + dig_by_water=0,destroy_by_lava_flow=0, + transport=1 + }, + _tt_help = S("Track for minecarts"), + _doc_items_usagehelp = railuse, + _doc_items_longdesc = S("Rails can be used to build transport tracks for minecarts. Normal rails slightly slow down minecarts due to friction."), + on_place = function(itemstack, placer, pointed_thing) + local node = minetest.get_node(pointed_thing.under) + local node_name = node.name + + -- Don't allow placing rail above rail + if minetest.get_item_group(node_name,"rail") ~= 0 then + return itemstack + end + + -- Handle right-clicking nodes with right-click handlers + if placer and not placer:get_player_control().sneak then + local node_def = minetest.registered_nodes[node_name] or {} + local on_rightclick = node_def and node_def.on_rightclick + if on_rightclick then + return on_rightclick(pointed_thing.under, node, placer, itemstack, pointed_thing) or itemstack + end + end + + -- Place the rail + return minetest.item_place_node(itemstack, placer, pointed_thing) + end, + after_place_node = function(pos, placer, itemstack, pointed_thing) + update_rail_connections(pos) + end, + _mcl_minecarts = { + get_next_dir = rail_dir_straight, + }, + _mcl_blast_resistance = 0.7, + _mcl_hardness = 0.7, +} + +local SLOPED_RAIL_DEF = table.copy(BASE_DEF) +table_merge(SLOPED_RAIL_DEF,{ + drawtype = "mesh", + mesh = "sloped_track.obj", + groups = { + rail_slope = 1, + not_in_creative_inventory = 1, + }, + collision_box = { + type = "fixed", + fixed = { + { -0.5, -0.5, -0.5, 0.5, 0.0, 0.5 }, + { -0.5, 0.0, 0.0, 0.5, 0.5, 0.5 } + } + }, + selection_box = { + type = "fixed", + fixed = { + { -0.5, -0.5, -0.5, 0.5, 0.0, 0.5 }, + { -0.5, 0.0, 0.0, 0.5, 0.5, 0.5 } + } + }, + _mcl_minecarts = { + get_next_dir = rail_dir_sloped, + }, +}) + +function mod.register_rail(itemstring, ndef) + assert(ndef.tiles) + assert(ndef.description) + + -- Extract out the craft recipe + local craft = ndef.craft + ndef.craft = nil + + -- Add sensible defaults + if not ndef.inventory_image then ndef.inventory_image = ndef.tiles[1] end + if not ndef.wield_image then ndef.wield_image = ndef.tiles[1] end + + --print("registering rail "..itemstring.." with definition: "..dump(ndef)) + + -- Make registrations + minetest.register_node(itemstring, ndef) + if craft then minetest.register_craft(craft) end +end + +local function make_mesecons(base_name, suffix, base_mesecons) + if not base_mesecons then + if suffix == "_tee_off" or suffix == "_tee_on" then + base_mesecons = {} + else + return + end + end + + local mesecons = table.copy(base_mesecons) + + if suffix == "_tee_off" then + mesecons.effector = base_mesecons.effector and table.copy(base_mesecons.effector) or {} + + local old_action_on = base_mesecons.effector and base_mesecons.effector.action_on + mesecons.effector.action_on = function(pos, node) + if old_action_on then old_action_on(pos, node) end + + node.name = base_name.."_tee_on" + minetest.set_node(pos, node) + end + mesecons.effector.rules = mesecons.effector.rules or mesecon.rules.alldirs + elseif suffix == "_tee_on" then + mesecons.effector = base_mesecons.effector and table.copy(base_mesecons.effector) or {} + + local old_action_off = base_mesecons.effector and base_mesecons.effector.action_off + mesecons.effector.action_off = function(pos, node) + if old_action_off then old_action_off(pos, node) end + + node.name = base_name.."_tee_off" + minetest.set_node(pos, node) + end + mesecons.effector.rules = mesecons.effector.rules or mesecon.rules.alldirs + end + + if mesecons.conductor then + mesecons.conductor = table.copy(base_mesecons.conductor) + + if mesecons.conductor.onstate then + mesecons.conductor.onstate = base_mesecons.conductor.onstate..suffix + end + if base_mesecons.conductor.offstate then + mesecons.conductor.offstate = base_mesecons.conductor.offstate..suffix + end + end + + return mesecons +end + + +function mod.register_straight_rail(base_name, tiles, def) + def = def or {} + local base_def = table.copy(BASE_DEF) + local sloped_def = table.copy(SLOPED_RAIL_DEF) + local add = { + tiles = { tiles[1] }, + drop = base_name, + groups = { + rail = mod.RAIL_GROUPS.STANDARD, + }, + _mcl_minecarts = { + base_name = base_name, + can_slope = true, + }, + } + table_merge(base_def, add); table_merge(sloped_def, add) + table_merge(base_def, def); table_merge(sloped_def, def) + + -- Register the base node + mod.register_rail(base_name, base_def) + base_def.craft = nil; sloped_def.craft = nil + table_merge(base_def,{ + _mcl_minecarts = { + railtype = "straight", + suffix = "", + }, + }) + + -- Sloped variant + mod.register_rail_sloped(base_name.."_sloped", table_merge(table.copy(sloped_def),{ + _mcl_minecarts = { + get_next_dir = rail_dir_sloped, + suffix = "_sloped", + }, + mesecons = make_mesecons(base_name, "_sloped", def.mesecons), + tiles = { tiles[1] }, + _mcl_minecarts = { + railtype = "sloped", + }, + })) +end + +function mod.register_curves_rail(base_name, tiles, def) + def = def or {} + local base_def = table.copy(BASE_DEF) + local sloped_def = table.copy(SLOPED_RAIL_DEF) + local add = { + _mcl_minecarts = { base_name = base_name }, + groups = { + rail = mod.RAIL_GROUPS.CURVES + }, + drop = base_name, + } + table_merge(base_def, add); table_merge(sloped_def, add) + table_merge(base_def, def); table_merge(sloped_def, def) + + -- Register the base node + mod.register_rail(base_name, table_merge(table.copy(base_def),{ + tiles = { tiles[1] }, + _mcl_minecarts = { + get_next_dir = rail_dir_straight, + railtype = "straight", + can_slope = true, + suffix = "", + }, + })) + + -- Update for other variants + base_def.craft = nil + table_merge(base_def, { + groups = { + not_in_creative_inventory = 1 + } + }) + + -- Corner variants + mod.register_rail(base_name.."_corner", table_merge(table.copy(base_def),{ + tiles = { tiles[2] }, + _mcl_minecarts = { + get_next_dir = rail_dir_curve, + railtype = "corner", + suffix = "_corner", + }, + mesecons = make_mesecons(base_name, "_corner", def.mesecons), + })) + + -- Tee variants + mod.register_rail(base_name.."_tee_off", table_merge(table.copy(base_def),{ + tiles = { tiles[3] }, + mesecons = make_mesecons(base_name, "_tee_off", def.mesecons), + _mcl_minecarts = { + get_next_dir = rail_dir_tee_off, + railtype = "tee", + suffix = "_tee_off", + }, + })) + mod.register_rail(base_name.."_tee_on", table_merge(table.copy(base_def),{ + tiles = { tiles[4] }, + _mcl_minecarts = { + get_next_dir = rail_dir_tee_on, + railtype = "tee", + suffix = "_tee_on", + }, + mesecons = make_mesecons(base_name, "_tee_on", def.mesecons), + })) + + -- Sloped variant + mod.register_rail_sloped(base_name.."_sloped", table_merge(table.copy(sloped_def),{ + description = S("Sloped Rail"), -- Temporary name to make debugging easier + _mcl_minecarts = { + get_next_dir = rail_dir_sloped, + railtype = "tee", + suffix = "_sloped", + }, + mesecons = make_mesecons(base_name, "_sloped", def.mesecons), + tiles = { tiles[1] }, + })) + + -- Cross variant + mod.register_rail(base_name.."_cross", table_merge(table.copy(base_def),{ + tiles = { tiles[5] }, + _mcl_minecarts = { + get_next_dir = rail_dir_cross, + railtype = "cross", + suffix = "_cross", + }, + mesecons = make_mesecons(base_name, "_cross", def.mesecons), + })) +end + +function mod.register_rail_sloped(itemstring, def) + assert(def.tiles) + + -- Build the node definition + local ndef = table.copy(SLOPED_RAIL_DEF) + table_merge(ndef, def) + + -- Add sensible defaults + if not ndef.inventory_image then ndef.inventory_image = ndef.tiles[1] end + if not ndef.wield_image then ndef.wield_image = ndef.tiles[1] end + + --print("registering sloped rail "..itemstring.." with definition: "..dump(ndef)) + + -- Make registrations minetest.register_node(itemstring, ndef) end -- Redstone rules -local rail_rules_long = +mod.rail_rules_long = {{x=-1, y= 0, z= 0, spread=true}, {x= 1, y= 0, z= 0, spread=true}, {x= 0, y=-1, z= 0, spread=true}, @@ -64,202 +417,73 @@ local rail_rules_long = {x= 0, y= 1, z=-1}, {x= 0, y=-1, z=-1}} -local rail_rules_short = mesecon.rules.pplate - -local railuse = S("Place them on the ground to build your railway, the rails will automatically connect to each other and will turn into curves, T-junctions, crossings and slopes as needed.") - --- Normal rail -register_rail("mcl_minecarts:rail", - {"default_rail.png", "default_rail_curved.png", "default_rail_t_junction.png", "default_rail_crossing.png"}, - { - description = S("Rail"), - _tt_help = S("Track for minecarts"), - _doc_items_longdesc = S("Rails can be used to build transport tracks for minecarts. Normal rails slightly slow down minecarts due to friction."), - _doc_items_usagehelp = railuse, - } -) - --- Powered rail (off = brake mode) -register_rail("mcl_minecarts:golden_rail", - {"mcl_minecarts_rail_golden.png", "mcl_minecarts_rail_golden_curved.png", "mcl_minecarts_rail_golden_t_junction.png", "mcl_minecarts_rail_golden_crossing.png"}, - { - description = S("Powered Rail"), - _tt_help = S("Track for minecarts").."\n"..S("Speed up when powered, slow down when not powered"), - _doc_items_longdesc = S("Rails can be used to build transport tracks for minecarts. Powered rails are able to accelerate and brake minecarts."), - _doc_items_usagehelp = railuse .. "\n" .. S("Without redstone power, the rail will brake minecarts. To make this rail accelerate minecarts, power it with redstone power."), - _rail_acceleration = -3, - mesecons = { - conductor = { - state = mesecon.state.off, - offstate = "mcl_minecarts:golden_rail", - onstate = "mcl_minecarts:golden_rail_on", - rules = rail_rules_long, - }, - }, - } -) - --- Powered rail (on = acceleration mode) -register_rail("mcl_minecarts:golden_rail_on", - {"mcl_minecarts_rail_golden_powered.png", "mcl_minecarts_rail_golden_curved_powered.png", "mcl_minecarts_rail_golden_t_junction_powered.png", "mcl_minecarts_rail_golden_crossing_powered.png"}, - { - _doc_items_create_entry = false, - _rail_acceleration = 4, - mesecons = { - conductor = { - state = mesecon.state.on, - offstate = "mcl_minecarts:golden_rail", - onstate = "mcl_minecarts:golden_rail_on", - rules = rail_rules_long, - }, - effector = { - action_on = function(pos, node) - local dir = mcl_minecarts:get_start_direction(pos) - if not dir then return end - local objs = minetest.get_objects_inside_radius(pos, 1) - for _, o in pairs(objs) do - local l = o:get_luaentity() - local v = o:get_velocity() - if l and string.sub(l.name, 1, 14) == "mcl_minecarts:" - and v and vector.equals(v, vector.zero()) - then - mcl_minecarts:set_velocity(l, dir) - end - end -end, - }, - }, - drop = "mcl_minecarts:golden_rail", - }, - false -) - --- Activator rail (off) -register_rail("mcl_minecarts:activator_rail", - {"mcl_minecarts_rail_activator.png", "mcl_minecarts_rail_activator_curved.png", "mcl_minecarts_rail_activator_t_junction.png", "mcl_minecarts_rail_activator_crossing.png"}, - { - description = S("Activator Rail"), - _tt_help = S("Track for minecarts").."\n"..S("Activates minecarts when powered"), - _doc_items_longdesc = S("Rails can be used to build transport tracks for minecarts. Activator rails are used to activate special minecarts."), - _doc_items_usagehelp = railuse .. "\n" .. S("To make this rail activate minecarts, power it with redstone power and send a minecart over this piece of rail."), - mesecons = { - conductor = { - state = mesecon.state.off, - offstate = "mcl_minecarts:activator_rail", - onstate = "mcl_minecarts:activator_rail_on", - rules = rail_rules_long, - - }, - }, - } -) - --- Activator rail (on) -register_rail("mcl_minecarts:activator_rail_on", - {"mcl_minecarts_rail_activator_powered.png", "mcl_minecarts_rail_activator_curved_powered.png", "mcl_minecarts_rail_activator_t_junction_powered.png", "mcl_minecarts_rail_activator_crossing_powered.png"}, - { - _doc_items_create_entry = false, - mesecons = { - conductor = { - state = mesecon.state.on, - offstate = "mcl_minecarts:activator_rail", - onstate = "mcl_minecarts:activator_rail_on", - rules = rail_rules_long, - }, - effector = { - -- Activate minecarts - action_on = function(pos, node) - local pos2 = { x = pos.x, y =pos.y + 1, z = pos.z } - local objs = minetest.get_objects_inside_radius(pos2, 1) - for _, o in pairs(objs) do - local l = o:get_luaentity() - if l and string.sub(l.name, 1, 14) == "mcl_minecarts:" and l.on_activate_by_rail then - l:on_activate_by_rail() - end - end - end, - }, - - }, - drop = "mcl_minecarts:activator_rail", - }, - false -) - --- Detector rail (off) -register_rail("mcl_minecarts:detector_rail", - {"mcl_minecarts_rail_detector.png", "mcl_minecarts_rail_detector_curved.png", "mcl_minecarts_rail_detector_t_junction.png", "mcl_minecarts_rail_detector_crossing.png"}, - { - description = S("Detector Rail"), - _tt_help = S("Track for minecarts").."\n"..S("Emits redstone power when a minecart is detected"), - _doc_items_longdesc = S("Rails can be used to build transport tracks for minecarts. A detector rail is able to detect a minecart above it and powers redstone mechanisms."), - _doc_items_usagehelp = railuse .. "\n" .. S("To detect a minecart and provide redstone power, connect it to redstone trails or redstone mechanisms and send any minecart over the rail."), - mesecons = { - receptor = { - state = mesecon.state.off, - rules = rail_rules_short, - }, - }, - } -) - --- Detector rail (on) -register_rail("mcl_minecarts:detector_rail_on", - {"mcl_minecarts_rail_detector_powered.png", "mcl_minecarts_rail_detector_curved_powered.png", "mcl_minecarts_rail_detector_t_junction_powered.png", "mcl_minecarts_rail_detector_crossing_powered.png"}, - { - _doc_items_create_entry = false, - mesecons = { - receptor = { - state = mesecon.state.on, - rules = rail_rules_short, - }, - }, - drop = "mcl_minecarts:detector_rail", - }, - false -) - - --- Crafting -minetest.register_craft({ - output = "mcl_minecarts:rail 16", - recipe = { - {"mcl_core:iron_ingot", "", "mcl_core:iron_ingot"}, - {"mcl_core:iron_ingot", "mcl_core:stick", "mcl_core:iron_ingot"}, - {"mcl_core:iron_ingot", "", "mcl_core:iron_ingot"}, - } -}) - -minetest.register_craft({ - output = "mcl_minecarts:golden_rail 6", - recipe = { - {"mcl_core:gold_ingot", "", "mcl_core:gold_ingot"}, - {"mcl_core:gold_ingot", "mcl_core:stick", "mcl_core:gold_ingot"}, - {"mcl_core:gold_ingot", "mesecons:redstone", "mcl_core:gold_ingot"}, - } -}) - -minetest.register_craft({ - output = "mcl_minecarts:activator_rail 6", - recipe = { - {"mcl_core:iron_ingot", "mcl_core:stick", "mcl_core:iron_ingot"}, - {"mcl_core:iron_ingot", "mesecons_torch:mesecon_torch_on", "mcl_core:iron_ingot"}, - {"mcl_core:iron_ingot", "mcl_core:stick", "mcl_core:iron_ingot"}, - } -}) - -minetest.register_craft({ - output = "mcl_minecarts:detector_rail 6", - recipe = { - {"mcl_core:iron_ingot", "", "mcl_core:iron_ingot"}, - {"mcl_core:iron_ingot", "mesecons_pressureplates:pressure_plate_stone_off", "mcl_core:iron_ingot"}, - {"mcl_core:iron_ingot", "mesecons:redstone", "mcl_core:iron_ingot"}, - } -}) - +dofile(modpath.."/rails/normal.lua") +dofile(modpath.."/rails/activator.lua") +dofile(modpath.."/rails/detector.lua") +dofile(modpath.."/rails/powered.lua") -- Aliases if minetest.get_modpath("doc") then doc.add_entry_alias("nodes", "mcl_minecarts:golden_rail", "nodes", "mcl_minecarts:golden_rail_on") end +local CURVY_RAILS_MAP = { + ["mcl_minecarts:rail"] = "mcl_minecarts:rail_v2", + ["mcl_minecarts:golden_rail"] = "mcl_minecarts:golden_rail_v2", + ["mcl_minecarts:golden_rail_on"] = "mcl_minecarts:golden_rail_v2_on", + ["mcl_minecarts:activator_rail"] = "mcl_minecarts:activator_rail_v2", + ["mcl_minecarts:activator_rail_on"] = "mcl_minecarts:activator_rail_v2_on", + ["mcl_minecarts:detector_rail"] = "mcl_minecarts:detector_rail_v2", + ["mcl_minecarts:detector_rail_on"] = "mcl_minecarts:detector_rail_v2_on", +} +local function convert_legacy_curvy_rails(pos, node) + node.name = CURVY_RAILS_MAP[node.name] + if node.name then + minetest.swap_node(pos, node) + mod.update_rail_connections(pos, { legacy = true, ignore_neighbor_connections = true }) + end +end +for old,new in pairs(CURVY_RAILS_MAP) do + local new_def = minetest.registered_nodes[new] + minetest.register_node(old, { + drawtype = "raillike", + inventory_image = new_def.inventory_image, + groups = { rail = 1, legacy = 1 }, + tiles = { new_def.tiles[1], new_def.tiles[1], new_def.tiles[1], new_def.tiles[1] }, + _vl_legacy_convert_node = convert_legacy_curvy_rails + }) + vl_legacy.register_item_conversion(old, new) +end +local STRAIGHT_RAILS_MAP ={ +} +local function convert_legacy_straight_rail(pos, node) + node.name = STRAIGHT_RAILS_MAP[node.name] + if node.name then + local connections = mod.get_rail_connections(pos, { legacy = true, ignore_neighbor_connections = true }) + if not mod.HORIZONTAL_STANDARD_RULES[connections] then + -- Drop an immortal object at this location + local item_entity = minetest.add_item(pos, ItemStack(node.name)) + if item_entity then + item_entity:get_luaentity()._immortal = true + end + + -- This is a configuration that doesn't exist in the new rail + -- Replace with a standard rail + node.name = "mcl_minecarts:rail_v2" + end + minetest.swap_node(pos, node) + mod.update_rail_connections(pos, { legacy = true, ignore_neighbor_connections = true }) + end +end +for old,new in pairs(STRAIGHT_RAILS_MAP) do + local new_def = minetest.registered_nodes[new] + minetest.register_node(old, { + drawtype = "raillike", + inventory_image = new_def.inventory_image, + groups = { rail = 1, legacy = 1 }, + tiles = { new_def.tiles[1], new_def.tiles[1], new_def.tiles[1], new_def.tiles[1] }, + _vl_legacy_convert_node = convert_legacy_straight_rail, + }) + vl_legacy.register_item_conversion(old, new) +end + diff --git a/mods/ENTITIES/mcl_minecarts/rails/activator.lua b/mods/ENTITIES/mcl_minecarts/rails/activator.lua new file mode 100644 index 000000000..605ba4a2f --- /dev/null +++ b/mods/ENTITIES/mcl_minecarts/rails/activator.lua @@ -0,0 +1,78 @@ +local modname = minetest.get_current_modname() +local modpath = minetest.get_modpath(modname) +local mod = mcl_minecarts +local S = minetest.get_translator(modname) + +-- Activator rail (off) +mod.register_curves_rail("mcl_minecarts:activator_rail_v2", { + "mcl_minecarts_rail_activator.png", + "mcl_minecarts_rail_activator_curved.png", + "mcl_minecarts_rail_activator_t_junction.png", + "mcl_minecarts_rail_activator_t_junction.png", + "mcl_minecarts_rail_activator_crossing.png" +},{ + description = S("Activator Rail"), + _tt_help = S("Track for minecarts").."\n"..S("Activates minecarts when powered"), + _doc_items_longdesc = S("Rails can be used to build transport tracks for minecarts. Activator rails are used to activate special minecarts."), + _doc_items_usagehelp = mod.text.railuse .. "\n" .. S("To make this rail activate minecarts, power it with redstone power and send a minecart over this piece of rail."), + mesecons = { + conductor = { + state = mesecon.state.off, + offstate = "mcl_minecarts:activator_rail_v2", + onstate = "mcl_minecarts:activator_rail_v2_on", + rules = mod.rail_rules_long, + }, + }, + craft = { + output = "mcl_minecarts:activator_rail_v2 6", + recipe = { + {"mcl_core:iron_ingot", "mcl_core:stick", "mcl_core:iron_ingot"}, + {"mcl_core:iron_ingot", "mesecons_torch:mesecon_torch_on", "mcl_core:iron_ingot"}, + {"mcl_core:iron_ingot", "mcl_core:stick", "mcl_core:iron_ingot"}, + } + }, +}) + +-- Activator rail (on) +local function activator_rail_action_on(pos, node) + local pos2 = { x = pos.x, y =pos.y + 1, z = pos.z } + local objs = minetest.get_objects_inside_radius(pos2, 1) + for _, o in pairs(objs) do + local l = o:get_luaentity() + if l and string.sub(l.name, 1, 14) == "mcl_minecarts:" and l.on_activate_by_rail then + l:on_activate_by_rail() + end + end +end +mod.register_curves_rail("mcl_minecarts:activator_rail_v2_on", { + "mcl_minecarts_rail_activator_powered.png", + "mcl_minecarts_rail_activator_curved_powered.png", + "mcl_minecarts_rail_activator_t_junction_powered.png", + "mcl_minecarts_rail_activator_t_junction_powered.png", + "mcl_minecarts_rail_activator_crossing_powered.png" +},{ + description = S("Activator Rail"), + _doc_items_create_entry = false, + groups = { + not_in_creative_inventory = 1, + }, + mesecons = { + conductor = { + state = mesecon.state.on, + offstate = "mcl_minecarts:activator_rail_v2", + onstate = "mcl_minecarts:activator_rail_v2_on", + rules = mod.rail_rules_long, + }, + effector = { + -- Activate minecarts + action_on = activator_rail_action_on, + }, + }, + _mcl_minecarts_on_enter = function(pos, cart) + if cart.on_activate_by_rail then + cart:on_activate_by_rail() + end + end, + drop = "mcl_minecarts:activator_rail_v2", +}) + diff --git a/mods/ENTITIES/mcl_minecarts/rails/detector.lua b/mods/ENTITIES/mcl_minecarts/rails/detector.lua new file mode 100644 index 000000000..443cecf07 --- /dev/null +++ b/mods/ENTITIES/mcl_minecarts/rails/detector.lua @@ -0,0 +1,71 @@ +local modname = minetest.get_current_modname() +local modpath = minetest.get_modpath(modname) +local mod = mcl_minecarts +local S = minetest.get_translator(modname) + +local rail_rules_short = mesecon.rules.pplate + +-- Detector rail (off) +mod.register_curves_rail("mcl_minecarts:detector_rail_v2",{ + "mcl_minecarts_rail_detector.png", + "mcl_minecarts_rail_detector_curved.png", + "mcl_minecarts_rail_detector_t_junction.png", + "mcl_minecarts_rail_detector_t_junction.png", + "mcl_minecarts_rail_detector_crossing.png" +},{ + description = S("Detector Rail"), + _tt_help = S("Track for minecarts").."\n"..S("Emits redstone power when a minecart is detected"), + _doc_items_longdesc = S("Rails can be used to build transport tracks for minecarts. A detector rail is able to detect a minecart above it and powers redstone mechanisms."), + _doc_items_usagehelp = mod.text.railuse .. "\n" .. S("To detect a minecart and provide redstone power, connect it to redstone trails or redstone mechanisms and send any minecart over the rail."), + mesecons = { + receptor = { + state = mesecon.state.off, + rules = rail_rules_short, + }, + }, + _mcl_minecarts_on_enter = function(pos, cart) + local node = minetest.get_node(pos) + local node_def = minetest.registered_nodes[node.name] + node.name = "mcl_minecarts:detector_rail_v2_on"..node_def._mcl_minecarts.suffix + minetest.set_node( pos, node ) + mesecon.receptor_on(pos) + end, + craft = { + output = "mcl_minecarts:detector_rail_v2 6", + recipe = { + {"mcl_core:iron_ingot", "", "mcl_core:iron_ingot"}, + {"mcl_core:iron_ingot", "mesecons_pressureplates:pressure_plate_stone_off", "mcl_core:iron_ingot"}, + {"mcl_core:iron_ingot", "mesecons:redstone", "mcl_core:iron_ingot"}, + } + } +}) + +-- Detector rail (on) +mod.register_curves_rail("mcl_minecarts:detector_rail_v2_on",{ + "mcl_minecarts_rail_detector_powered.png", + "mcl_minecarts_rail_detector_curved_powered.png", + "mcl_minecarts_rail_detector_t_junction_powered.png", + "mcl_minecarts_rail_detector_t_junction_powered.png", + "mcl_minecarts_rail_detector_crossing_powered.png" +},{ + description = S("Detector Rail"), + groups = { + not_in_creative_inventory = 1, + }, + _doc_items_create_entry = false, + mesecons = { + receptor = { + state = mesecon.state.on, + rules = rail_rules_short, + }, + }, + _mcl_minecarts_on_leave = function(pos, cart) + local node = minetest.get_node(pos) + local node_def = minetest.registered_nodes[node.name] + node.name = "mcl_minecarts:detector_rail_v2"..node_def._mcl_minecarts.suffix + minetest.set_node( pos, node ) + mesecon.receptor_off(pos) + end, + drop = "mcl_minecarts:detector_rail_v2", +}) + diff --git a/mods/ENTITIES/mcl_minecarts/rails/normal.lua b/mods/ENTITIES/mcl_minecarts/rails/normal.lua new file mode 100644 index 000000000..58ff60758 --- /dev/null +++ b/mods/ENTITIES/mcl_minecarts/rails/normal.lua @@ -0,0 +1,27 @@ +local modname = minetest.get_current_modname() +local modpath = minetest.get_modpath(modname) +local mod = mcl_minecarts +local S = minetest.get_translator(modname) + +-- Normal rail +mod.register_curves_rail("mcl_minecarts:rail_v2", { + "default_rail.png", + "default_rail_curved.png", + "default_rail_t_junction.png", + "default_rail_t_junction_on.png", + "default_rail_crossing.png" +},{ + description = S("Rail"), + _tt_help = S("Track for minecarts"), + _doc_items_longdesc = S("Rails can be used to build transport tracks for minecarts. Normal rails slightly slow down minecarts due to friction."), + _doc_items_usagehelp = mod.text.railuse, + craft = { + output = "mcl_minecarts:rail_v2 16", + recipe = { + {"mcl_core:iron_ingot", "", "mcl_core:iron_ingot"}, + {"mcl_core:iron_ingot", "mcl_core:stick", "mcl_core:iron_ingot"}, + {"mcl_core:iron_ingot", "", "mcl_core:iron_ingot"}, + } + }, +}) + diff --git a/mods/ENTITIES/mcl_minecarts/rails/powered.lua b/mods/ENTITIES/mcl_minecarts/rails/powered.lua new file mode 100644 index 000000000..e76aff2c7 --- /dev/null +++ b/mods/ENTITIES/mcl_minecarts/rails/powered.lua @@ -0,0 +1,84 @@ +local modname = minetest.get_current_modname() +local modpath = minetest.get_modpath(modname) +local mod = mcl_minecarts +local S = minetest.get_translator(modname) + +-- Powered rail (off = brake mode) +mod.register_curves_rail("mcl_minecarts:golden_rail_v2",{ + "mcl_minecarts_rail_golden.png", + "mcl_minecarts_rail_golden_curved.png", + "mcl_minecarts_rail_golden_t_junction.png", + "mcl_minecarts_rail_golden_t_junction.png", + "mcl_minecarts_rail_golden_crossing.png" +},{ + description = S("Powered Rail"), + _tt_help = S("Track for minecarts").."\n"..S("Speed up when powered, slow down when not powered"), + _doc_items_longdesc = S("Rails can be used to build transport tracks for minecarts. Powered rails are able to accelerate and brake minecarts."), + _doc_items_usagehelp = mod.text.railuse .. "\n" .. S("Without redstone power, the rail will brake minecarts. To make this rail accelerate".. + " minecarts, power it with redstone power."), + _doc_items_create_entry = false, + _rail_acceleration = -3, + _max_acceleration_velocity = 8, + mesecons = { + conductor = { + state = mesecon.state.off, + offstate = "mcl_minecarts:golden_rail_v2", + onstate = "mcl_minecarts:golden_rail_v2_on", + rules = mod.rail_rules_long, + }, + }, + drop = "mcl_minecarts:golden_rail_v2", + craft = { + output = "mcl_minecarts:golden_rail_v2 6", + recipe = { + {"mcl_core:gold_ingot", "", "mcl_core:gold_ingot"}, + {"mcl_core:gold_ingot", "mcl_core:stick", "mcl_core:gold_ingot"}, + {"mcl_core:gold_ingot", "mesecons:redstone", "mcl_core:gold_ingot"}, + } + } +}) + +-- Powered rail (on = acceleration mode) +mod.register_curves_rail("mcl_minecarts:golden_rail_v2_on",{ + "mcl_minecarts_rail_golden_powered.png", + "mcl_minecarts_rail_golden_curved_powered.png", + "mcl_minecarts_rail_golden_t_junction_powered.png", + "mcl_minecarts_rail_golden_t_junction_powered.png", + "mcl_minecarts_rail_golden_crossing_powered.png", +},{ + description = S("Powered Rail"), + _doc_items_create_entry = false, + _rail_acceleration = function(pos, staticdata) + if staticdata.velocity ~= 0 then + return 4 + end + + local dir = mod.get_rail_direction(pos, staticdata.dir, nil, nil, staticdata.railtype) + local node_a = minetest.get_node(vector.add(pos, dir)) + local node_b = minetest.get_node(vector.add(pos, -dir)) + local has_adjacent_solid = minetest.get_item_group(node_a.name, "solid") ~= 0 or + minetest.get_item_group(node_b.name, "solid") ~= 0 or + minetest.get_item_group(node_a.name, "stair") ~= 0 or + minetest.get_item_group(node_b.name, "stair") ~= 0 + + if has_adjacent_solid then + return 4 + else + return 0 + end + end, + _max_acceleration_velocity = 8, + groups = { + not_in_creative_inventory = 1, + }, + mesecons = { + conductor = { + state = mesecon.state.on, + offstate = "mcl_minecarts:golden_rail_v2", + onstate = "mcl_minecarts:golden_rail_v2_on", + rules = mod.rail_rules_long, + }, + }, + drop = "mcl_minecarts:golden_rail_v2", +}) + diff --git a/mods/ENTITIES/mcl_minecarts/storage.lua b/mods/ENTITIES/mcl_minecarts/storage.lua new file mode 100644 index 000000000..7f242b7f8 --- /dev/null +++ b/mods/ENTITIES/mcl_minecarts/storage.lua @@ -0,0 +1,92 @@ +local storage = minetest.get_mod_storage() +local mod = mcl_minecarts + +-- Imports +local CART_BLOCK_SIZE = mod.CART_BLOCK_SIZE +assert(CART_BLOCK_SIZE) + +local cart_data = {} +local cart_data_fail_cache = {} +local cart_ids = storage:get_keys() + +local function get_cart_data(uuid) + if cart_data[uuid] then return cart_data[uuid] end + if cart_data_fail_cache[uuid] then return nil end + + local data = minetest.deserialize(storage:get_string("cart-"..uuid)) + if not data then + cart_data_fail_cache[uuid] = true + return nil + else + -- Repair broken data + if not data.distance then data.distance = 0 end + if data.distance == 0/0 then data.distance = 0 end + if data.distance == -0/0 then data.distance = 0 end + data.dir = vector.new(data.dir) + data.connected_at = vector.new(data.connected_at) + end + + cart_data[uuid] = data + return data +end +mod.get_cart_data = get_cart_data + +-- Preload all cart data into memory +for _,id in pairs(cart_ids) do + local uuid = string.sub(id,6) + get_cart_data(uuid) +end + +local function save_cart_data(uuid) + if not cart_data[uuid] then return end + storage:set_string("cart-"..uuid,minetest.serialize(cart_data[uuid])) +end +mod.save_cart_data = save_cart_data + +function mod.update_cart_data(data) + local uuid = data.uuid + cart_data[uuid] = data + cart_data_fail_cache[uuid] = nil + save_cart_data(uuid) +end +function mod.destroy_cart_data(uuid) + storage:set_string("cart-"..uuid,"") + cart_data[uuid] = nil + cart_data_fail_cache[uuid] = true +end + +function mod.carts() + return pairs(cart_data) +end + +function mod.find_carts_by_block_map(block_map) + local cart_list = {} + for _,data in pairs(cart_data) do + if data and data.connected_at then + local pos = mod.get_cart_position(data) + local block = vector.floor(vector.divide(pos,CART_BLOCK_SIZE)) + if block_map[vector.to_string(block)] then + cart_list[#cart_list + 1] = data + end + end + end + return cart_list +end + +function mod.add_blocks_to_map(block_map, min_pos, max_pos) + local min = vector.floor(vector.divide(min_pos, CART_BLOCK_SIZE)) + local max = vector.floor(vector.divide(max_pos, CART_BLOCK_SIZE)) + vector.new(1,1,1) + for z = min.z,max.z do + for y = min.y,max.y do + for x = min.x,max.x do + block_map[ vector.to_string(vector.new(x,y,z)) ] = true + end + end + end +end + +minetest.register_on_shutdown(function() + for uuid,_ in pairs(cart_data) do + save_cart_data(uuid) + end +end) diff --git a/mods/ENTITIES/mcl_minecarts/train.lua b/mods/ENTITIES/mcl_minecarts/train.lua new file mode 100644 index 000000000..c21cbb0eb --- /dev/null +++ b/mods/ENTITIES/mcl_minecarts/train.lua @@ -0,0 +1,147 @@ +local modname = minetest.get_current_modname() +local modpath = minetest.get_modpath(modname) +local mod = mcl_minecarts + +-- Imports +local get_cart_data = mod.get_cart_data +local save_cart_data = mod.save_cart_data +local MAX_TRAIN_LENGTH = mod.MAX_TRAIN_LENGTH + +-- Follow .behind to the back end of a train +local function find_back(start) + assert(start) + + while start.behind do + local nxt = get_cart_data(start.behind) + if not nxt then return start end + start = nxt + end + return start +end + +-- Iterate across all the cars in a train +function mod.train_cars(staticdata) + assert(staticdata) + + local back = find_back(staticdata) + local limit = MAX_TRAIN_LENGTH + return function() + if not back or limit <= 0 then return end + limit = limit - 1 + + local ret = back + if back.ahead then + back = get_cart_data(back.ahead) + else + back = nil + end + return ret + end +end +local train_cars = mod.train_cars + +function mod.train_length(cart) + local count = 0 + for cart in train_cars(cart) do + count = count + 1 + end + return count +end + +function mod.is_in_same_train(anchor, other) + for cart in train_cars(anchor) do + if cart.uuid == other.uuid then return true end + end + return false +end + +function mod.distance_between_cars(car1, car2) + if not car1.connected_at then return nil end + if not car2.connected_at then return nil end + + if not car1.dir then car1.dir = vector.zero() end + if not car2.dir then car2.dir = vector.zero() end + + local pos1 = vector.add(car1.connected_at, vector.multiply(car1.dir, car1.distance)) + local pos2 = vector.add(car2.connected_at, vector.multiply(car2.dir, car2.distance)) + + return vector.distance(pos1, pos2) +end +local distance_between_cars = mod.distance_between_cars + +local function break_train_at(cart) + if cart.ahead then + local ahead = get_cart_data(cart.ahead) + if ahead then + ahead.behind = nil + cart.ahead = nil + save_cart_data(ahead.uuid) + end + end + if cart.behind then + local behind = get_cart_data(cart.behind) + if behind then + behind.ahead = nil + cart.behind = nil + save_cart_data(behind.uuid) + end + end + save_cart_data(cart.uuid) +end +mod.break_train_at = break_train_at + +function mod.update_train(staticdata) + -- Only update from the back + if staticdata.behind or not staticdata.ahead then return end + + -- Do no special processing if the cart is not part of a train + if not staticdata.ahead and not staticdata.behind then return end + + -- Calculate the maximum velocity of all train cars + local velocity = staticdata.velocity + + -- Set the entire train to the average velocity + local behind = nil + for c in train_cars(staticdata) do + local e = 0 + local separation + local cart_velocity = velocity + if not c.connected_at then + break_train_at(c) + elseif behind then + separation = distance_between_cars(behind, c) + local e = 0 + if not separation then + break_train_at(c) + elseif separation > 1.6 then + cart_velocity = velocity * 0.9 + elseif separation > 2.5 then + break_train_at(c) + elseif separation < 1.15 then + cart_velocity = velocity * 1.1 + end + end + --[[ + print(tostring(c.behind).."->"..c.uuid.."->"..tostring(c.ahead).."("..tostring(separation)..") setting cart #".. + c.uuid.." velocity from "..tostring(c.velocity).." to "..tostring(cart_velocity)) + --]] + c.velocity = cart_velocity + + behind = c + end +end + +function mod.link_cart_ahead(staticdata, ca_staticdata) + minetest.log("action","Linking cart #"..staticdata.uuid.." to cart #"..ca_staticdata.uuid) + + staticdata.ahead = ca_staticdata.uuid + ca_staticdata.behind = staticdata.uuid +end + +function mod.reverse_train(cart) + for c in train_cars(cart) do + mod.reverse_cart_direction(c) + c.behind,c.ahead = c.ahead,c.behind + end +end + diff --git a/mods/HUD/mcl_inventory/creative.lua b/mods/HUD/mcl_inventory/creative.lua index 3deade807..1f25932b2 100644 --- a/mods/HUD/mcl_inventory/creative.lua +++ b/mods/HUD/mcl_inventory/creative.lua @@ -408,7 +408,7 @@ local tab_icon = { blocks = "mcl_core:brick_block", deco = "mcl_flowers:peony", redstone = "mesecons:redstone", - rail = "mcl_minecarts:golden_rail", + rail = "mcl_minecarts:golden_rail_v2", misc = "mcl_buckets:bucket_lava", nix = "mcl_compass:compass", food = "mcl_core:apple", diff --git a/mods/ITEMS/REDSTONE/mcl_droppers/init.lua b/mods/ITEMS/REDSTONE/mcl_droppers/init.lua index 625549a1d..13a5b879d 100644 --- a/mods/ITEMS/REDSTONE/mcl_droppers/init.lua +++ b/mods/ITEMS/REDSTONE/mcl_droppers/init.lua @@ -132,9 +132,9 @@ local dropperdef = { -- If they are containers - double down as hopper mcl_util.hopper_push(pos, droppos) end - if dropnodedef.walkable then - return - end + if dropnodedef.walkable then return end + + -- Build a list of items in the dropper local stacks = {} for i = 1, inv:get_size("main") do local stack = inv:get_stack("main", i) @@ -142,17 +142,36 @@ local dropperdef = { table.insert(stacks, { stack = stack, stackpos = i }) end end + + -- Pick an item to drop + local dropitem = nil + local stack = nil + local r = nil if #stacks >= 1 then - local r = math.random(1, #stacks) - local stack = stacks[r].stack - local dropitem = ItemStack(stack) + r = math.random(1, #stacks) + stack = stacks[r].stack + dropitem = ItemStack(stack) local stackdef = core.registered_items[stack:get_name()] if not stackdef then return end - dropitem:set_count(1) - local stack_id = stacks[r].stackpos + end + if not dropitem then return end + + -- Flag for if the item was dropped. If true the item will be removed from + -- the inventory after dropping + local item_dropped = false + + -- Check if the drop item has a custom handler + local itemdef = minetest.registered_craftitems[dropitem:get_name()] + if itemdef._mcl_dropper_on_drop then + item_dropped = itemdef._mcl_dropper_on_drop(dropitem, droppos) + end + + -- If a custom handler wasn't successful then drop the item as an entity + if not item_dropped then + -- Drop as entity local pos_variation = 100 droppos = vector.offset(droppos, math.random(-pos_variation, pos_variation) / 1000, @@ -164,6 +183,12 @@ local dropperdef = { local speed = 3 item_entity:set_velocity(vector.multiply(drop_vel, speed)) stack:take_item() + item_dropped = trie + end + + -- Remove dropped items from inventory + if item_dropped then + local stack_id = stacks[r].stackpos inv:set_stack("main", stack_id, stack) end end, diff --git a/mods/ITEMS/REDSTONE/mesecons_commandblock/api.lua b/mods/ITEMS/REDSTONE/mesecons_commandblock/api.lua new file mode 100644 index 000000000..7edcc9083 --- /dev/null +++ b/mods/ITEMS/REDSTONE/mesecons_commandblock/api.lua @@ -0,0 +1,227 @@ +mesecon = mesecon or {} +local mod = {} +mesecon.commandblock = mod + +local S = minetest.get_translator(minetest.get_current_modname()) +local F = minetest.formspec_escape +local color_red = mcl_colors.RED + +mod.initialize = function(meta) + meta:set_string("commands", "") + meta:set_string("commander", "") +end + +mod.place = function(meta, placer) + if not placer then return end + + meta:set_string("commander", placer:get_player_name()) +end + +mod.resolve_commands = function(commands, meta, pos) + local players = minetest.get_connected_players() + local commander = meta:get_string("commander") + + -- A non-printable character used while replacing “@@”. + local SUBSTITUTE_CHARACTER = "\26" -- ASCII SUB + + -- No players online: remove all commands containing + -- problematic placeholders. + if #players == 0 then + commands = commands:gsub("[^\r\n]+", function (line) + line = line:gsub("@@", SUBSTITUTE_CHARACTER) + if line:find("@n") then return "" end + if line:find("@p") then return "" end + if line:find("@f") then return "" end + if line:find("@r") then return "" end + line = line:gsub("@c", commander) + line = line:gsub(SUBSTITUTE_CHARACTER, "@") + return line + end) + return commands + end + + local nearest, farthest = nil, nil + local min_distance, max_distance = math.huge, -1 + for index, player in pairs(players) do + local distance = vector.distance(pos, player:get_pos()) + if distance < min_distance then + min_distance = distance + nearest = player:get_player_name() + end + if distance > max_distance then + max_distance = distance + farthest = player:get_player_name() + end + end + local random = players[math.random(#players)]:get_player_name() + commands = commands:gsub("@@", SUBSTITUTE_CHARACTER) + commands = commands:gsub("@p", nearest) + commands = commands:gsub("@n", nearest) + commands = commands:gsub("@f", farthest) + commands = commands:gsub("@r", random) + commands = commands:gsub("@c", commander) + commands = commands:gsub(SUBSTITUTE_CHARACTER, "@") + return commands +end +local resolve_commands = mod.resolve_commands + +mod.check_commands = function(commands, player_name) + for _, command in pairs(commands:split("\n")) do + local pos = command:find(" ") + local cmd = command + if pos then + cmd = command:sub(1, pos - 1) + end + local cmddef = minetest.chatcommands[cmd] + if not cmddef then + -- Invalid chat command + local msg = S("Error: The command “@1” does not exist; your command block has not been changed. Use the “help” chat command for a list of available commands.", cmd) + if string.sub(cmd, 1, 1) == "/" then + msg = S("Error: The command “@1” does not exist; your command block has not been changed. Use the “help” chat command for a list of available commands. Hint: Try to remove the leading slash.", cmd) + end + return false, minetest.colorize(color_red, msg) + end + if player_name then + local player_privs = minetest.get_player_privs(player_name) + + for cmd_priv, _ in pairs(cmddef.privs) do + if player_privs[cmd_priv] ~= true then + local msg = S("Error: You have insufficient privileges to use the command “@1” (missing privilege: @2)! The command block has not been changed.", cmd, cmd_priv) + return false, minetest.colorize(color_red, msg) + end + end + end + end + return true +end +local check_commands = mod.check_commands + +mod.action_on = function(meta, pos) + local commander = meta:get_string("commander") + local commands = resolve_commands(meta:get_string("commands"), meta, pos) + for _, command in pairs(commands:split("\n")) do + local cpos = command:find(" ") + local cmd, param = command, "" + if cpos then + cmd = command:sub(1, cpos - 1) + param = command:sub(cpos + 1) + end + local cmddef = minetest.chatcommands[cmd] + if not cmddef then + -- Invalid chat command + return + end + -- Execute command in the name of commander + cmddef.func(commander, param) + end +end + +local formspec_metas = {} + +mod.handle_rightclick = function(meta, player) + local can_edit = true + -- Only allow write access in Creative Mode + if not minetest.is_creative_enabled(player:get_player_name()) then + can_edit = false + end + local pname = player:get_player_name() + if minetest.is_protected(pos, pname) then + can_edit = false + end + local privs = minetest.get_player_privs(pname) + if not privs.maphack then + can_edit = false + end + + local commands = meta:get_string("commands") + if not commands then + commands = "" + end + local commander = meta:get_string("commander") + local commanderstr + if commander == "" or commander == nil then + commanderstr = S("Error: No commander! Block must be replaced.") + else + commanderstr = S("Commander: @1", commander) + end + local textarea_name, submit, textarea + -- If editing is not allowed, only allow read-only access. + -- Player can still view the contents of the command block. + if can_edit then + textarea_name = "commands" + submit = "button_exit[3.3,4.4;2,1;submit;"..F(S("Submit")).."]" + else + textarea_name = "" + submit = "" + end + if not can_edit and commands == "" then + textarea = "label[0.5,0.5;"..F(S("No commands.")).."]" + else + textarea = "textarea[0.5,0.5;8.5,4;"..textarea_name..";"..F(S("Commands:"))..";"..F(commands).."]" + end + local formspec = "size[9,5;]" .. + textarea .. + submit .. + "image_button[8,4.4;1,1;doc_button_icon_lores.png;doc;]" .. + "tooltip[doc;"..F(S("Help")).."]" .. + "label[0,4;"..F(commanderstr).."]" + + -- Store the metadata object for later use + local fs_id = #formspec_metas + 1 + formspec_metas[fs_id] = meta + print("using fs_id="..tostring(fs_id)..",meta="..tostring(meta)..",formspec_metas[fs_id]="..tostring(formspec_metas[fs_id])) + + minetest.show_formspec(pname, "commandblock_"..tostring(fs_id), formspec) +end + +minetest.register_on_player_receive_fields(function(player, formname, fields) + if string.sub(formname, 1, 13) == "commandblock_" then + -- Show documentation + if fields.doc and minetest.get_modpath("doc") then + doc.show_entry(player:get_player_name(), "nodes", "mesecons_commandblock:commandblock_off", true) + return + end + + -- Validate form fields + if (not fields.submit and not fields.key_enter) or (not fields.commands) then + return + end + + -- Check privileges + local privs = minetest.get_player_privs(player:get_player_name()) + if not privs.maphack then + minetest.chat_send_player(player:get_player_name(), S("Access denied. You need the “maphack” privilege to edit command blocks.")) + return + end + + -- Check game mode + if not minetest.is_creative_enabled(player:get_player_name()) then + minetest.chat_send_player(player:get_player_name(), + S("Editing the command block has failed! You can only change the command block in Creative Mode!") + ) + return + end + + -- Retrieve the metadata object this formspec data belongs to + local index, _, fs_id = string.find(formname, "commandblock_(-?%d+)") + fs_id = tonumber(fs_id) + if not index or not fs_id or not formspec_metas[fs_id] then + print("index="..tostring(index)..", fs_id="..tostring(fs_id).."formspec_metas[fs_id]="..tostring(formspec_metas[fs_id])) + minetest.chat_send_player(player:get_player_name(), S("Editing the command block has failed! The command block is gone.")) + return + end + local meta = formspec_metas[fs_id] + formspec_metas[fs_id] = nil + + -- Verify the command + local check, error_message = check_commands(fields.commands, player:get_player_name()) + if check == false then + -- Command block rejected + minetest.chat_send_player(player:get_player_name(), error_message) + return + end + + -- Update the command in the metadata + meta:set_string("commands", fields.commands) + end +end) diff --git a/mods/ITEMS/REDSTONE/mesecons_commandblock/init.lua b/mods/ITEMS/REDSTONE/mesecons_commandblock/init.lua index 3902c3c18..3c3aa741a 100644 --- a/mods/ITEMS/REDSTONE/mesecons_commandblock/init.lua +++ b/mods/ITEMS/REDSTONE/mesecons_commandblock/init.lua @@ -1,104 +1,27 @@ +local modname = minetest.get_current_modname() +local modpath = minetest.get_modpath(modname) local S = minetest.get_translator(minetest.get_current_modname()) -local F = minetest.formspec_escape -local tonumber = tonumber - -local color_red = mcl_colors.RED +-- Initialize API +dofile(modpath.."/api.lua") +local api = mesecon.commandblock local command_blocks_activated = minetest.settings:get_bool("mcl_enable_commandblocks", true) local msg_not_activated = S("Command blocks are not enabled on this server") local function construct(pos) local meta = minetest.get_meta(pos) - - meta:set_string("commands", "") - meta:set_string("commander", "") + api.initialize(meta) end local function after_place(pos, placer) - if placer then - local meta = minetest.get_meta(pos) - meta:set_string("commander", placer:get_player_name()) - end + local meta = minetest.get_meta(pos) + api.place(meta, placer) end local function resolve_commands(commands, pos) - local players = minetest.get_connected_players() - local meta = minetest.get_meta(pos) - local commander = meta:get_string("commander") - - -- A non-printable character used while replacing “@@”. - local SUBSTITUTE_CHARACTER = "\26" -- ASCII SUB - - -- No players online: remove all commands containing - -- problematic placeholders. - if #players == 0 then - commands = commands:gsub("[^\r\n]+", function (line) - line = line:gsub("@@", SUBSTITUTE_CHARACTER) - if line:find("@n") then return "" end - if line:find("@p") then return "" end - if line:find("@f") then return "" end - if line:find("@r") then return "" end - line = line:gsub("@c", commander) - line = line:gsub(SUBSTITUTE_CHARACTER, "@") - return line - end) - return commands - end - - local nearest, farthest = nil, nil - local min_distance, max_distance = math.huge, -1 - for index, player in pairs(players) do - local distance = vector.distance(pos, player:get_pos()) - if distance < min_distance then - min_distance = distance - nearest = player:get_player_name() - end - if distance > max_distance then - max_distance = distance - farthest = player:get_player_name() - end - end - local random = players[math.random(#players)]:get_player_name() - commands = commands:gsub("@@", SUBSTITUTE_CHARACTER) - commands = commands:gsub("@p", nearest) - commands = commands:gsub("@n", nearest) - commands = commands:gsub("@f", farthest) - commands = commands:gsub("@r", random) - commands = commands:gsub("@c", commander) - commands = commands:gsub(SUBSTITUTE_CHARACTER, "@") - return commands -end - -local function check_commands(commands, player_name) - for _, command in pairs(commands:split("\n")) do - local pos = command:find(" ") - local cmd = command - if pos then - cmd = command:sub(1, pos - 1) - end - local cmddef = minetest.chatcommands[cmd] - if not cmddef then - -- Invalid chat command - local msg = S("Error: The command “@1” does not exist; your command block has not been changed. Use the “help” chat command for a list of available commands.", cmd) - if string.sub(cmd, 1, 1) == "/" then - msg = S("Error: The command “@1” does not exist; your command block has not been changed. Use the “help” chat command for a list of available commands. Hint: Try to remove the leading slash.", cmd) - end - return false, minetest.colorize(color_red, msg) - end - if player_name then - local player_privs = minetest.get_player_privs(player_name) - - for cmd_priv, _ in pairs(cmddef.privs) do - if player_privs[cmd_priv] ~= true then - local msg = S("Error: You have insufficient privileges to use the command “@1” (missing privilege: @2)! The command block has not been changed.", cmd, cmd_priv) - return false, minetest.colorize(color_red, msg) - end - end - end - end - return true + return api.resolve_commands(commands, meta) end local function commandblock_action_on(pos, node) @@ -107,7 +30,6 @@ local function commandblock_action_on(pos, node) end local meta = minetest.get_meta(pos) - local commander = meta:get_string("commander") if not command_blocks_activated then --minetest.chat_send_player(commander, msg_not_activated) @@ -115,22 +37,7 @@ local function commandblock_action_on(pos, node) end minetest.swap_node(pos, {name = "mesecons_commandblock:commandblock_on"}) - local commands = resolve_commands(meta:get_string("commands"), pos) - for _, command in pairs(commands:split("\n")) do - local cpos = command:find(" ") - local cmd, param = command, "" - if cpos then - cmd = command:sub(1, cpos - 1) - param = command:sub(cpos + 1) - end - local cmddef = minetest.chatcommands[cmd] - if not cmddef then - -- Invalid chat command - return - end - -- Execute command in the name of commander - cmddef.func(commander, param) - end + api.action_on(meta, pos) end local function commandblock_action_off(pos, node) @@ -144,54 +51,10 @@ local function on_rightclick(pos, node, player, itemstack, pointed_thing) minetest.chat_send_player(player:get_player_name(), msg_not_activated) return end - local can_edit = true - -- Only allow write access in Creative Mode - if not minetest.is_creative_enabled(player:get_player_name()) then - can_edit = false - end - local pname = player:get_player_name() - if minetest.is_protected(pos, pname) then - can_edit = false - end - local privs = minetest.get_player_privs(pname) - if not privs.maphack then - can_edit = false - end local meta = minetest.get_meta(pos) - local commands = meta:get_string("commands") - if not commands then - commands = "" - end - local commander = meta:get_string("commander") - local commanderstr - if commander == "" or commander == nil then - commanderstr = S("Error: No commander! Block must be replaced.") - else - commanderstr = S("Commander: @1", commander) - end - local textarea_name, submit, textarea - -- If editing is not allowed, only allow read-only access. - -- Player can still view the contents of the command block. - if can_edit then - textarea_name = "commands" - submit = "button_exit[3.3,4.4;2,1;submit;"..F(S("Submit")).."]" - else - textarea_name = "" - submit = "" - end - if not can_edit and commands == "" then - textarea = "label[0.5,0.5;"..F(S("No commands.")).."]" - else - textarea = "textarea[0.5,0.5;8.5,4;"..textarea_name..";"..F(S("Commands:"))..";"..F(commands).."]" - end - local formspec = "size[9,5;]" .. - textarea .. - submit .. - "image_button[8,4.4;1,1;doc_button_icon_lores.png;doc;]" .. - "tooltip[doc;"..F(S("Help")).."]" .. - "label[0,4;"..F(commanderstr).."]" - minetest.show_formspec(pname, "commandblock_"..pos.x.."_"..pos.y.."_"..pos.z, formspec) + api.handle_rightclick(meta, player) + end local function on_place(itemstack, placer, pointed_thing) @@ -200,12 +63,10 @@ local function on_place(itemstack, placer, pointed_thing) end -- Use pointed node's on_rightclick function first, if present - local new_stack = mcl_util.call_on_rightclick(itemstack, placer, pointed_thing) - if new_stack then - return new_stack - end - - --local node = minetest.get_node(pointed_thing.under) + local new_stack = mcl_util.call_on_rightclick(itemstack, placer, pointed_thing) + if new_stack then + return new_stack + end local privs = minetest.get_player_privs(placer:get_player_name()) if not privs.maphack then @@ -280,44 +141,6 @@ minetest.register_node("mesecons_commandblock:commandblock_on", { _mcl_hardness = -1, }) -minetest.register_on_player_receive_fields(function(player, formname, fields) - if string.sub(formname, 1, 13) == "commandblock_" then - if fields.doc and minetest.get_modpath("doc") then - doc.show_entry(player:get_player_name(), "nodes", "mesecons_commandblock:commandblock_off", true) - return - end - if (not fields.submit and not fields.key_enter) or (not fields.commands) then - return - end - - local privs = minetest.get_player_privs(player:get_player_name()) - if not privs.maphack then - minetest.chat_send_player(player:get_player_name(), S("Access denied. You need the “maphack” privilege to edit command blocks.")) - return - end - - local index, _, x, y, z = string.find(formname, "commandblock_(-?%d+)_(-?%d+)_(-?%d+)") - if index and x and y and z then - local pos = {x = tonumber(x), y = tonumber(y), z = tonumber(z)} - local meta = minetest.get_meta(pos) - if not minetest.is_creative_enabled(player:get_player_name()) then - minetest.chat_send_player(player:get_player_name(), S("Editing the command block has failed! You can only change the command block in Creative Mode!")) - return - end - local check, error_message = check_commands(fields.commands, player:get_player_name()) - if check == false then - -- Command block rejected - minetest.chat_send_player(player:get_player_name(), error_message) - return - else - meta:set_string("commands", fields.commands) - end - else - minetest.chat_send_player(player:get_player_name(), S("Editing the command block has failed! The command block is gone.")) - end - end -end) - -- Add entry alias for the Help if minetest.get_modpath("doc") then doc.add_entry_alias("nodes", "mesecons_commandblock:commandblock_off", "nodes", "mesecons_commandblock:commandblock_on") diff --git a/mods/ITEMS/mcl_core/nodes_cactuscane.lua b/mods/ITEMS/mcl_core/nodes_cactuscane.lua index 805385124..5e37a92c4 100644 --- a/mods/ITEMS/mcl_core/nodes_cactuscane.lua +++ b/mods/ITEMS/mcl_core/nodes_cactuscane.lua @@ -48,6 +48,11 @@ minetest.register_node("mcl_core:cactus", { end), _mcl_blast_resistance = 0.4, _mcl_hardness = 0.4, + _mcl_minecarts_on_enter_side = function(pos, _, _, _, cart_data) + if mcl_minecarts then + mcl_minecarts.kill_cart(cart_data) + end + end, }) minetest.register_node("mcl_core:reeds", { @@ -135,4 +140,4 @@ minetest.register_node("mcl_core:reeds", { end, _mcl_blast_resistance = 0, _mcl_hardness = 0, -}) \ No newline at end of file +}) diff --git a/mods/ITEMS/mcl_hoppers/init.lua b/mods/ITEMS/mcl_hoppers/init.lua index ec7f7ab8f..b120f22b5 100644 --- a/mods/ITEMS/mcl_hoppers/init.lua +++ b/mods/ITEMS/mcl_hoppers/init.lua @@ -9,6 +9,8 @@ local function mcl_log(message) end end +mcl_hoppers = {} + --[[ BEGIN OF NODE DEFINITIONS ]] local mcl_hoppers_formspec = table.concat({ @@ -52,7 +54,7 @@ local function straight_hopper_act(pos, node, active_object_count, active_count_ mcl_util.hopper_push(pos, dst_pos) local src_pos = vector.offset(pos, 0, 1, 0) - mcl_util.hopper_pull(pos, src_pos) + mcl_util.hopper_pull_to_inventory(minetest.get_meta(pos):get_inventory(), "main", src_pos, pos) end local function bent_hopper_act(pos, node, active_object_count, active_object_count_wider) @@ -91,9 +93,102 @@ local function bent_hopper_act(pos, node, active_object_count, active_object_cou end local src_pos = vector.offset(pos, 0, 1, 0) - mcl_util.hopper_pull(pos, src_pos) + mcl_util.hopper_pull_to_inventory(inv, "main", src_pos, pos) end +--[[ + Returns true if an item was pushed to the minecart +]] +local function hopper_push_to_mc(mc_ent, dest_pos, inv_size) + if not mcl_util.metadata_last_act(minetest.get_meta(dest_pos), "hopper_push_timer", 1) then return false end + + local dest_inv = mcl_entity_invs.load_inv(mc_ent, inv_size) + if not dest_inv then + mcl_log("No inv") + return false + end + + local meta = minetest.get_meta(dest_pos) + local inv = meta:get_inventory() + if not inv then + mcl_log("No dest inv") + return + end + + mcl_log("inv. size: " .. mc_ent._inv_size) + for i = 1, mc_ent._inv_size, 1 do + local stack = inv:get_stack("main", i) + + mcl_log("i: " .. tostring(i)) + mcl_log("Name: [" .. tostring(stack:get_name()) .. "]") + mcl_log("Count: " .. tostring(stack:get_count())) + mcl_log("stack max: " .. tostring(stack:get_stack_max())) + + if not stack:get_name() or stack:get_name() ~= "" then + if dest_inv:room_for_item("main", stack:peek_item()) then + mcl_log("Room so unload") + dest_inv:add_item("main", stack:take_item()) + inv:set_stack("main", i, stack) + + -- Take one item and stop until next time + return + else + mcl_log("no Room") + end + + else + mcl_log("nothing there") + end + end +end +--[[ + Returns true if an item was pulled from the minecart +]] +local function hopper_pull_from_mc(mc_ent, dest_pos, inv_size) + if not mcl_util.metadata_last_act(minetest.get_meta(dest_pos), "hopper_pull_timer", 1) then return false end + + local inv = mcl_entity_invs.load_inv(mc_ent, inv_size) + if not inv then + mcl_log("No inv") + return false + end + + local dest_meta = minetest.get_meta(dest_pos) + local dest_inv = dest_meta:get_inventory() + if not dest_inv then + mcl_log("No dest inv") + return false + end + + mcl_log("inv. size: " .. mc_ent._inv_size) + for i = 1, mc_ent._inv_size, 1 do + local stack = inv:get_stack("main", i) + + mcl_log("i: " .. tostring(i)) + mcl_log("Name: [" .. tostring(stack:get_name()) .. "]") + mcl_log("Count: " .. tostring(stack:get_count())) + mcl_log("stack max: " .. tostring(stack:get_stack_max())) + + if not stack:get_name() or stack:get_name() ~= "" then + if dest_inv:room_for_item("main", stack:peek_item()) then + mcl_log("Room so unload") + dest_inv:add_item("main", stack:take_item()) + inv:set_stack("main", i, stack) + + -- Take one item and stop until next time, report that we took something + return true + else + mcl_log("no Room") + end + + else + mcl_log("nothing there") + end + end +end +mcl_hoppers.pull_from_minecart = hopper_pull_from_mc + + -- Downwards hopper (base definition) ---@type node_definition @@ -199,6 +294,50 @@ local def_hopper = { minetest.log("action", player:get_player_name() .. " takes stuff from mcl_hoppers at " .. minetest.pos_to_string(pos)) end, + _mcl_minecarts_on_enter_below = function(pos, cart, next_dir) + -- Hopper is below minecart + + -- Only pull to containers + if cart and cart.groups and (cart.groups.container or 0) ~= 0 then + cart:add_node_watch(pos) + hopper_pull_from_mc(cart, pos, 5) + end + end, + _mcl_minecarts_on_enter_above = function(pos, cart, next_dir) + -- Hopper is above minecart + + -- Only push to containers + if cart and cart.groups and (cart.groups.container or 0) ~= 0 then + cart:add_node_watch(pos) + hopper_push_to_mc(cart, pos, 5) + end + end, + _mcl_minecarts_on_leave_above = function(pos, cart, next_dir) + if not cart then return end + + cart:remove_node_watch(pos) + end, + _mcl_minecarts_node_on_step = function(pos, cart, dtime, cartdata) + if not cart then + minetest.log("warning", "trying to process hopper-to-minecart movement without luaentity") + return + end + + local cart_pos = mcl_minecarts.get_cart_position(cartdata) + if not cart_pos then return false end + if vector.distance(cart_pos, pos) > 1.5 then + cart:remove_node_watch(pos) + return + end + if vector.direction(pos,cart_pos).y > 0 then + -- The cart is above us, pull from minecart + hopper_pull_from_mc(cart, pos, 5) + else + hopper_push_to_mc(cart, pos, 5) + end + + return true + end, sounds = mcl_sounds.node_sound_metal_defaults(), _mcl_blast_resistance = 4.8, @@ -404,6 +543,70 @@ local def_hopper_side = { on_rotate = on_rotate, sounds = mcl_sounds.node_sound_metal_defaults(), + _mcl_minecarts_on_enter_below = function(pos, cart, next_dir) + -- Hopper is below minecart + + -- Only push to containers + if cart and cart.groups and (cart.groups.container or 0) ~= 0 then + cart:add_node_watch(pos) + hopper_pull_from_mc(cart, pos, 5) + end + end, + _mcl_minecarts_on_leave_below = function(pos, cart, next_dir) + if not cart then return end + + cart:remove_node_watch(pos) + end, + _mcl_minecarts_on_enter_side = function(pos, cart, next_dir, rail_pos) + -- Hopper is to the side of the minecart + + if not cart then return end + + -- Only try to push to minecarts when the spout position is pointed at the rail + local face = minetest.get_node(pos).param2 + local dst_pos = {} + if face == 0 then + dst_pos = vector.offset(pos, -1, 0, 0) + elseif face == 1 then + dst_pos = vector.offset(pos, 0, 0, 1) + elseif face == 2 then + dst_pos = vector.offset(pos, 1, 0, 0) + elseif face == 3 then + dst_pos = vector.offset(pos, 0, 0, -1) + end + if dst_pos ~= rail_pos then return end + + -- Only push to containers + if cart.groups and (cart.groups.container or 0) ~= 0 then + cart:add_node_watch(pos) + end + + hopper_push_to_mc(cart, pos, 5) + end, + _mcl_minecarts_on_leave_side = function(pos, cart, next_dir) + if not cart then return end + + cart:remove_node_watch(pos) + end, + _mcl_minecarts_node_on_step = function(pos, cart, dtime, cartdata) + if not cart then return end + + local cart_pos = mcl_minecarts.get_cart_position(cartdata) + if not cart_pos then return false end + if vector.distance(cart_pos, pos) > 1.5 then + cart:remove_node_watch(pos) + return false + end + + if cart_pos.y == pos.y then + hopper_push_to_mc(cart, pos, 5) + elseif cart_pos.y > pos.y then + hopper_pull_from_mc(cart, pos, 5) + end + + return true + end, + _mcl_blast_resistance = 4.8, _mcl_hardness = 3, } @@ -435,87 +638,6 @@ minetest.register_node("mcl_hoppers:hopper_side_disabled", def_hopper_side_disab --[[ END OF NODE DEFINITIONS ]] -local function hopper_pull_from_mc(mc_ent, dest_pos, inv_size) - local inv = mcl_entity_invs.load_inv(mc_ent, inv_size) - if not inv then - mcl_log("No inv") - return false - end - - local dest_meta = minetest.get_meta(dest_pos) - local dest_inv = dest_meta:get_inventory() - if not dest_inv then - mcl_log("No dest inv") - return - end - - mcl_log("inv. size: " .. mc_ent._inv_size) - for i = 1, mc_ent._inv_size, 1 do - local stack = inv:get_stack("main", i) - - mcl_log("i: " .. tostring(i)) - mcl_log("Name: [" .. tostring(stack:get_name()) .. "]") - mcl_log("Count: " .. tostring(stack:get_count())) - mcl_log("stack max: " .. tostring(stack:get_stack_max())) - - if not stack:get_name() or stack:get_name() ~= "" then - if dest_inv:room_for_item("main", stack:peek_item()) then - mcl_log("Room so unload") - dest_inv:add_item("main", stack:take_item()) - inv:set_stack("main", i, stack) - - -- Take one item and stop until next time - return - else - mcl_log("no Room") - end - - else - mcl_log("nothing there") - end - end -end - -local function hopper_push_to_mc(mc_ent, dest_pos, inv_size) - local dest_inv = mcl_entity_invs.load_inv(mc_ent, inv_size) - if not dest_inv then - mcl_log("No inv") - return false - end - - local meta = minetest.get_meta(dest_pos) - local inv = meta:get_inventory() - if not inv then - mcl_log("No dest inv") - return - end - - mcl_log("inv. size: " .. mc_ent._inv_size) - for i = 1, mc_ent._inv_size, 1 do - local stack = inv:get_stack("main", i) - - mcl_log("i: " .. tostring(i)) - mcl_log("Name: [" .. tostring(stack:get_name()) .. "]") - mcl_log("Count: " .. tostring(stack:get_count())) - mcl_log("stack max: " .. tostring(stack:get_stack_max())) - - if not stack:get_name() or stack:get_name() ~= "" then - if dest_inv:room_for_item("main", stack:peek_item()) then - mcl_log("Room so unload") - dest_inv:add_item("main", stack:take_item()) - inv:set_stack("main", i, stack) - - -- Take one item and stop until next time - return - else - mcl_log("no Room") - end - - else - mcl_log("nothing there") - end - end -end --[[ BEGIN OF ABM DEFINITONS ]] @@ -555,7 +677,7 @@ minetest.register_abm({ and (hm_pos.z >= pos.z - DIST_FROM_MC and hm_pos.z <= pos.z + DIST_FROM_MC) then mcl_log("Minecart close enough") if entity.name == "mcl_minecarts:hopper_minecart" then - hopper_pull_from_mc(entity, pos, 5) + --hopper_pull_from_mc(entity, pos, 5) elseif entity.name == "mcl_minecarts:chest_minecart" or entity.name == "mcl_boats:chest_boat" then hopper_pull_from_mc(entity, pos, 27) end @@ -564,7 +686,7 @@ minetest.register_abm({ and (hm_pos.z >= pos.z - DIST_FROM_MC and hm_pos.z <= pos.z + DIST_FROM_MC) then mcl_log("Minecart close enough") if entity.name == "mcl_minecarts:hopper_minecart" then - hopper_push_to_mc(entity, pos, 5) + --hopper_push_to_mc(entity, pos, 5) elseif entity.name == "mcl_minecarts:chest_minecart" or entity.name == "mcl_boats:chest_boat" then hopper_push_to_mc(entity, pos, 27) end diff --git a/mods/MAPGEN/tsm_railcorridors/gameconfig.lua b/mods/MAPGEN/tsm_railcorridors/gameconfig.lua index 9f924f00b..084dacb34 100644 --- a/mods/MAPGEN/tsm_railcorridors/gameconfig.lua +++ b/mods/MAPGEN/tsm_railcorridors/gameconfig.lua @@ -3,17 +3,38 @@ -- Adapted for MineClone 2! +-- Imports +local create_minecart = mcl_minecarts.create_minecart +local get_cart_data = mcl_minecarts.get_cart_data +local save_cart_data = mcl_minecarts.save_cart_data + -- Node names (Don't use aliases!) tsm_railcorridors.nodes = { dirt = "mcl_core:dirt", chest = "mcl_chests:chest", - rail = "mcl_minecarts:rail", + rail = "mcl_minecarts:rail_v2", torch_floor = "mcl_torches:torch", torch_wall = "mcl_torches:torch_wall", cobweb = "mcl_core:cobweb", spawner = "mcl_mobspawners:spawner", } +local update_rail_connections = mcl_minecarts.update_rail_connections +local rails_to_update = {} +tsm_railcorridors.on_place_node = { + [tsm_railcorridors.nodes.rail] = function(pos, node) + rails_to_update[#rails_to_update + 1] = pos + end, +} +tsm_railcorridors.on_start = function() + rails_to_update = {} +end +tsm_railcorridors.on_finish = function() + for _,pos in pairs(rails_to_update) do + update_rail_connections(pos, {legacy = true, ignore_neighbor_connections = true}) + end +end + local mg_name = minetest.get_mapgen_setting("mg_name") if mg_name == "v6" then @@ -41,6 +62,10 @@ tsm_railcorridors.carts = { "mcl_minecarts:chest_minecart", "mcl_minecarts:chest_minecart", "mcl_minecarts:tnt_minecart" } +local has_loot = { + ["mcl_minecarts:chest_minecart"] = true, + ["mcl_minecarts:hopper_minceart"] = true, +} -- This is called after a spawner has been placed by the game. -- Use this to properly set up the metadata and stuff. @@ -50,19 +75,42 @@ function tsm_railcorridors.on_construct_spawner(pos) mcl_mobspawners.setup_spawner(pos, "mobs_mc:cave_spider", 0, 7) end - -- This is called after a cart has been placed by the game. -- Use this to properly set up entity metadata and stuff. +-- * entity_id - type of cart to create -- * pos: Position of cart --- * cart: Cart entity -function tsm_railcorridors.on_construct_cart(_, cart, pr_carts) - local l = cart:get_luaentity() - local inv = mcl_entity_invs.load_inv(l,27) - if inv then -- otherwise probably not a chest minecart - local items = tsm_railcorridors.get_treasures(pr_carts) - mcl_loot.fill_inventory(inv, "main", items, pr_carts) - mcl_entity_invs.save_inv(l) +-- * pr: pseudorandom +function tsm_railcorridors.create_cart_staticdata(entity_id, pos, pr, pr_carts) + local uuid = create_minecart(entity_id, pos, vector.new(1,0,0)) + + -- Fill the cart with loot + local cartdata = get_cart_data(uuid) + if cartdata and has_loot[entity_id] then + local items = tsm_railcorridors.get_treasures(pr) + + local size = core.registered_entities[entity_id]._inv_size + local inventory = {} + for i = 1,size do inventory[i] = "" end + cartdata.inventory = inventory + + -- Fill a fake inventory using mcl_loot + local fake_inv = { + get_size = function(self) + return size + end, + get_stack = function(self, _, i) + return ItemStack(inventory[i]) + end, + set_stack = function(self, _, i, stack) + inventory[i] = stack:to_string() + end, + } + mcl_loot.fill_inventory(fake_inv, "main", items, pr_carts) + + save_cart_data(uuid) end + + return minetest.serialize({ uuid=uuid, seq=1 }) end -- Fallback function. Returns a random treasure. This function is called for chests @@ -110,11 +158,11 @@ function tsm_railcorridors.get_treasures(pr) stacks_min = 3, stacks_max = 3, items = { - { itemstring = "mcl_minecarts:rail", weight = 20, amount_min = 4, amount_max = 8 }, + { itemstring = "mcl_minecarts:rail_v2", weight = 20, amount_min = 4, amount_max = 8 }, { itemstring = "mcl_torches:torch", weight = 15, amount_min = 1, amount_max = 16 }, - { itemstring = "mcl_minecarts:activator_rail", weight = 5, amount_min = 1, amount_max = 4 }, - { itemstring = "mcl_minecarts:detector_rail", weight = 5, amount_min = 1, amount_max = 4 }, - { itemstring = "mcl_minecarts:golden_rail", weight = 5, amount_min = 1, amount_max = 4 }, + { itemstring = "mcl_minecarts:activator_rail_v2", weight = 5, amount_min = 1, amount_max = 4 }, + { itemstring = "mcl_minecarts:detector_rail_v2", weight = 5, amount_min = 1, amount_max = 4 }, + { itemstring = "mcl_minecarts:golden_rail_v2", weight = 5, amount_min = 1, amount_max = 4 }, } }, -- non-MC loot: 50% chance to add a minecart, offered as alternative to spawning minecarts on rails. diff --git a/mods/MAPGEN/tsm_railcorridors/init.lua b/mods/MAPGEN/tsm_railcorridors/init.lua index 9895ab44c..2d9043ae7 100644 --- a/mods/MAPGEN/tsm_railcorridors/init.lua +++ b/mods/MAPGEN/tsm_railcorridors/init.lua @@ -1,7 +1,9 @@ local pairs = pairs local tonumber = tonumber -tsm_railcorridors = {} +tsm_railcorridors = { + after = {}, +} -- Load node names dofile(minetest.get_modpath(minetest.get_current_modname()).."/gameconfig.lua") @@ -169,6 +171,10 @@ local function SetNodeIfCanBuild(pos, node, check_above, can_replace_rail) (can_replace_rail and name == tsm_railcorridors.nodes.rail) ) then minetest.set_node(pos, node) + local after = tsm_railcorridors.on_place_node[node.name] + if after then + after(pos, node) + end return true else return false @@ -199,7 +205,7 @@ local function IsRailSurface(pos) local nodename = minetest.get_node(pos).name local nodename_above = minetest.get_node({x=pos.x,y=pos.y+2,z=pos.z}).name local nodedef = minetest.registered_nodes[nodename] - return nodename ~= "unknown" and nodename ~= "ignore" and nodedef and nodedef.walkable and (nodedef.node_box == nil or nodedef.node_box.type == "regular") and nodename_above ~= tsm_railcorridors.nodes.rail + return nodename ~= "unknown" and nodename ~= "ignore" and nodedef and nodedef.walkable and (nodedef.node_box == nil or nodedef.node_box.type == "regular") and nodename_above ~= tsm_railcorridors.nodes.rail and nodename ~= tsm_railcorridors.nodes.rail end -- Checks if the node is empty space which requires to be filled by a platform @@ -392,30 +398,6 @@ local function PlaceChest(pos, param2) end end --- This function checks if a cart has ACTUALLY been spawned. --- To be calld by minetest.after. --- This is a workaround thanks to the fact that minetest.add_entity is unreliable as fuck --- See: https://github.com/minetest/minetest/issues/4759 --- FIXME: Kill this horrible hack with fire as soon you can. -local RecheckCartHack = nil -if not minetest.features.random_state_restore then -- proxy for minetest > 5.9.0, this feature will not be removed -RecheckCartHack = function(params) - local pos = params[1] - local cart_id = params[2] - -- Find cart - for _, obj in ipairs(minetest.get_objects_inside_radius(pos, 1)) do - if obj ~= nil and obj:get_luaentity().name == cart_id then - -- Cart found! We can now safely call the callback func. - -- (calling it earlier has the danger of failing) - minetest.log("info", "[tsm_railcorridors] Cart spawn succeeded: "..minetest.pos_to_string(pos)) - tsm_railcorridors.on_construct_cart(pos, obj, pr_carts) - return - end - end - minetest.log("info", "[tsm_railcorridors] Cart spawn FAILED: "..minetest.pos_to_string(pos)) -end -end - -- Try to place a cobweb. -- pos: Position of cobweb -- needs_check: If true, checks if any of the nodes above, below or to the side of the cobweb. @@ -938,17 +920,13 @@ local function spawn_carts() -- See local cart_id = tsm_railcorridors.carts[cart_type] minetest.log("info", "[tsm_railcorridors] Cart spawn attempt: "..minetest.pos_to_string(cpos)) - local obj = minetest.add_entity(cpos, cart_id) + local cart_staticdata = nil - -- This checks if the cart is actually spawned, it's a giant hack! - -- Note that the callback function is also called there. - -- TODO: Move callback function to this position when the - -- minetest.add_entity bug has been fixed (supposedly in 5.9.0?) - if RecheckCartHack then - minetest.after(3, RecheckCartHack, {cpos, cart_id}) - else - tsm_railcorridors.on_construct_cart(cpos, obj, pr_carts) - end + -- Try to create cart staticdata + local hook = tsm_railcorridors.create_cart_staticdata + if hook then cart_staticdata = hook(cart_id, cpos, pr, pr_carts) end + + minetest.add_entity(cpos, cart_id, cart_staticdata) end end carts_table = {} @@ -957,7 +935,7 @@ end -- Start generation of a rail corridor system -- main_cave_coords is the center of the floor of the dirt room, from which -- all corridors expand. -local function create_corridor_system(main_cave_coords) +local function create_corridor_system(main_cave_coords, pr) -- Dirt room size local maxsize = 6 @@ -1118,7 +1096,15 @@ mcl_structures.register_structure("mineshaft",{ end if p.y > -10 then return true end InitRandomizer(blockseed) - create_corridor_system(p) + + local hook = tsm_railcorridors.on_start + if hook then hook() end + + create_corridor_system(p, pr) + + local hook = tsm_railcorridors.on_finish + if hook then hook() end + return true end, diff --git a/mods/PLAYER/mcl_playerinfo/init.lua b/mods/PLAYER/mcl_playerinfo/init.lua index 1f1b84749..b2ce5cf70 100644 --- a/mods/PLAYER/mcl_playerinfo/init.lua +++ b/mods/PLAYER/mcl_playerinfo/init.lua @@ -1,7 +1,10 @@ local table = table +local storage = minetest.get_mod_storage() + -- Player state for public API mcl_playerinfo = {} +local player_mod_metadata = {} -- Get node but use fallback for nil or unknown local function node_ok(pos, fallback) @@ -21,8 +24,6 @@ local function node_ok(pos, fallback) return fallback end -local time = 0 - local function get_player_nodes(player_pos) local work_pos = table.copy(player_pos) @@ -43,11 +44,10 @@ local function get_player_nodes(player_pos) return node_stand, node_stand_below, node_head, node_feet, node_head_top end +local time = 0 minetest.register_globalstep(function(dtime) - - time = time + dtime - -- Run the rest of the code every 0.5 seconds + time = time + dtime if time < 0.5 then return end @@ -76,6 +76,23 @@ minetest.register_globalstep(function(dtime) end) +function mcl_playerinfo.get_mod_meta(player_name, modname) + -- Load the player's metadata + local meta = player_mod_metadata[player_name] + if not meta then + meta = minetest.deserialize(storage:get_string(player_name)) + end + if not meta then + meta = {} + end + player_mod_metadata[player_name] = meta + + -- Get the requested module's section of the metadata + local mod_meta = meta[modname] or {} + meta[modname] = mod_meta + return mod_meta +end + -- set to blank on join (for 3rd party mods) minetest.register_on_joinplayer(function(player) local name = player:get_player_name() @@ -87,7 +104,6 @@ minetest.register_on_joinplayer(function(player) node_stand_below = "", node_head_top = "", } - end) -- clear when player leaves @@ -96,3 +112,9 @@ minetest.register_on_leaveplayer(function(player) mcl_playerinfo[name] = nil end) + +minetest.register_on_shutdown(function() + for name,data in pairs(player_mod_metadata) do + storage:set_string(name, minetest.serialize(data)) + end +end) diff --git a/settingtypes.txt b/settingtypes.txt index 86c635582..ec829b783 100644 --- a/settingtypes.txt +++ b/settingtypes.txt @@ -314,6 +314,9 @@ enable_real_maps (Enable Real Maps) bool true # Hack 1: teleport golems home if they are very far from home mcl_mob_allow_nav_hacks (Mob navigation hacks) bool false +# Enable minecart trains +mcl_minecarts_enable_trains (Enable minecart trains) bool true + [Additional Features] # Enable Bookshelf inventories mcl_bookshelf_inventories (Enable bookshelf inventories) bool true diff --git a/textures/default_rail_t_junction_on.png b/textures/default_rail_t_junction_on.png new file mode 100644 index 000000000..cfaece833 Binary files /dev/null and b/textures/default_rail_t_junction_on.png differ diff --git a/textures/mcl_minecarts_rail_activator_t_junction_powered.png b/textures/mcl_minecarts_rail_activator_t_junction_powered.png index 370364817..93b2dec6f 100644 Binary files a/textures/mcl_minecarts_rail_activator_t_junction_powered.png and b/textures/mcl_minecarts_rail_activator_t_junction_powered.png differ diff --git a/textures/mcl_minecarts_rail_detector_t_junction_powered.png b/textures/mcl_minecarts_rail_detector_t_junction_powered.png index 7638413ca..0ce8af4b8 100644 Binary files a/textures/mcl_minecarts_rail_detector_t_junction_powered.png and b/textures/mcl_minecarts_rail_detector_t_junction_powered.png differ diff --git a/textures/mcl_minecarts_rail_golden_t_junction_powered.png b/textures/mcl_minecarts_rail_golden_t_junction_powered.png index 6a4af0209..142d5784d 100644 Binary files a/textures/mcl_minecarts_rail_golden_t_junction_powered.png and b/textures/mcl_minecarts_rail_golden_t_junction_powered.png differ