forked from VoxeLibre/VoxeLibre
Added datapacks and loot table functionality
This commit is contained in:
parent
026ea5940c
commit
ed1bb20e24
|
@ -0,0 +1,36 @@
|
|||
{
|
||||
"type": "chest",
|
||||
"functions": [],
|
||||
"pools": [
|
||||
{
|
||||
"conditions": [],
|
||||
"functions": [],
|
||||
"rolls": {
|
||||
"type": "uniform",
|
||||
"min": 2,
|
||||
"max": 4
|
||||
},
|
||||
"bonus_rolls": 1,
|
||||
"entries": [
|
||||
{
|
||||
"conditions": [],
|
||||
"type": "item",
|
||||
"weight": 1,
|
||||
"quality": 0,
|
||||
"functions": [
|
||||
{
|
||||
"conditions": [],
|
||||
"function": "set_count",
|
||||
"count": {
|
||||
"type": "binomial",
|
||||
"n": 20,
|
||||
"p": 0.5
|
||||
}
|
||||
}
|
||||
],
|
||||
"name": "mcl_core:diamond"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,54 @@
|
|||
local modpath = minetest.get_modpath(minetest.get_current_modname())
|
||||
|
||||
local default_datapack_path = modpath .. "/../../../datapacks/vanilla"
|
||||
|
||||
|
||||
vl_datapacks = {
|
||||
registries = {},
|
||||
loaded_datapacks = {},
|
||||
registry_specs = {
|
||||
["loot_table"] = "json",
|
||||
}
|
||||
}
|
||||
|
||||
--[[ Format of `vl_datapacks.registries`:
|
||||
{
|
||||
<registry name>: {
|
||||
<namespace>: {
|
||||
<path string>: resource,
|
||||
}
|
||||
<namespace>: {
|
||||
<path string>: resource,
|
||||
}
|
||||
},
|
||||
<registry name>: {
|
||||
<namespace>: {
|
||||
<path string>: resource,
|
||||
}
|
||||
<namespace>: {
|
||||
<path string>: resource,
|
||||
}
|
||||
},
|
||||
}
|
||||
]]
|
||||
|
||||
local function split_resource_string(resource_string)
|
||||
local match_start, _, namespace, path = string.find(resource_string, "([^%s]+)%:([^%s]+)")
|
||||
if not match_start then
|
||||
error("Invalid resource string: " .. resource_string)
|
||||
end
|
||||
return namespace, path
|
||||
end
|
||||
|
||||
function vl_datapacks.get_resource(registry, resource_string)
|
||||
local namespace, path = split_resource_string(resource_string)
|
||||
return vl_datapacks.registries[registry][namespace][path]
|
||||
end
|
||||
|
||||
for registry_name, _ in pairs(vl_datapacks.registry_specs) do
|
||||
vl_datapacks.registries[registry_name] = {}
|
||||
end
|
||||
|
||||
dofile(modpath .. "/resource_loader.lua")
|
||||
|
||||
vl_datapacks.load_datapack("vanilla", default_datapack_path)
|
|
@ -0,0 +1,155 @@
|
|||
--[[ -- Gets the last extension of a filename
|
||||
-- Returns: stem, extension
|
||||
local function split_fname(filename)
|
||||
local matched, _, stem, extension = string.find(filename, "^(.*)%.(.*)$")
|
||||
if not matched then
|
||||
return filename, ""
|
||||
else
|
||||
return stem, extension
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
|
||||
|
||||
-- Loads a resource into `load_into`
|
||||
-- key: filename with last extension stripped
|
||||
-- value: preferred lua format for that resource
|
||||
local function load_resource(path, filename, load_into, strict)
|
||||
local stem, extension = split_fname(filename)
|
||||
local filepath = path .. "/" .. filename
|
||||
local filestring = read_file_string(filepath, strict)
|
||||
if extension == "json" then
|
||||
local parsed_json, err = minetest.parse_json(filestring)
|
||||
if not parsed_json then
|
||||
-- Not valid json
|
||||
error("Error while reading json file " .. filepath .. ": " .. error)
|
||||
end
|
||||
load_into[stem] = parsed_json
|
||||
end
|
||||
end
|
||||
|
||||
-- Recursively load resources from `path`
|
||||
-- `load_into`: table to load into
|
||||
-- `strict`: whether to error if resources not found
|
||||
local function load_resources_internal(path, expected_resources, load_into, strict)
|
||||
minetest.debug(path, dump(expected_resources))
|
||||
for subfile, subfile_expected in pairs(expected_resources) do
|
||||
if type(subfile_expected) == "table" then
|
||||
if not load_into[subfile] then
|
||||
load_into[subfile] = {}
|
||||
end
|
||||
local subdir_load_into = load_into[subfile]
|
||||
load_resources_internal(path .. "/" .. subfile, subfile_expected, subdir_load_into, strict)
|
||||
else -- subfile is file, not dir
|
||||
load_resource(path, subfile, load_into, strict)
|
||||
end
|
||||
end
|
||||
end ]]
|
||||
|
||||
--[[ -- Check if match_string starts with start_check
|
||||
local function startswith(match_string, start_check)
|
||||
return string.sub(match_string, 1, string.len(start_check)) == start_check
|
||||
end ]]
|
||||
|
||||
-- Get file subpath without suffix
|
||||
-- Returns nil if invalid (e.g. file had wrong suffix)
|
||||
local function get_stem(subpath, file_suffix)
|
||||
local _, _, stem = string.find(subpath, "(.*)%." .. file_suffix .. "$")
|
||||
return stem
|
||||
end
|
||||
|
||||
-- Read a file at `filepath` and return its contents as a string
|
||||
-- Raises an error if file could not be opened
|
||||
local function read_file_string(filepath)
|
||||
local file, err = io.open(filepath)
|
||||
if not file then
|
||||
error("Error while loading resources: could not open file " .. filepath .. ": " .. err)
|
||||
end
|
||||
local filestring = file:read("*all")
|
||||
file:close()
|
||||
return filestring
|
||||
end
|
||||
|
||||
-- registry_path must end in a slash
|
||||
local function load_single_resource(registry_path, subpath, namespace_table, file_suffix)
|
||||
local absolute_path = registry_path .. subpath
|
||||
local stem = get_stem(subpath, file_suffix)
|
||||
if not stem then
|
||||
error("Invalid filename in datapack at " .. absolute_path)
|
||||
end
|
||||
local raw_data = read_file_string(absolute_path)
|
||||
local loaded_data, err
|
||||
|
||||
minetest.debug("Loading resource at " .. subpath .. " in " .. registry_path)
|
||||
|
||||
if file_suffix == "json" then
|
||||
loaded_data, err = minetest.parse_json(raw_data)
|
||||
if not loaded_data then
|
||||
-- Not valid json
|
||||
error("Error while reading json file " .. filepath .. ": " .. err)
|
||||
end
|
||||
else
|
||||
error("Can't read file with format " .. file_suffix .. " at " .. absolute_path)
|
||||
end
|
||||
|
||||
namespace_table[stem] = loaded_data
|
||||
end
|
||||
|
||||
-- registry_path must end in a slash
|
||||
local function load_resources_recursive(registry_path, subpath, namespace_table, file_suffix)
|
||||
-- Put a / on the end of the subpath, unless we are in the main registry directory
|
||||
if subpath ~= nil then subpath = subpath .. "/"
|
||||
else subpath = "" end
|
||||
|
||||
local absolute_path = registry_path .. subpath
|
||||
|
||||
minetest.debug("Loading resources from " .. absolute_path)
|
||||
-- Load files in this directory
|
||||
for _, filename in pairs(minetest.get_dir_list(absolute_path, false)) do
|
||||
minetest.debug("Loading resource " .. filename)
|
||||
load_single_resource(registry_path, subpath .. filename, namespace_table, file_suffix)
|
||||
end
|
||||
|
||||
-- Load from subdirectories
|
||||
for _, dirname in pairs(minetest.get_dir_list(absolute_path, true)) do
|
||||
minetest.debug("Going into subdirectory " .. dirname)
|
||||
load_resources_recursive(registry_path, subpath .. dirname, namespace_table, file_suffix)
|
||||
end
|
||||
minetest.debug("Finished loading resources from " .. absolute_path)
|
||||
end
|
||||
|
||||
local function load_into_registry(path, registry_name, namespace, file_suffix)
|
||||
local registry = vl_datapacks.registries[registry_name]
|
||||
if not registry[namespace] then registry[namespace] = {} end
|
||||
local namespace_table = registry[namespace]
|
||||
load_resources_recursive(path, nil, namespace_table, file_suffix)
|
||||
end
|
||||
|
||||
|
||||
local function load_into_namespace(namespace, path)
|
||||
for registry_name, file_suffix in pairs(vl_datapacks.registry_specs) do
|
||||
-- registry_path must end in a slash for internal functions
|
||||
load_into_registry(path .. "/" .. registry_name .. "/", registry_name, namespace, file_suffix)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
-- Loads a full datapack into memory, thereby enabling it and any overrides
|
||||
local function load_datapack(name, path)
|
||||
for _, other_name in pairs(vl_datapacks.loaded_datapacks) do
|
||||
if name == other_name then
|
||||
error("Datapack " .. name .. " is already loaded")
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
local data_path = path .. "/data"
|
||||
|
||||
for _, namespace in pairs(minetest.get_dir_list(data_path, true)) do
|
||||
load_into_namespace(namespace, data_path .. "/" .. namespace)
|
||||
end
|
||||
table.insert(vl_datapacks.loaded_datapacks, name)
|
||||
end
|
||||
|
||||
vl_datapacks.load_datapack = load_datapack
|
|
@ -0,0 +1,144 @@
|
|||
-- Main loot engine
|
||||
vl_loot.engine = {}
|
||||
|
||||
-- Adds all items from array `to_append` into old_table
|
||||
-- Returns nil
|
||||
local function append_table(old_table, to_append)
|
||||
for _, item in ipairs(to_append) do
|
||||
table.insert(old_table, item)
|
||||
end
|
||||
end
|
||||
|
||||
-- Modifies each of the stacks in `loot_stacks` according to `modifier_table` (array)
|
||||
-- Edits `loot_stacks` in-place
|
||||
local function modify_stacks(modifier_table, loot_stacks, loot_context)
|
||||
for i, itemstack in pairs(loot_stacks) do
|
||||
loot_stacks[i] = vl_loot.modifier.apply_item_modifier(modifier_table, itemstack, loot_context)
|
||||
end
|
||||
end
|
||||
|
||||
-- Conditionally returns an entry based on whether its conditions passed
|
||||
-- For compound-type entries this can return multiple entries
|
||||
-- Returns array of entries to add to pool
|
||||
local function unpack_entry(entry_provider_table, loot_context)
|
||||
-- ENTRY PROVIDER TYPES (compound have *)
|
||||
-- item
|
||||
-- tag
|
||||
-- loot_table
|
||||
-- dynamic
|
||||
-- empty
|
||||
-- group *
|
||||
-- alternative *
|
||||
-- sequence *
|
||||
|
||||
if not vl_loot.predicate.check_predicates(entry_provider_table.conditions, loot_context) then return {} end
|
||||
|
||||
-- TODO: Support other types
|
||||
if entry_provider_table.type == "item" then
|
||||
return {entry_provider_table}
|
||||
end
|
||||
end
|
||||
|
||||
-- Should always receive a simple-type entry
|
||||
-- Returns all (already modified) stacks to use as pool loot
|
||||
local function get_entry_loot(entry_table, loot_context)
|
||||
-- SIMPLE ENTRY TYPES
|
||||
-- item
|
||||
-- tag
|
||||
-- loot_table
|
||||
-- dynamic
|
||||
-- empty
|
||||
|
||||
-- Don't check conditions, these were already checked in unpack_entry
|
||||
|
||||
local loot_stacks = {}
|
||||
-- TODO: Support other types
|
||||
if entry_table.type == "item" then
|
||||
local itemstack = ItemStack(entry_table.name)
|
||||
table.insert(loot_stacks, itemstack)
|
||||
end
|
||||
|
||||
-- Apply modifier
|
||||
modify_stacks(entry_table.functions, loot_stacks, loot_context)
|
||||
|
||||
return loot_stacks
|
||||
end
|
||||
|
||||
local function get_final_weight(entry_table, loot_context)
|
||||
-- TODO: Add 'quality' based on luck
|
||||
-- TODO: Do some floor stuff?
|
||||
return entry_table.weight
|
||||
end
|
||||
|
||||
local function get_final_rolls(pool_table, loot_context)
|
||||
-- TODO: bonus_rolls (also a number provider)
|
||||
local rolls = vl_loot.number_provider.evaluate(pool_table.rolls, loot_context)
|
||||
-- Must be an integer
|
||||
return math.floor(rolls)
|
||||
end
|
||||
|
||||
-- returns list of itemstacks to add to loot
|
||||
local function get_pool_loot(pool_table, loot_context)
|
||||
if not vl_loot.predicate.check_predicates(pool_table.conditions, loot_context) then return {} end
|
||||
local loot_stacks = {}
|
||||
|
||||
-- Calculate how many rolls to do
|
||||
local rolls = get_final_rolls(pool_table, loot_context)
|
||||
|
||||
-- Unpack entries of a compound type into simple entries
|
||||
local unpacked_entries = {}
|
||||
for _, entry_provider_table in ipairs(pool_table.entries) do
|
||||
local new_unpacked_entries = unpack_entry(entry_provider_table, loot_context)
|
||||
append_table(unpacked_entries, new_unpacked_entries)
|
||||
end
|
||||
|
||||
for i=1,rolls do
|
||||
-- The 'total weight' of all entries combined (= 1 probability)
|
||||
local total_weight = 0
|
||||
-- Array of weights of individual entries
|
||||
local entry_weights = {}
|
||||
|
||||
-- Calculate weights and add to array and total_weight
|
||||
for _, entry_table in ipairs(unpacked_entries) do
|
||||
local current_entry_weight = get_final_weight(entry_table, loot_context)
|
||||
table.insert(entry_weights, current_entry_weight)
|
||||
total_weight = total_weight + current_entry_weight
|
||||
end
|
||||
|
||||
-- Get a random value
|
||||
-- TODO: Make this deterministic per-loot table and world seed
|
||||
local random_value = math.random(0, total_weight)
|
||||
|
||||
-- Work out which entry was picked
|
||||
local chosen_entry
|
||||
for i, entry_weight in ipairs(entry_weights) do
|
||||
random_value = random_value - entry_weight
|
||||
-- If the value went <= 0 then this was the chosen entry
|
||||
if random_value <= 0 then
|
||||
chosen_entry = unpacked_entries[i]
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
minetest.debug(dump(unpacked_entries), dump(entry_weights), total_weight, dump(chosen_entry))
|
||||
-- Get loot from the chosen entry
|
||||
local current_roll_loot = get_entry_loot(chosen_entry, loot_context)
|
||||
append_table(loot_stacks, current_roll_loot)
|
||||
|
||||
end
|
||||
modify_stacks(pool_table.functions, loot_stacks, loot_context)
|
||||
minetest.debug("Pool's loot stacks:", dump(loot_stacks))
|
||||
return loot_stacks
|
||||
end
|
||||
|
||||
local function get_loot(loot_table, loot_context)
|
||||
local loot_stacks = {}
|
||||
for _, pool_table in ipairs(loot_table.pools) do
|
||||
local pool_loot_stacks = get_pool_loot(pool_table, loot_context, loot_stacks)
|
||||
append_table(loot_stacks, pool_loot_stacks)
|
||||
end
|
||||
modify_stacks(loot_table.functions, loot_stacks, loot_context)
|
||||
return loot_stacks
|
||||
end
|
||||
|
||||
vl_loot.engine.get_loot = get_loot
|
|
@ -0,0 +1,59 @@
|
|||
local modpath = minetest.get_modpath(minetest.get_current_modname())
|
||||
|
||||
vl_loot = {
|
||||
}
|
||||
|
||||
|
||||
dofile(modpath .. "/item_modifier.lua")
|
||||
dofile(modpath .. "/predicate.lua")
|
||||
dofile(modpath .. "/number_provider.lua")
|
||||
dofile(modpath .. "/engine.lua")
|
||||
|
||||
--[[ -- Load resources specified in `expected_resources` from `path`
|
||||
-- `strict`: whether to error if resources not found
|
||||
local function load_loot_tables(path, expected_resources, strict)
|
||||
vl_loot.load_resources_internal(path, expected_resources, vl_loot.loot_tables, strict)
|
||||
end ]]
|
||||
|
||||
|
||||
|
||||
-- loot_table: chest loot table
|
||||
-- pos: position of chest node
|
||||
-- opener: objectref of entity that opened chest
|
||||
local function get_chest_loot(loot_table, pos, opener)
|
||||
-- TODO: Provide better context (there are implied fields)
|
||||
local context = {
|
||||
["this"] = opener,
|
||||
["origin"] = pos,
|
||||
}
|
||||
return vl_loot.engine.get_loot(loot_table, context)
|
||||
end
|
||||
|
||||
minetest.register_chatcommand("testloot", {
|
||||
func = function(name, param)
|
||||
local player = minetest.get_player_by_name(name)
|
||||
local pos = player:get_pos()
|
||||
local loot_table = vl_datapacks.get_resource("loot_table", "vanilla:chests/testloot")
|
||||
minetest.debug("testloot table:", dump(loot_table))
|
||||
local loot = get_chest_loot(loot_table, pos, player)
|
||||
for _, stack in ipairs(loot) do
|
||||
minetest.debug(stack:to_string())
|
||||
end
|
||||
local player_inv = player:get_inventory()
|
||||
local inv_list = player_inv:get_list("main")
|
||||
for i, itemstack in ipairs(inv_list) do
|
||||
if itemstack:is_empty() then
|
||||
inv_list[i] = table.remove(loot, 1)
|
||||
end
|
||||
end
|
||||
player_inv:set_list("main", inv_list)
|
||||
if #loot > 0 then
|
||||
minetest.debug("Too much loot for inventory!")
|
||||
end
|
||||
return true
|
||||
end
|
||||
})
|
||||
|
||||
--load_loot_tables(default_loot_path, expected_loot_tables, true)
|
||||
|
||||
minetest.debug(dump(vl_loot))
|
|
@ -0,0 +1,80 @@
|
|||
-- Item modifiers are lists of item functions
|
||||
-- [
|
||||
-- <item function 1>
|
||||
-- <item function 2>
|
||||
-- ]
|
||||
-- The item functions are applied in order to an itemstack when applying a modifier
|
||||
vl_loot.modifier = {}
|
||||
|
||||
local function apply_item_function(function_table, itemstack, loot_context)
|
||||
-- Only apply function if predicates pass
|
||||
if not vl_loot.predicate.check_predicates(function_table.conditions, loot_context) then
|
||||
return itemstack
|
||||
end
|
||||
|
||||
-- TODO: Make this work
|
||||
-- Item functions:
|
||||
--[[
|
||||
apply_bonus
|
||||
copy_components
|
||||
copy_custom_data
|
||||
copy_name
|
||||
copy_state
|
||||
enchant_randomly
|
||||
enchant_with_levels
|
||||
exploration_map
|
||||
explosion_decay
|
||||
fill_player_head
|
||||
filtered
|
||||
furnace_smelt
|
||||
limit_count
|
||||
enchanted_count_increase
|
||||
modify_contents
|
||||
reference
|
||||
sequence
|
||||
set_attributes
|
||||
set_banner_pattern
|
||||
set_book_cover
|
||||
set_components
|
||||
set_contents
|
||||
set_count
|
||||
set_custom_data
|
||||
set_custom_model_data
|
||||
set_damage
|
||||
set_enchantments
|
||||
set_fireworks
|
||||
set_firework_explosion
|
||||
set_instrument
|
||||
set_item
|
||||
set_loot_table
|
||||
set_lore
|
||||
set_name
|
||||
set_potion
|
||||
set_stew_effect
|
||||
set_writable_book_pages
|
||||
set_written_book_pages
|
||||
toggle_tooltips
|
||||
]]
|
||||
if function_table["function"] == "set_count" then
|
||||
local count = vl_loot.number_provider.evaluate(function_table.count, loot_context)
|
||||
if function_table.add then
|
||||
count = count + itemstack:get_count()
|
||||
end
|
||||
itemstack:set_count(count)
|
||||
return itemstack
|
||||
end
|
||||
|
||||
|
||||
|
||||
end
|
||||
|
||||
-- Modify an itemstack based on an item modifier
|
||||
-- Returns the resulting itemstack
|
||||
local function apply_item_modifier(modifier_table, itemstack, loot_context)
|
||||
for _, function_table in ipairs(modifier_table) do
|
||||
itemstack = apply_item_function(function_table, itemstack, loot_context)
|
||||
end
|
||||
return itemstack
|
||||
end
|
||||
|
||||
vl_loot.modifier.apply_item_modifier = apply_item_modifier
|
|
@ -0,0 +1 @@
|
|||
name=vl_loot
|
|
@ -0,0 +1,46 @@
|
|||
-- Evaluates number providers in loot tables
|
||||
vl_loot.number_provider = {}
|
||||
|
||||
local function evaluate(number_provider, loot_context)
|
||||
-- TODO: Support all types
|
||||
-- TODO: Use deterministic random gen
|
||||
-- Possible number provider types:
|
||||
-- [implied constant]
|
||||
-- [implied uniform]
|
||||
-- constant: use .value (f)
|
||||
-- uniform: between .min (f) and .max (f)
|
||||
-- binomial: .n (f), .p (i)
|
||||
-- score: TODO
|
||||
-- storage: TODO
|
||||
-- enchantment_level: TODO (not used for what you think it is)
|
||||
|
||||
-- implied constant
|
||||
if type(number_provider) == "number" then
|
||||
return number_provider
|
||||
-- otherwise we can assume it is a table
|
||||
elseif number_provider.type == "constant" then
|
||||
return number_provider.value
|
||||
elseif number_provider.type == "uniform" then
|
||||
return math.random(number_provider.min, number_provider.max)
|
||||
elseif number_provider.type == "binomial" then
|
||||
-- Sample binomial distribution
|
||||
local total_value = 0
|
||||
local p = number_provider.p
|
||||
for i=1,number_provider.n do
|
||||
if math.random() >= p then
|
||||
total_value = total_value + 1
|
||||
end
|
||||
end
|
||||
return total_value
|
||||
--elseif number_provider.type == "score" then
|
||||
--elseif number_provider.type == "storage" then
|
||||
--elseif number_provider.type == "enchantment_level" then
|
||||
elseif number_provider.min and number_provider.max then
|
||||
return math.random(number_provider.min, number_provider.max)
|
||||
else
|
||||
minetest.log("error", "invalid number provider when calculating loot: ", dump(number_provider))
|
||||
return 0
|
||||
end
|
||||
end
|
||||
|
||||
vl_loot.number_provider.evaluate = evaluate
|
|
@ -0,0 +1,18 @@
|
|||
-- Evaluates predicates (conditions) in loot tables
|
||||
vl_loot.predicate = {}
|
||||
|
||||
local function check_predicate(predicate_table, loot_context)
|
||||
-- TODO: Make this work
|
||||
return true
|
||||
end
|
||||
|
||||
local function check_predicates(predicate_tables, loot_context)
|
||||
for _, predicate_table in ipairs(predicate_tables) do
|
||||
if not vl_loot.predicate.check_predicate(predicate_table, loot_context) then
|
||||
return false
|
||||
end
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
vl_loot.predicate.check_predicates = check_predicates
|
Loading…
Reference in New Issue