From a5ad426c8a053889ad1fc4ec017a07f49e48cfec Mon Sep 17 00:00:00 2001 From: OgelGames Date: Wed, 15 May 2024 18:51:46 +1000 Subject: [PATCH] working version --- init.lua | 47 +++++ inventory.lua | 310 +++++++++++++++++++++++++++++++ metadata.lua | 130 +++++++++++++ player.lua | 494 ++++++++++++++++++++++++++++++++++++++++++++++++++ tests.lua | 251 +++++++++++++++++++++++++ 5 files changed, 1232 insertions(+) create mode 100644 init.lua create mode 100644 inventory.lua create mode 100644 metadata.lua create mode 100644 player.lua create mode 100644 tests.lua diff --git a/init.lua b/init.lua new file mode 100644 index 0000000..b6c0522 --- /dev/null +++ b/init.lua @@ -0,0 +1,47 @@ + +fakelib = {} + +local function check(n, v, a, b) + local t = type(v) + if t == a or t == b then + return v + end + local info = debug.getinfo(2, "n") + local f = info.name or "?" + if info.namewhat ~= "method" then + -- Offset argument number when called using '.' instead of ':' + n = n + 1 + end + error(string.format("bad argument #%i to '%s' (%s expected, got %s)", n, f, a, t), 3) +end + +local function secure_table(t, index, id) + setmetatable(t, { + __index = index, + __newindex = {}, + __metatable = id, + }) + return t +end + +local path = minetest.get_modpath("fakelib") + +for _,file in pairs({"metadata", "inventory", "player"}) do + loadfile(path.."/"..file..".lua")(check, secure_table) +end + +if minetest.is_singleplayer() then + minetest.register_chatcommand("fakelib_test", { + description = "Test fakelib's API.", + params = "[]", + func = function(_, param) + local start_time = minetest.get_us_time() + local success = loadfile(path.."/tests.lua")(param == "true") + local end_time = minetest.get_us_time() + if success then + return true, string.format("Testing completed in %i us", end_time - start_time) + end + return true, "Testing failed. See console for errors." + end, + }) +end diff --git a/inventory.lua b/inventory.lua new file mode 100644 index 0000000..04a1ffb --- /dev/null +++ b/inventory.lua @@ -0,0 +1,310 @@ + +local fake_inventory = {} +local identifier = "fakelib:inventory" +local check, secure_table = ... + +-- API functions +---------------------------------------- + +function fakelib.is_inventory(x) + if type(x) == "userdata" and x.get_lists then + return true + elseif type(x) == "table" and getmetatable(x) == identifier then + return true + end + return false +end + +function fakelib.create_inventory(inv) + local lists = {} + if inv and fakelib.is_inventory(inv) then + lists = inv:get_lists() + for listname in pairs(lists) do + local width = inv:get_width(listname) + if width > 0 then + lists[listname].width = width + end + end + elseif type(inv) == "table" then + for listname, size in pairs(inv) do + if type(listname) == "string" and type(size) == "number" then + local list = {} + for i=1, size do + list[i] = ItemStack() + end + lists[listname] = list + end + end + end + return secure_table({lists = lists}, fake_inventory, identifier) +end + +-- Helper functions +---------------------------------------- + +local function copy_list(list) + local copy = {} + for i=1, #list do + copy[i] = ItemStack(list[i]) + end + return copy +end + +local function stack_matches(a, b, match_meta) + if a:get_name() ~= b:get_name() then + return false + end + if match_meta then + if a:get_wear() ~= b:get_wear() then + return false + end + return a:get_meta():equals(b:get_meta()) + end + return true +end + +-- Inventory functions +---------------------------------------- + +function fake_inventory:is_empty(listname) + check(1, listname, "string", "number") + local list = self.lists[tostring(listname)] + if not list or #list == 0 then + return true + end + for _,stack in ipairs(list) do + if not stack:is_empty() then + return false + end + end + return true +end + +function fake_inventory:get_size(listname) + check(1, listname, "string", "number") + local list = self.lists[tostring(listname)] + return list and #list or 0 +end + +function fake_inventory:set_size(listname, size) + check(1, listname, "string", "number") + check(2, size, "number") + listname = tostring(listname) + if size ~= size or size < 0 then + return false + end + size = math.floor(size) + if size == 0 then + self.lists[listname] = nil + return true + end + local list = self.lists[listname] or {} + if #list < size then + for i=#list+1, size do + list[i] = ItemStack() + end + elseif #list > size then + for i=size+1, #list do + list[i] = nil + end + end + self.lists[listname] = list + return true +end + +function fake_inventory:get_width(listname) + check(1, listname, "string", "number") + local list = self.lists[tostring(listname)] + return list and list.width or 0 +end + +function fake_inventory:set_width(listname, width) + check(1, listname, "string", "number") + check(2, width, "number") + local list = self.lists[tostring(listname)] + if not list or width ~= width or width < 0 then + return false + end + width = math.floor(width) + list.width = width > 0 and width or nil + return true +end + +function fake_inventory:get_stack(listname, i) + check(1, listname, "string", "number") + check(2, i, "number") + i = math.floor(i) + local list = self.lists[tostring(listname)] + if not list or not list[i] then + return ItemStack() + end + return ItemStack(list[i]) +end + +function fake_inventory:set_stack(listname, i, stack) + check(1, listname, "string", "number") + check(2, i, "number") + stack = ItemStack(stack) + i = math.floor(i) + local list = self.lists[tostring(listname)] + if not list or not list[i] or stack:is_empty() then + return false + end + list[i] = stack + return true +end + +function fake_inventory:get_list(listname) + check(1, listname, "string", "number") + local list = self.lists[tostring(listname)] + return list and copy_list(list) or nil +end + +function fake_inventory:set_list(listname, list) + check(1, listname, "string", "number") + listname = tostring(listname) + if list == nil then + self.lists[listname] = nil + return + end + check(2, list, "table") + local new_list, size = {}, 0 + for i,s in pairs(list) do + check(4, i, "number") + if i > size then + size = i + end + new_list[i] = ItemStack(s) + end + for i=1, size do + if not new_list[i] then + new_list[i] = ItemStack() + end + end + self.lists[listname] = new_list +end + +function fake_inventory:get_lists() + local lists = {} + for listname, list in pairs(self.lists) do + lists[listname] = copy_list(list) + end + return lists +end + +function fake_inventory:set_lists(lists) + check(1, lists, "table") + local new_lists = {} + for listname, list in pairs(lists) do + check(3, listname, "string", "number") + check(3, list, "table") + listname = tostring(listname) + local new_list, size = {}, 0 + for i,s in pairs(list) do + check(5, i, "number") + if i > size then + size = i + end + new_list[i] = ItemStack(s) + end + for i=1, size do + if not new_list[i] then + new_list[i] = ItemStack() + end + end + new_lists[listname] = new_list + end + self.lists = new_lists +end + +function fake_inventory:add_item(listname, stack) + check(1, listname, "string", "number") + stack = ItemStack(stack) + local list = self.lists[tostring(listname)] + if not list or #list == 0 or stack:is_empty() then + return stack + end + local empty = {} + for _,s in ipairs(list) do + if s:is_empty() then + table.insert(empty, s) + else + stack = s:add_item(stack) + if stack:is_empty() then + return stack + end + end + end + for _,s in ipairs(empty) do + stack = s:add_item(stack) + if stack:is_empty() then + return stack + end + end + return stack +end + +function fake_inventory:room_for_item(listname, stack) + check(1, listname, "string", "number") + stack = ItemStack(stack) + local list = self.lists[tostring(listname)] + if not list or #list == 0 or stack:is_empty() then + return false + end + for _,s in ipairs(copy_list(list)) do + stack = s:add_item(stack) + if stack:is_empty() then + return true + end + end + return false +end + +function fake_inventory:contains_item(listname, stack, match_meta) + check(1, listname, "string", "number") + stack = ItemStack(stack) + local list = self.lists[tostring(listname)] + if not list or stack:is_empty() or stack:is_empty() then + return false + end + local count = stack:get_count() + for _,s in ipairs(list) do + if stack_matches(stack, s, match_meta) then + count = count - s:get_count() + if count <= 0 then + return true + end + end + end + return false +end + +function fake_inventory:remove_item(listname, stack) + check(1, listname, "string", "number") + stack = ItemStack(stack) + local list = self.lists[tostring(listname)] + if not list or #list == 0 or stack:is_empty() then + return ItemStack() + end + local name, remaining, removed = stack:get_name(), stack:get_count() + for i=#list, 1, -1 do + local s = list[i] + if s:get_name() == name then + s = s:take_item(remaining) + remaining = remaining - s:get_count() + if not removed then + removed = s + else + removed:set_count(removed:get_count() + s:get_count()) + end + if remaining == 0 then + break + end + end + end + return removed or ItemStack() +end + +function fake_inventory.get_location() + return {type = "undefined"} +end diff --git a/metadata.lua b/metadata.lua new file mode 100644 index 0000000..f8a7b0d --- /dev/null +++ b/metadata.lua @@ -0,0 +1,130 @@ + +local fake_metadata = {} +local identifier = "fakelib:metadata" +local check, secure_table = ... + +-- API functions +---------------------------------------- + +function fakelib.is_metadata(x) + if type(x) == "userdata" and x.get_keys then + return true + elseif type(x) == "table" and getmetatable(x) == identifier then + return true + end + return false +end + +function fakelib.create_metadata(meta) + local fields = {} + if meta and fakelib.is_metadata(meta) then + fields = meta:to_table().fields + end + return secure_table({fields = fields}, fake_metadata, identifier) +end + +-- Metadata functions +---------------------------------------- + +function fake_metadata:contains(key) + check(1, key, "string", "number") + key = tostring(key) + return self.fields[key] ~= nil +end + +function fake_metadata:get(key) + check(1, key, "string", "number") + key = tostring(key) + return self.fields[key] +end + +function fake_metadata:set_string(key, value) + check(1, key, "string", "number") + check(2, value, "string", "number") + key = tostring(key) + value = tostring(value) + if value == "" then + self.fields[key] = nil + end + self.fields[key] = value +end + +function fake_metadata:get_string(key) + check(1, key, "string", "number") + key = tostring(key) + return self.fields[key] or "" +end + +function fake_metadata:set_int(key, value) + check(1, key, "string", "number") + check(2, value, "number") + key = tostring(key) + if value >= 2^31 then + value = 0 + end + self.fields[key] = string.format("%i", value) +end + +function fake_metadata:get_int(key) + check(1, key, "string", "number") + key = tostring(key) + return tonumber(self.fields[key]) or 0 +end + +function fake_metadata:set_float(key, value) + check(1, key, "string", "number") + check(2, value, "number") + key = tostring(key) + self.fields[key] = string.format("%s", value) +end + +function fake_metadata:get_float(key) + check(1, key, "string", "number") + key = tostring(key) + return tonumber(self.fields[key]) or 0 +end + +function fake_metadata:get_keys() + local keys = {} + for key in pairs(self.fields) do + table.insert(keys, key) + end + return keys +end + +function fake_metadata:to_table() + return {fields = table.copy(self.fields)} +end + +function fake_metadata:from_table(data) + if type(data) ~= "table" or type(data.fields) ~= "table" then + self.fields = {} + return true + end + local fields = {} + for k,v in pairs(data.fields) do + check(4, k, "string") + check(5, v, "string", "number") + fields[k] = tostring(v) + end + self.fields = fields + return true +end + +function fake_metadata:equals(other) + if not fakelib.is_metadata(other) then + check(1, other, "MetaDataRef") + end + local fields = other:to_table().fields + for k,v in pairs(self.fields) do + if fields[k] == v then + fields[k] = nil + elseif fields[k] ~= nil then + return false + end + end + if next(fields) == nil then + return true + end + return false +end diff --git a/player.lua b/player.lua new file mode 100644 index 0000000..2cf746a --- /dev/null +++ b/player.lua @@ -0,0 +1,494 @@ + +local fake_player = {is_fake_player = true} +local identifier = "fakelib:player" +local check, secure_table = ... + +local player_controls = { + up = 1, down = 2, left = 4, right = 8, jump = 16, + aux1 = 32, sneak = 64, dig = 128, place = 256, zoom = 512, +} + +-- API functions +---------------------------------------- + +function fakelib.is_player(x) + if type(x) == "userdata" and x.get_player_name then + return true + elseif type(x) == "table" and getmetatable(x) == identifier then + return true + end + return false +end + +function fakelib.create_player(player) + local data = {} + if type(player) == "string" then + data.name = player + elseif fakelib.is_player(player) then + data.name = player:get_player_name() + elseif type(player) == "table" then + if type(player.name) == "string" then + data.name = player.name + end + if type(player.position) == "table" then + data.position = vector.copy(player.position) + end + if type(player.direction) == "table" then + local dir = vector.normalize(player.direction) + data.pitch = -math.asin(dir.y) + data.yaw = math.atan2(-dir.x, dir.z) % (math.pi * 2) + end + if type(player.controls) == "table" then + data.controls = {} + player.controls.dig = player.controls.dig or player.controls.LMB + player.controls.place = player.controls.place or player.controls.RMB + for name in pairs(player_controls) do + data.controls[name] = player.controls[name] == true + end + end + if fakelib.is_metadata(player.metadata) then + data.metadata = fakelib.create_metadata(player.metadata) + end + if fakelib.is_inventory(player.inventory) then + data.inventory = fakelib.create_inventory(player.inventory) + end + local size = 32 + if data.inventory and type(player.wield_list) == "string" then + size = data.inventory:get_size(player.wield_list) + if size > 0 then + data.wield_list = player.wield_list + end + end + if type(player.wield_index) == "number" then + if player.wield_index > 0 and player.wield_index <= size then + data.wield_index = player.wield_index + end + end + end + return secure_table({data = data}, fake_player, identifier) +end + +-- Helper functions +---------------------------------------- + +local function check_vector(v) + local t = type(v) + if t ~= "table" then + error(string.format("\"Invalid vector (expected table got %s).\"", t), 3) + end + for _,c in ipairs({"x", "y", "z"}) do + t = type(v[c]) + if t ~= "number" then + error(string.format("\"Invalid vector coordinate %s (expected number got %s).\"", c, t), 3) + end + end +end + +-- Dynamic get/set functions +---------------------------------------- + +function fake_player:get_player_name() + return self.data.name or "" +end + +function fake_player:get_inventory() + if not self.data.inventory then + self.data.inventory = fakelib.create_inventory({ + main = 32, craft = 9, craftpreview = 1, craftresult = 1 + }) + end + return self.data.inventory +end + +function fake_player:get_meta() + if not self.data.metadata then + self.data.metadata = fakelib:create_metadata() + end + return self.data.metadata +end + +function fake_player:get_look_dir() + local p, y = self.data.pitch or 0, self.data.yaw or 0 + return vector.new(math.sin(-y) * math.cos(p), math.sin(-p), math.cos(y) * math.cos(p)) +end + +function fake_player:get_look_horizontal() + return self.data.yaw or 0 +end + +function fake_player:set_look_horizontal(value) + check(1, value, "number") + self.data.yaw = value % (math.pi * 2) +end + +function fake_player:get_look_vertical() + return self.data.pitch or 0 +end + +function fake_player:set_look_vertical(value) + check(1, value, "number") + self.data.pitch = math.max(-math.pi / 2, math.min(value, math.pi / 2)) +end + +function fake_player:get_player_control() + local controls = {} + if self.data.controls then + for name in pairs(player_controls) do + controls[name] = self.data.controls[name] + end + else + for name in pairs(player_controls) do + controls[name] = false + end + end + controls.LMB = controls.dig + controls.RMB = controls.place + return controls +end + +function fake_player:get_player_control_bits() + if not self.data.controls then + return 0 + end + local total = 0 + for name, value in pairs(player_controls) do + total = total + self.data.controls[name] and value or 0 + end + return total +end + +function fake_player:get_pos() + if self.data.position then + return vector.copy(self.data.position) + end + return vector.zero() +end + +function fake_player:set_pos(pos) + check_vector(pos) + self.data.position = vector.copy(pos) +end +fake_player.move_to = fake_player.set_pos + +function fake_player:add_pos(pos) + check_vector(pos) + if self.data.position then + self.data.position = vector.add(self.data.position, pos) + else + self.data.position = vector.copy(pos) + end +end + +function fake_player:get_wield_index() + return self.data.wield_index or 1 +end + +function fake_player:get_wield_list() + return self.data.wield_list or "main" +end + +function fake_player:get_wielded_item() + if self.data.inventory then + return self.data.inventory:get_stack(self:get_wield_list(), self:get_wield_index()) + end + return ItemStack() +end + +function fake_player:set_wielded_item(stack) + stack = ItemStack(stack) + if not self.data.inventory and stack:is_empty() then + return true + end + self:get_inventory():set_stack(self:get_wield_list(), self:get_wield_index(), stack) + return true +end + +-- Static get functions +---------------------------------------- + +function fake_player.is_player() + return true +end + +function fake_player.get_animation() + return {x = 1, y = 1}, 15, 0, true +end + +function fake_player.get_armor_groups() + return {immortal = 1} +end + +function fake_player.get_bone_override() + return { + position = {absolute = false, vec = vector.zero(), interpolation = 0}, + rotation = {absolute = false, vec = vector.zero(), interpolation = 0}, + scale = {absolute = false, vec = vector.new(1, 1, 1), interpolation = 0}, + } +end + +function fake_player.get_bone_overrides() + return {} +end + +function fake_player.get_bone_position() + return vector.zero(), vector.zero() +end + +function fake_player.get_breath() + return 10 +end + +function fake_player.get_children() + return {} +end + +function fake_player.get_clouds() + return { + ambient = {r = 0, g = 0, b = 0, a = 255}, + color = {r = 240, g = 240, b = 255, a = 229}, + density = 0.4, + height = 120, + speed = {x = 0, y = -2}, + thickness = 16, + } +end + +function fake_player.get_eye_offset() + return vector.zero(), vector.zero(), vector.zero() +end + +function fake_player.get_formspec_prepend() + return "" +end + +function fake_player.get_fov() + return 0, false, 0 +end + +function fake_player.get_hp() + return 20 +end + +function fake_player.get_inventory_formspec() + return "" +end + +function fake_player.get_lighting() + return { + exposure = { + speed_bright_dark = 1000, + center_weight_power = 1, + luminance_min = -3, + luminance_max = -3, + exposure_correction = 0, + speed_dark_bright = 1000 + }, + saturation = 1, + shadows = {intensity = 0}, + volumetric_light = {strength = 0}, + } +end + +function fake_player.get_local_animation() + return {x = 0, y = 0}, {x = 0, y = 0}, {x = 0, y = 0}, {x = 0, y = 0}, 0 +end + +function fake_player.get_moon() + return { + scale = 1, + texture = "moon.png", + tonemap = "moon_tonemap.png", + visible = true, + } +end + +function fake_player.get_nametag_attributes() + return { + bgcolor = false, + color = {r = 255, g = 255, b = 255, a = 255}, + text = "", + } +end + +function fake_player.get_physics_override() + return { + acceleration_air = 1, acceleration_default = 1, acceleration_fast = 1, + gravity = 1, jump = 1, speed = 1, + liquid_fluidity = 1, liquid_fluidity_smooth = 1, liquid_sink = 1, + speed_climb = 1, speed_crouch = 1, speed_fast = 1, speed_walk = 1, + new_move = true, sneak = true, sneak_glitch = false, + } +end + +function fake_player.get_properties() + return { + automatic_face_movement_dir = false, + automatic_face_movement_max_rotation_per_sec = -1, + automatic_rotate = 0, + backface_culling = false, + breath_max = 10, + collide_with_objects = true, + collisionbox = {-0.5, -0.5, -0.5, 0.5, 0.5, 0.5}, + colors = {{r = 255, g = 255, b = 255, a = 255}}, + damage_texture_modifier = "^[brighten", + eye_height = 0, + glow = 0, + hp_max = 20, + infotext = "", + initial_sprite_basepos = {x = 0, y = 0}, + is_visible = true, + makes_footstep_sound = true, + mesh = "", + nametag = "", + nametag_bgcolor = false, + nametag_color = {r = 255, g = 255, b = 255, a = 255}, + physical = false, + pointable = true, + selectionbox = {-0.5, -0.5, -0.5, 0.5, 0.5, 0.5, rotate = false}, + shaded = true, + show_on_minimap = true, + spritediv = {x = 1, y = 1}, + static_save = true, + stepheight = 0.6, + textures = {"blank.png"}, + use_texture_alpha = false, + visual = "cube", + visual_size = vector.new(1, 1, 1), + wield_item = "", + zoom_fov = 15, + } +end + +function fake_player.get_sky_color() + return fake_player.get_sky(true).sky_color +end + +function fake_player.get_sky(as_table) + if as_table then + return { + base_color = {r = 255, g = 255, b = 255, a = 255}, + clouds = true, + fog = {fog_distance = -1, fog_start = -1}, + sky_color = { + day_sky = {r = 97, g = 181, b = 245, a = 255}, + day_horizon = {r = 144, g = 211, b = 246, a = 255}, + dawn_sky = {r = 180, g = 186, b = 250, a = 255}, + dawn_horizon = {r = 186, g = 193, b = 240, a = 255}, + night_sky = {r = 0, g = 107, b = 255, a = 255}, + night_horizon = {r = 64, g = 144, b = 255, a = 255}, + indoors = {r = 100, g = 100, b = 100, a = 255}, + fog_sun_tint = {r = 244, g = 125, b = 29, a = 255}, + fog_moon_tint = {r = 128, g = 153, b = 204, a = 255}, + fog_tint_type = "default", + }, + textures = {}, + type = "regular", + } + end + return {r = 255, g = 255, b = 255, a = 255}, "regular", {}, true +end + +function fake_player.get_stars() + return { + count = 1000, + day_opacity = 0, + scale = 1, + star_color = {r = 235, g = 235, b = 255, a = 105}, + visible = true, + } +end + +function fake_player.get_sun() + return { + scale = 1, + sunrise = "sunrisebg.png", + sunrise_visible = true, + texture = "sun.png", + tonemap = "sun_tonemap.png", + visible = true, + } +end + +function fake_player.get_velocity() + return vector.zero() +end + +function fake_player.hud_get_all() + return {} +end + +function fake_player.hud_get_flags() + return { + basic_debug = false, + breathbar = false, + chat = false, + crosshair = false, + healthbar = false, + hotbar = false, + minimap = false, + minimap_radar = false, + wielditem = false, + } +end + +function fake_player.hud_get_hotbar_image() + return "" +end + +function fake_player.hud_get_hotbar_itemcount() + return 8 +end + +function fake_player.hud_get_hotbar_selected_image() + return "" +end + +-- No-op functions +---------------------------------------- +do + local functions = { + -- Lua entity only (no-op for players) + "get_acceleration", "get_entity_name", "get_luaentity", "get_rotation", + "get_texture_mod", "get_yaw", "getacceleration", "getyaw", "remove", + "set_acceleration", "set_rotation", "set_sprite", "set_texture_mod", + "set_velocity", "set_yaw", "setacceleration", "setsprite", + "settexturemod", "setvelocity", "setyaw", + -- Non-functional get/set functions + "add_velocity", "get_attach", "get_attribute", "get_day_night_ratio", + "hud_add", "hud_change", "hud_get", "hud_remove", "hud_set_flags", + "hud_set_hotbar_image", "hud_set_hotbar_itemcount", + "hud_set_hotbar_selected_image", "override_day_night_ratio", + "set_animation", "set_animation_frame_speed", "set_armor_groups", + "set_attach", "set_attribute", "set_bone_override", "set_bone_position", + "set_breath", "set_clouds", "set_detach", "set_eye_offset", + "set_formspec_prepend", "set_fov", "set_hp", "set_inventory_formspec", + "set_lighting", "set_local_animation", "set_minimap_modes", "set_moon", + "set_nametag_attributes", "set_physics_override", + "set_properties", "set_sky", "set_stars", "set_sun", + -- Other functions that do nothing + "punch", "respawn", "right_click", "send_mapblock", + } + for _,func in ipairs(functions) do + fake_player[func] = function() end + end +end + +-- Deprecated functions +---------------------------------------- + +function fake_player:get_look_pitch() + return self:get_look_vertical() * -1 +end + +function fake_player:get_look_yaw() + return self:get_look_horizontal() + math.pi / 2 +end + +fake_player.set_look_pitch = fake_player.set_look_vertical +fake_player.set_look_yaw = fake_player.set_look_horizontal +fake_player.getpos = fake_player.get_pos +fake_player.setpos = fake_player.set_pos +fake_player.moveto = fake_player.set_pos +fake_player.getvelocity = fake_player.get_velocity +fake_player.add_player_velocity = fake_player.add_velocity +fake_player.get_player_velocity = fake_player.get_velocity diff --git a/tests.lua b/tests.lua new file mode 100644 index 0000000..a9d8a97 --- /dev/null +++ b/tests.lua @@ -0,0 +1,251 @@ + +local error_tests = ... +local success = true + +-- Helper functions +------------------------------ + +local function round(x) + if x >= 0 then + return math.floor(x + 0.5) + end + return math.ceil(x - 0.5) +end + +local function is_equal(a, b) + local ta, tb = type(a), type(b) + -- Compare table values recursively + if ta == "table" and tb == "table" then + if next(a) == nil and next(b) == nil then + return true + end + local keys = {} + for k in pairs(a) do + keys[k] = true + end + for k in pairs(b) do + keys[k] = true + end + for k in pairs(keys) do + if not is_equal(a[k], b[k]) then + return false + end + end + return true + end + -- Get rid of floating point errors + if ta == "number" and tb == "number" then + a = round(a * 1000) / 1000 + b = round(b * 1000) / 1000 + end + return a == b +end + +local function test(real, fake, func, ...) + local rvals_real = {pcall(real[func], real, ...)} + local rvals_fake = {pcall(fake[func], fake, ...)} + local i = 1 + while true do + if rvals_real[i] == nil and rvals_fake[i] == nil then + break + end + if not is_equal(rvals_real[i], rvals_fake[i]) then + success = false + print(string.format("[fakelib] Test failed with function '%s' and return value #%i", func, i-1)) + if #{...} > 0 then + print(" > Function arguments: ", ...) + end + local a = type(rvals_real[i]) == "table" and dump(rvals_real[i]) or tostring(rvals_real[i]) + print(string.format(" > Real return value: %s", a)) + local b = type(rvals_fake[i]) == "table" and dump(rvals_fake[i]) or tostring(rvals_fake[i]) + print(string.format(" > Fake return value: %s", b)) + end + i = i + 1 + end +end + +local test_values = {"abc", "", 0, -1, {}, {true}, true, function() end, 0/0, nil} + +local function iter_values() + local i = 0 + return function () + i = i + 1 + if i <= 1 then return test_values[i] end + end +end + +-- Player tests +------------------------------ + +local real_player = minetest.get_player_by_name("singleplayer") +local fake_player = fakelib.create_player("singleplayer") + +-- Function check +for func in pairs(getmetatable(real_player)) do + if not fake_player[func] then + success = false + print(string.format("[fakelib] Missing function '%s'", func)) + end +end + +-- Functional tests +test(real_player, fake_player, "get_player_name") +test(real_player, fake_player, "get_player_control") +test(real_player, fake_player, "get_player_control_bits") +test(real_player, fake_player, "set_pos", vector.zero()) +test(real_player, fake_player, "add_pos", vector.new(1, 1, 1)) +test(real_player, fake_player, "get_pos") +test(real_player, fake_player, "set_look_horizontal", 0.6) +test(real_player, fake_player, "set_look_vertical", 0.3) +test(real_player, fake_player, "get_look_dir") +test(real_player, fake_player, "get_look_horizontal") +test(real_player, fake_player, "get_look_pitch") +test(real_player, fake_player, "get_look_yaw") +test(real_player, fake_player, "set_wielded_item", ItemStack("air 99")) +test(real_player, fake_player, "get_wielded_item") + +-- Error tests +if error_tests then + for value in iter_values() do + -- Only test dynamic functions, no-op functions don't check arguments + test(real_player, fake_player, "set_pos", value) + test(real_player, fake_player, "add_pos", value) + test(real_player, fake_player, "set_look_horizontal", value) + test(real_player, fake_player, "set_look_vertical", value) + test(real_player, fake_player, "set_wielded_item", value) + end +end + +-- Inventory tests +------------------------------ + +local real_inv = real_player:get_inventory() +local fake_inv = fakelib.create_inventory() + +-- Function check +for func in pairs(getmetatable(real_inv)) do + if not fake_inv[func] then + success = false + print(string.format("[fakelib] Missing function '%s'", func)) + end +end + +-- Save and clear +local old_inv = real_inv:get_lists() +real_inv:set_lists({}) + +local test_item = ItemStack({name = "air", meta = {test = "abc"}}) +local test_list = {"", "air 50", test_item, "", "air 50", test_item} + +-- Functional tests +test(real_inv, fake_inv, "set_list", "test", test_list) +test(real_inv, fake_inv, "is_empty", "test") +test(real_inv, fake_inv, "set_lists", {test = test_list}) +test(real_inv, fake_inv, "set_size", "test", 10) +test(real_inv, fake_inv, "get_size", "test") +test(real_inv, fake_inv, "set_width", "test", 3) +test(real_inv, fake_inv, "get_width", "test") +test(real_inv, fake_inv, "set_stack", "test", 1.5, "air") +test(real_inv, fake_inv, "get_stack", "test", 1) +test(real_inv, fake_inv, "get_list", "test") +test(real_inv, fake_inv, "add_item", "test", "air") +test(real_inv, fake_inv, "room_for_item", "test", "air 99") +test(real_inv, fake_inv, "contains_item", "test", "air 99") +test(real_inv, fake_inv, "contains_item", "test", test_item, true) +test(real_inv, fake_inv, "remove_item", "test", "air 99") +test(real_inv, fake_inv, "get_lists") + +-- Error tests +if error_tests then + for value in iter_values() do + test(real_inv, fake_inv, "set_list", value, {}) + test(real_inv, fake_inv, "set_list", "test", value) + test(real_inv, fake_inv, "is_empty", value) + test(real_inv, fake_inv, "set_lists", value) + test(real_inv, fake_inv, "set_size", value, 1) + test(real_inv, fake_inv, "set_size", "test", value) + test(real_inv, fake_inv, "get_size", value) + test(real_inv, fake_inv, "set_width", value, 1) + test(real_inv, fake_inv, "set_width", "test", value) + test(real_inv, fake_inv, "get_width", value) + test(real_inv, fake_inv, "set_stack", value, 1, "test") + test(real_inv, fake_inv, "set_stack", "test", value, "test") + test(real_inv, fake_inv, "set_stack", "test", 1, value) + test(real_inv, fake_inv, "get_stack", value, 1) + test(real_inv, fake_inv, "get_stack", "test", value) + test(real_inv, fake_inv, "get_list", value) + test(real_inv, fake_inv, "add_item", value, "test") + test(real_inv, fake_inv, "add_item", "test", value) + test(real_inv, fake_inv, "room_for_item", value, "test") + test(real_inv, fake_inv, "room_for_item", "test", value) + test(real_inv, fake_inv, "contains_item", value, "test") + test(real_inv, fake_inv, "contains_item", "test", value) + test(real_inv, fake_inv, "contains_item", "test", "test", value) + test(real_inv, fake_inv, "remove_item", value, "test") + test(real_inv, fake_inv, "remove_item", "test", value) + end +end + +-- Reset +real_inv:set_lists(old_inv) + + +-- Metadata tests +------------------------------ + +local real_meta = real_player:get_meta() +local fake_meta = fakelib.create_metadata() + +-- Function check +for func in pairs(getmetatable(real_meta)) do + if not fake_meta[func] then + success = false + print(string.format("[fakelib] Missing function '%s'", func)) + end +end + +-- Save and clear +local old_meta = real_meta:to_table() +real_meta:from_table({}) + +local test_data = {fields = {test = "abc"}} + +-- Functional tests +test(real_meta, fake_meta, "from_table", test_data) +test(real_meta, fake_meta, "contains", "test") +test(real_meta, fake_meta, "get", "test") +test(real_meta, fake_meta, "set_string", "test", "xyz") +test(real_meta, fake_meta, "get_string", "test") +test(real_meta, fake_meta, "set_int", "test", 123) +test(real_meta, fake_meta, "get_int", "test") +test(real_meta, fake_meta, "set_float", "test", 123.456789) +test(real_meta, fake_meta, "get_float", "test") +test(real_meta, fake_meta, "get_keys") +test(real_meta, fake_meta, "to_table") +test(real_meta, fake_meta, "equals", real_meta) + +-- Error tests +if error_tests then + for value in iter_values() do + test(real_meta, fake_meta, "from_table", value) + test(real_meta, fake_meta, "contains", value) + test(real_meta, fake_meta, "get", value) + test(real_meta, fake_meta, "set_string", value, "test") + test(real_meta, fake_meta, "set_string", "test", value) + test(real_meta, fake_meta, "get_string", value) + test(real_meta, fake_meta, "set_int", value, 0) + test(real_meta, fake_meta, "set_int", "test", value) + test(real_meta, fake_meta, "get_int", value) + test(real_meta, fake_meta, "set_float", value, 0) + test(real_meta, fake_meta, "set_float", "test", value) + test(real_meta, fake_meta, "get_float", value) + test(real_meta, fake_meta, "equals", value) + end +end + +-- Reset +real_meta:from_table(old_meta) + +------------------------------ + +return success