finally all done

This commit is contained in:
1F616EMO 2023-06-07 18:36:17 +08:00
parent 0290cd0df5
commit 8fad85ca32
No known key found for this signature in database
GPG Key ID: CDF659A4657D3557
7 changed files with 409 additions and 78 deletions

107
README.md
View File

@ -1,4 +1,101 @@
# player_settings
# Player Settings
Allow every players to have their own settings screen and tweak their own settings. Type `/settings` to open the settings GUI.
## Settings in `minetest.conf`
* `player_settings_register_example` (bool): Register example settings
* Default: `false`
* If enabled, register setting examples.
* This is always set to true in singleplayer mode regardless of the settings.
## API
### Register Settings
* `player_settings.register_metacategory(name, def)`: Register a settings meta category
* `name`: The ID of the meta category
* `def`: a [meta category definition table](#metacategories)
* `player_settings.register_category(name, def)`: Register a settings category
* `name`: The ID of the category
* `def`: a [category definition table](#categories)
* `player_settings.register_setting(name, def)`: Register a setting
* `name`: The ID of the setting
* `def`: a [setting definition table](#settings)
* `player_settings.unregister_metacategory(name)`: Unregister a metacategory by its name
* `player_settings.unregister_category(name)`: Unregister a category by its name
* `player_settings.unregister_setting(name)`: Unregister a setting by its name
### Interact with Settings
* `player_settings.register_on_settings_set(name,key,old_value,new_value)`: Register callacks on settings set
* `name`: Name of a player
* `key`: The ID of the setting
* `old_value`: The value being replaced
* `new_value`: The current setting value
* *Not recommended.* To monitor individual settings, set `after_change` in the [setting definition table](#settings).
* `player_settings.set_setting(name,key,value)`: Set the value of a setting for a player
* `name`: Name or [`ObjectRef`][ObjectRef] of a player
* `key`: The ID of the setting
* `value`: The value to be set to the setting
* Return boolean indicating success
* `after_change` and `player_settings.register_on_settings_set` callbacks are called
* If failed, the second returned value may be one of the following:
* `PLAYER_NOT_EXIST`: The player's auth data does not exist
* `KEY_NOT_EXIST`: The setting does not exist
* `TYPE_CONVERT_FAILED`: The given value cannot be converted to the type specified in the setting definition
* `NUMBER_TOO_SMALL` and `NUMBER_TOO_LARGE` (number only): The given value is below or higher than the required range set in the setting definition
* `SETTING_ENUM_TYPE_INVALID` (enum only): The type of enum values set in the settings is invalid
* `SETTING_VALUE_NOT_IN_ENUM` (enum only): The given value is not in the list of allowed options set in the setting definition
* `SETTING_TYPE_INVALID`: The type of the setting specified in the definition is invalid
* `player_settings.set_default(name,key)`: Set the value of a settings back to its default for a player
* `name`: Name or [`ObjectRef`][ObjectRef] of a player
* `key`: The ID of the setting
* Return boolean indicating success
* `after_change` and `player_settings.register_on_settings_set` callbacks are called
* If failed, the second returned value may be one of the following:
* `PLAYER_NOT_EXIST`: The player's auth data does not exist
* `KEY_NOT_EXIST`: The setting does not exist
* `player_settings.get_setting(name,key)`: Get the value of a setting from a player
* `name`: Name or [`ObjectRef`][ObjectRef] of a player
* `key`: The ID of the setting
* Return boolean indicating success
* If success, the second returned value is the setting value
* If failed, the second returned value may be one of the following:
* `PLAYER_NOT_EXIST`: The player's auth data does not exist
* `KEY_NOT_EXIST`: The setting does not exist
#### Internal
These functions are for internal uses. Avoid using them in your code.
* `player_settings.get_settings_path(name)`: Get the file path of the setting file of a player
* `name`: Name of the player
* `player_settings.get_settings(name)`: Get the table of settings of a player
* `name`: Name or [`ObjectRef`][ObjectRef] of a player
* `player_settings.write_settings(name,tb)`: Write a setting table to the setting file of a player
* `name`: Name or [`ObjectRef`][ObjectRef] of a player
* `tb`: Key-value pair of player settings
* `player_settings.erase_settings(name)`: Erase all the settings of a player
* **WARNING: This is irreversable!**
* `name`: Name or [`ObjectRef`][ObjectRef] of a player
* `player_settings.save_all_settings()`: Save all in-cache changes to settings
### Constants
These are the read-only variables.
* `player_settings.registered_metacategories`: All registered metacategories
* Key: The ID of the metacategory
* Value: The [meta category definition table](#metacategories)
* `player_settings.registered_categories`: All registered categories
* Key: The ID of the category
* Value: The [category definition table](#categories)
* `player_settings.registered_settings`: All registered settings
* Key: The ID of the setting
* Value: The [setting definition table](#settings)
* `player_settings.gui`: A [flow](https://gitlab.com/luk3yx/minetest-flow/) GUI object
* Refer to [the `README.md` of flow mod](https://gitlab.com/luk3yx/minetest-flow/-/blob/main/README.md) for further documentations
## Definition tables
@ -66,6 +163,12 @@ Used by `player_settings.register_setting`.
-- Only applies when type == "enum".
-- All the avaliable choices.
display_type = "string" / "enum" / "bool",
-- How the setting is displayed in the GUI.
-- string: For all data types (default of int, float and string)
-- enum: for enum only
-- bool: for bool only
validator = function(name, key, value) end,
-- Validates the input before it is being stored.
-- `name` is the player's name.
@ -88,3 +191,5 @@ Used by `player_settings.register_setting`.
-- If returned false, it will be hidden.
}
```
[ObjectRef]: https://github.com/minetest/minetest/blob/master/doc/lua_api.md#objectref

113
api.lua
View File

@ -3,6 +3,7 @@ minetest.mkdir(WP .. "/player_settings/")
local _ps = player_settings
local s = {}
local s_nochg = {}
local function RTN_TRUE() return true end
@ -25,6 +26,11 @@ _ps.registered_metacategories, _ps.register_metacategory, _ps.unregister_metacat
_ps.registered_categories, _ps.register_category, _ps.unregister_category = do_register()
_ps.registered_settings, _ps.register_setting, _ps.unregister_setting = do_register()
_ps.registered_on_settings_set = {}
_ps.register_on_settings_set = function(func)
table.insert(_ps.registered_on_settings_set,func)
end
_ps.get_settings_path = function(name)
return (WP .. "/player_settings/" .. name .. ".conf.lua")
end
@ -44,19 +50,27 @@ _ps.write_settings = function(name, tb)
if type(name) == "userdata" then
name = name:get_player_name()
end
if not minetest.player_exists(name) then
return false, "PLAYER_NOT_EXIST"
end
minetest.safe_file_write(_ps.get_settings_path(name), minetest.serialize(tb))
return true
end
_ps.set_setting = function(name,key,value)
if type(name) == "userdata" then
name = name:get_player_name()
end
if not minetest.player_exists(name) then
return false, "PLAYER_NOT_EXIST"
end
local setting_entry = _ps.registered_settings[key]
if not setting_entry then
return false, "KEY_NOT_EXIST"
end
if not s[name] then
s[name] = _ps.get_settings(name)
s_nochg[name] = 0
end
-- type = "int" / "string" / "bool" / "float" / "enum",
if setting_entry.type == "int" or setting_entry.type == "float" then
@ -67,6 +81,12 @@ _ps.set_setting = function(name,key,value)
if setting_entry.type == "int" then
value = math.floor(value + 0.5)
end
if setting_entry.number_min and setting_entry.number_min > value then
return false, "NUMBER_TOO_SMALL"
elseif setting_entry.number_max and setting_entry.number_max < value then
return false, "NUMBER_TOO_LARGE"
end
elseif setting_entry.type == "string" then
value = tostring(value)
elseif setting_entry.type == "enum" then
@ -101,7 +121,30 @@ _ps.set_setting = function(name,key,value)
else
return false, "SETTING_TYPE_INVALID"
end
-- validator = function(name, key, value)
if setting_entry.validator then
local status, errmsg = setting_entry.validator(name, key, value)
if not status then
return false, (errmsg or "VALIDATION_FAILED")
end
end
minetest.log("action", string.format(
"[player_settings] %s set setting %s to %s",
name, key, value
))
local old_value = s[name][key]
if setting_entry.default and setting_entry.default == value then
value = nil -- to save disk space
end
s[name][key] = value
s_nochg[name] = nil
-- after_change = function(name, key, old_value, new_value) end,
if setting_entry.after_change then
setting_entry.after_change(name, key, old_value, value or setting_entry.default)
end
for _,func in ipairs(_ps.registered_on_settings_set) do
func(name, key, old_value, value or setting_entry.default)
end
return true
end
@ -109,37 +152,92 @@ _ps.set_default = function(name,key)
if type(name) == "userdata" then
name = name:get_player_name()
end
if not minetest.player_exists(name) then
return false, "PLAYER_NOT_EXIST"
end
local setting_entry = _ps.registered_settings[key]
if not setting_entry then
return false, "KEY_NOT_EXIST"
end
if not s[name] then
s[name] = _ps.get_settings(name)
s_nochg[name] = 0
end
local old_value = s[name][key]
minetest.log("action", string.format(
"[player_settings] %s set setting %s to default",
name, key
))
if setting_entry.after_change then
setting_entry.after_change(name, key, old_value, setting_entry.default)
end
for _,func in ipairs(_ps.registered_on_settings_set) do
func(name, key, old_value, setting_entry.default)
end
s[name][key] = nil
s_nochg[name] = nil
end
_ps.get_setting = function(name,key)
if type(name) == "userdata" then
name = name:get_player_name()
end
if not minetest.player_exists(name) then
return false, "PLAYER_NOT_EXIST"
end
local setting_entry = _ps.registered_settings[key]
if not setting_entry then
return false, "KEY_NOT_EXIST"
end
if not s[name] then
s[name] = _ps.get_settings(name)
s_nochg[name] = 0
end
local value = s[name][key]
if value == nil then
value = setting_entry.default
end
return value
return true, value
end
_ps.erase_settings = function(name)
if type(name) == "userdata" then
name = name:get_player_name()
end
minetest.log("action", string.format(
"[player_settings] Erasing %s's data",
name
))
s[name] = nil
s_nochg[name] = nil
os.remove(_ps.get_settings_path(name))
end
_ps.save_all_settings = function()
for k,v in pairs(s) do
_ps.write_settings(k, v)
if not s_nochg[k] then
minetest.log("action", string.format(
"[player_settings] Writing %s's data",
k
))
_ps.write_settings(k, v)
s_nochg[k] = 1
else
if s_nochg[k] > 2 then
minetest.log("action", string.format(
"[player_settings] %s's data has been idle for 120 seconds. Removing from cache.",
k
))
s[k] = nil -- Free memory
s_nochg[k] = nil
else
minetest.log("action", string.format(
"[player_settings] %s's data has not been edited. Skipped.",
k
))
s_nochg[k] = s_nochg[k] + 1
end
end
end
end
@ -150,3 +248,14 @@ end
minetest.after(5, after_loop)
minetest.register_on_shutdown(_ps.save_all_settings)
do
local old_remove_player = minetest.remove_player
function minetest.remove_player(name)
local success = old_remove_player(name)
if success == 0 then
_ps.erase_settings(name)
end
return success
end
end

View File

@ -40,4 +40,26 @@ _ps.register_setting("ps_example_bool", {
long_description = S("Long description. \nExample of @1","bool"),
default = true,
category = "ps_example",
})
_ps.register_setting("ps_example_enum_string", {
type = "enum",
description = S("Example of @1", "enum (string)"),
long_description = S("Long description. \nExample of @1","enum (string)"),
default = "1F616EMO",
category = "ps_example",
enum_type = "string",
enum_choices = {
"lorem",
"ipsum",
"hello",
"world",
"minetest",
"1F616EMO",
}
})
_ps.register_category("ps_example_empty",{
title = S("Empty Examples"),
metacategory = "ps_example_mc"
})

214
gui.lua
View File

@ -4,6 +4,7 @@ local S = minetest.get_translator("player_settings")
_ps.gui = flow.make_gui(function(player,ctx)
ctx.name = player:get_player_name()
if not ctx.showinfo then ctx.showinfo = {} end
if not ctx.navbarData then
local settings_by_category = {}
for k,v in pairs(_ps.registered_settings) do
@ -30,7 +31,6 @@ _ps.gui = flow.make_gui(function(player,ctx)
end
end
ctx.navbarData = categories_by_metacat
print(dump(ctx.navbarData))
end
local navbar = {}
for k,v in pairs(ctx.navbarData) do
@ -39,7 +39,9 @@ _ps.gui = flow.make_gui(function(player,ctx)
table.insert(navbar,gui.Button {
label = _ps.registered_categories[k2].title,
w = 1, expand = true,
---@diagnostic disable-next-line: redefined-local, unused-local
on_event = function(player,ctx)
if ctx.current_category == k2 then return end
ctx.current_metacat = k
ctx.current_category = k2
return true
@ -61,22 +63,53 @@ _ps.gui = flow.make_gui(function(player,ctx)
local list_settings = ctx.navbarData[ctx.current_metacat][ctx.current_category]
local svbox = {}
for k,v in pairs(list_settings) do
if v.type == "bool" then
local desc = S("@1 (@2)",v.description,_ps.util.types[v.type] or v.type)
if v.type == "enum" then
desc = S("@1 (Multiple-choice of @2)",
v.description,
_ps.util.types[v.enum_type] or v.enum_type)
end
local display_type = v.display_type
if display_type == "enum" and v.type ~= "enum" or
display_type == "bool" and v.type ~= "bool" then
display_type = nil
end
-- TODO: Add support to scrollbar for numbers (waiting for flow mod support)
if not display_type then
if v.type == "bool" then
display_type = "bool"
elseif v.type == "enum" then
display_type = "enum"
else
display_type = "string"
end
end
local s_status, selected = _ps.get_setting(ctx.name,k)
if not s_status then
ctx.errmsg = {k, S("Failed to get setting of @1: @2",
k, _ps.util.errmsgs[selected] or selected
)}
elseif display_type == "bool" then
table.insert(svbox, gui.HBox {
gui.Checkbox {
w = 5,h=1,
name = "settings_" .. k,
label = v.description,
selected = _ps.get_setting(ctx.name,k),
label = desc,
selected = selected,
on_event = function(player,ctx)
ctx.errmsg = nil
local form = ctx.form
if type(form["settings_" .. k]) == "boolean" then
local status, errmsg =_ps.set_setting(ctx.name,k,form["settings_" .. k])
if not status then
print(errmsg)
ctx.errmsg = {k,errmsg}
form["settings_" .. k] = _ps.get_setting(ctx.name,k)
return true
end
return true
end
end,
expand = true, align_h = "left",
@ -85,91 +118,54 @@ _ps.gui = flow.make_gui(function(player,ctx)
w = 1, h = 1,
texture_name = "settings_reset.png",
name = "settingsReset_" .. k,
---@diagnostic disable-next-line: redefined-local, unused-local
on_event = function(player,ctx)
ctx.errmsg = nil
local form = ctx.form
_ps.set_default(ctx.name,k)
form["settings_" .. k] = v.default
return true
end
end,
drawborder = false,
},
gui.Image {
gui.ImageButton {
w = 1, h = 1,
name = "settingsInfo_" .. k,
texture_name = "settings_info.png",
},
gui.Tooltip {
gui_element_name = "settingsInfo_" .. k,
tooltip_text = v.long_description or ""
name = "settingsInfo_" .. k,
---@diagnostic disable-next-line: redefined-local, unused-local
on_event = function(player,ctx)
ctx.showinfo[k] = not ctx.showinfo[k]
return true
end,
drawborder = false,
}
})
elseif v.type == "enum" then
elseif display_type == "enum" then
table.insert(svbox, gui.Label {
label = desc
})
table.insert(svbox, gui.HBox {
gui.Dropdown {
w = 3,h=1,
name = "settings_" .. k,
label = v.short_description,
items = v.enum_choices,
selected = _ps.util.idx_in_table(v.enum_choices,_ps.get_setting(ctx.name,k)),
selected_idx = _ps.util.idx_in_table(v.enum_choices,selected),
},
gui.Button {
w = 2,h=1,
name = "settingsSubmit_" .. k,
label = S("Set"),
on_event = function(player,ctx)
local form = ctx.form
local status, errmsg =_ps.set_setting(ctx.name,k,v.enum_choices[form["settings_" .. k]])
if not status then
print(errmsg)
form["settings_" .. k] = _ps.util.idx_in_table(v.enum_choices,_ps.get_setting(ctx.name,k))
return true
end
end,
expand = true, align_h = "left",
},
gui.ImageButton {
w = 1, h = 1,
texture_name = "settings_reset.png",
name = "settingsReset_" .. k,
on_event = function(player,ctx)
local form = ctx.form
_ps.set_default(ctx.name,k)
form["settings_" .. k] = _ps.util.idx_in_table(v.enum_choices,v.default)
return true
end
},
gui.Image {
w = 1, h = 1,
name = "settingsInfo_" .. k,
texture_name = "settings_info.png",
},
gui.Tooltip {
gui_element_name = "settingsInfo_" .. k,
tooltip_text = v.long_description or ""
}
})
else -- String-like
table.insert(svbox, gui.Label {
label = v.description
})
print(_ps.get_setting(ctx.name,k))
table.insert(svbox, gui.HBox {
gui.Field {
w = 3,h=1,
name = "settings_" .. k,
default = _ps.get_setting(ctx.name,k),
},
gui.Button {
w = 2,h=1,
w = 2, h = 1,
name = "settingsSubmit_" .. k,
label = S("Set"),
---@diagnostic disable-next-line: redefined-local, unused-local
on_event = function(player,ctx)
ctx.errmsg = nil
local form = ctx.form
print(form["settings_" .. k])
local status, errmsg =_ps.set_setting(ctx.name,k,form["settings_" .. k])
if not status then
print(errmsg)
ctx.errmsg = {k,errmsg}
form["settings_" .. k] = _ps.get_setting(ctx.name,k)
return true
end
return true
end,
expand = true, align_h = "left",
},
@ -177,23 +173,95 @@ _ps.gui = flow.make_gui(function(player,ctx)
w = 1, h = 1,
texture_name = "settings_reset.png",
name = "settingsReset_" .. k,
---@diagnostic disable-next-line: redefined-local, unused-local
on_event = function(player,ctx)
ctx.errmsg = nil
local form = ctx.form
_ps.set_default(ctx.name,k)
form["settings_" .. k] = v.default
return true
end
end,
drawborder = false,
},
gui.Image {
gui.ImageButton {
w = 1, h = 1,
name = "settingsInfo_" .. k,
texture_name = "settings_info.png",
},
gui.Tooltip {
gui_element_name = "settingsInfo_" .. k,
tooltip_text = v.long_description or ""
name = "settingsInfo_" .. k,
---@diagnostic disable-next-line: redefined-local, unused-local
on_event = function(player,ctx)
ctx.showinfo[k] = not ctx.showinfo[k]
return true
end,
drawborder = false,
}
})
elseif display_type == "string" then -- String-like
table.insert(svbox, gui.Label {
label = desc
})
table.insert(svbox, gui.HBox {
gui.Field {
w = 3,h=1,
name = "settings_" .. k,
default = selected,
},
gui.Button {
w = 2,h=1,
name = "settingsSubmit_" .. k,
label = S("Set"),
---@diagnostic disable-next-line: redefined-local, unused-local
on_event = function(player,ctx)
ctx.errmsg = nil
local form = ctx.form
local status, errmsg =_ps.set_setting(ctx.name,k,form["settings_" .. k])
if not status then
ctx.errmsg = {k,errmsg}
form["settings_" .. k] = _ps.get_setting(ctx.name,k)
end
return true
end,
expand = true, align_h = "left",
},
gui.ImageButton {
w = 1, h = 1,
texture_name = "settings_reset.png",
name = "settingsReset_" .. k,
---@diagnostic disable-next-line: redefined-local, unused-local
on_event = function(player,ctx)
ctx.errmsg = nil
local form = ctx.form
_ps.set_default(ctx.name,k)
form["settings_" .. k] = v.default
return true
end,
drawborder = false,
},
gui.ImageButton {
w = 1, h = 1,
texture_name = "settings_info.png",
name = "settingsInfo_" .. k,
---@diagnostic disable-next-line: redefined-local, unused-local
on_event = function(player,ctx)
ctx.showinfo[k] = not ctx.showinfo[k]
return true
end,
drawborder = false,
}
})
else
error("[player_settings] Attempt to display setting with unknown display type " .. display_type)
end
if ctx.errmsg and ctx.errmsg[1] == k then
local user_errmsg = _ps.util.errmsgs[ctx.errmsg[2]] or ctx.errmsg[2]
table.insert(svbox, gui.Label {
label = minetest.colorize("#FF0000", S("Error: @1", user_errmsg)),
-- label = ctx.errmsg[2],
})
end
if ctx.showinfo[k] and v.long_description then
table.insert(svbox, gui.Label {
label = minetest.get_color_escape_sequence("#00FF00") .. v.long_description,
})
end
end
svbox.w = 10; svbox.h = 10;

View File

@ -6,4 +6,6 @@ dofile(MP .. "/util.lua")
dofile(MP .. "/api.lua")
dofile(MP .. "/gui.lua")
dofile(MP .. "/example.lua")
if minetest.is_singleplayer() or minetest.settings:get_bool("player_settings_register_example", false) then
dofile(MP .. "/example.lua")
end

3
settingtypes.txt Normal file
View File

@ -0,0 +1,3 @@
# If enabled, register setting examples.
# This is always set to true in singleplayer mode regardless of the settings.
player_settings_register_example (Register example settings) bool false

View File

@ -1,3 +1,4 @@
local S = minetest.get_translator("player_settings")
local _ps = player_settings
_ps.util = {}
@ -6,4 +7,25 @@ _ps.util.idx_in_table = function(tb,v)
if tv == v then return i end
end
return nil
end
end
_ps.util.errmsgs = {
PLAYER_NOT_EXIST = S("Player does not exist"),
KEY_NOT_EXIST = S("Key does not exist"),
TYPE_CONVERT_FAILED = S("Value not matching the data type"),
SETTING_ENUM_TYPE_INVALID = S("Invalid enum data type"),
SETTING_VALUE_NOT_IN_ENUM = S("Value not in list of choices"),
SETTING_TYPE_INVALID = S("Invalid setting data type"),
VALIDATION_FAILED = S("Validation failed"),
NUMBER_TOO_SMALL = S("Number too small"),
NUMBER_TOO_LARGE = S("Number too large"),
}
-- type = "int" / "string" / "bool" / "float" / "enum"
_ps.util.types = {
int = S("Integer"),
string = S("String"),
bool = S("Boolean"),
float = S("Float"),
enum = S("Multiple-choice")
}