Resolved merge conflicts with master

This commit is contained in:
SkylarHew 2022-01-23 16:01:44 -07:00
commit 7cb200ab04
31 changed files with 1525 additions and 291 deletions

View File

@ -1,23 +1,25 @@
# Modpol for Minetest
Modpol, short for "modular politics," is an extension that enables diverse governance processes on multi-user platforms. It offers a library that enables users to create or adapt their own modules that add specific governance functionalities.
Modpol, short for "modular politics," enables diverse governance processes on multi-user platforms. It offers a library with which users can choose, modify, and create modules that add specific governance functionalities.
This implementation is a mod for [Minetest](https://minetest.net), a free/open-source voxel game. It is designed to be easily adapted to other multi-user platforms that also employ Lua as an extension language.
This implementation is a mod for [Minetest](https://minetest.net), a free/open-source voxel game. It is designed to be adapted to other multi-user platforms that also employ Lua as an extension language.
## How to use it
Modpol is built around groups called *orgs*. At the base is an org with all users in it, called `Root` by default.
Modpol is built around groups called *orgs*. At the base is an org with all users in it, called `Root` by default. *Modules* enable people to do things within orgs, such as decide on membership, grant powers to the org, and much more. To get started in Minetest:
*Modules* enable people to do things within orgs, such as decide on membership, grant powers to the org, and much more. Modules can be added and modified by users to meet their needs. Modules can also be nested in each other, so one module can rely on another module to accomplish a process. Within an org, choose the module that you want to use:
* Type the command `/mp`
* Select the org `Root`
* Choose one of its modules to make new orgs and craft their behavior
![](lib/module_list.png)
Modules might simply carry out actions in the game, or they might require a group decision to do so. They might also change the modules available to users of a given org. There are currently two ways of doing this:
Modules can be nested in each other, so one module can rely on another module to accomplish a process. Users might use a module to unilaterally carry out actions in the game, or the module might require a group decision to do so. Users can also change the modules available to users of a given org. There are currently two ways of doing this:
* Remove modules from the list of modules loaded in `modpol_core/api.lua` and `modpol_minetest/api.lua`. This will make those modules no longer available to any user.
* Remove modules for a given org from within the program using the `Change modules` module. The removed modules can be re-added in any org by using `Change modules` again.
* Admins can remove modules from the list of modules loaded in `modpol_core/api.lua` and `modpol_minetest/api.lua`. This will make those modules no longer available to any user.
* Players can change the modules available in a given org from within the program using the `Change modules` module. Removed modules can be re-added in any org by using `Change modules` again.
The point is that Modpol should give you the ability to do whatever kind of politics you want with your modules. If there is something you would like to do that is not available, develop a module for it (or ask us for help!).
Modpol should give you the ability to do whatever kind of politics you want with your modules. If there is something you would like to do that is not available, [develop a module for it](https://gitlab.com/medlabboulder/modpol/-/wikis/Module-Writing-Guide) (or ask us for help!).
## Installation in Minetest
@ -39,24 +41,23 @@ The command-line version is in the `modpol` subdirectory. To run the program on
$ lua[jit] login.lua
```
You can also interact with the interpreter by starting it this way:
Alternatively, to test arbitrary functions in the interpreter outside of the interactive dashboards, load Modpol's library with:
```
$ lua[jit]
> dofile("login.lua")
> dofile("modpol_core/modpol.lua")
```
In the interpreter, for a list of global functions and tables, use `modpol.menu()`.
## Storage
By default, a data directory named "data" will be created in this directory. `/data` will contain a log file and serialized program data files.
The persistent storage method may be chosen in `modpol.lua`. If available, Modpol uses Minetest's built-in StorageRef system for Minetest 5.*. If that is not available, or in CLI mode, data will be stored in a data directory at `modpol_core/data/`. This will contain a log file and serialized program data files.
Another storage method may be chosen in `modpol.lua`. A StorageRef-based method for Minetest 5.* is included: `storage-mod_storage.lua`.
## Design philosophy
Modpol seeks to implement a theoretical framework, also called "[modular politics](https://metagov.org/modpol)," which proposes these design goals:
Modpol seeks to implement a theoretical framework called "[modular politics](https://metagov.org/modpol)," which proposes these design goals:
* *Modularity*: Platform operators and community members should have the ability to construct systems by creating, importing, and arranging composable parts together as a coherent whole.
* *Expressiveness*: The governance layer should be able to implement as wide a range of processes as possible.

View File

@ -1,7 +1,5 @@
dofile("modpol_core/modpol.lua")
modpol.orgs.reset()
print("Log in as which user?")
local username = io.read()

View File

@ -1,7 +1,7 @@
name = modpol
title = Modpol
author = ntnsndr <n@nathanschneider.info>
description = Framework that enables diverse governance processes
description = A framework that enables diverse governance processes
license = MIT
forum = https://forum.minetest.net/viewtopic.php?f=9&t=27598
version = 0.1

View File

@ -5,6 +5,7 @@ local localdir = modpol.topdir
--orgs
dofile (localdir .. "/orgs/base.lua")
dofile (localdir .. "/orgs/process.lua")
dofile (localdir .. "/orgs/users.lua")
--interactions
dofile (localdir .. "/interactions/interactions.lua")
@ -14,11 +15,18 @@ dofile (localdir .. "/interactions/interactions.lua")
dofile (localdir .. "/modules/add_child_org_consent.lua")
dofile (localdir .. "/modules/change_modules.lua")
dofile (localdir .. "/modules/consent.lua")
dofile (localdir .. "/modules/create_token.lua")
dofile (localdir .. "/modules/defer_consent.lua")
dofile (localdir .. "/modules/display_processes.lua")
dofile (localdir .. "/modules/join_org_consent.lua")
dofile (localdir .. "/modules/leave_org.lua")
dofile (localdir .. "/modules/message_org.lua")
dofile (localdir .. "/modules/randomizer.lua")
dofile (localdir .. "/modules/remove_child_consent.lua")
dofile (localdir .. "/modules/remove_member_consent.lua")
dofile (localdir .. "/modules/remove_org_consent.lua")
dofile (localdir .. "/modules/remove_org.lua")
dofile (localdir .. "/modules/remove_process.lua")
dofile (localdir .. "/modules/rename_org_consent.lua")
dofile (localdir .. "/modules/send_token.lua")
dofile (localdir .. "/modules/tokenomics.lua")

View File

@ -38,8 +38,10 @@ function modpol.interactions.dashboard(user)
local user_pending_count = 0
for k,v in ipairs(modpol.orgs.array) do
if v.pending and v.pending[user] then
table.insert(user_pending, v.name)
user_pending_count = user_pending_count + 1
if modpol.util.num_pairs(v.pending[user]) ~= 0 then
table.insert(user_pending, v.name)
user_pending_count = user_pending_count + 1
end
end
end
@ -48,18 +50,53 @@ function modpol.interactions.dashboard(user)
print('All users: ' .. table.concat(all_users, ', '))
print()
print('Access which org id?')
print("Commands: (O)rg, (U)ser, (R)eset, (Q)uit")
local sel = io.read()
print()
if modpol.orgs.array[tonumber(sel)] then
local sel_org = modpol.orgs.array[tonumber(sel)].name
modpol.interactions.org_dashboard(user, sel_org)
else
print("Org id not found.")
end
if sel == "O" or sel == "o" then
print('Access which org id?')
sel = io.read()
print()
if modpol.orgs.array[tonumber(sel)] then
local sel_org = modpol.orgs.array[tonumber(sel)].name
modpol.interactions.org_dashboard(user, sel_org)
else
print("Org id not found")
modpol.interactions.dashboard(user)
end
elseif sel == "U" or sel == "u" then
print("Access which user?")
sel = io.read()
print()
if modpol.instance:has_member(sel) then
modpol.interactions.user_dashboard(
user, sel,
function()
modpol.interactions.dashboard(user)
end
)
else
print("User name not found")
modpol.interactions.dashboard(user)
end
elseif sel == "R" or sel == "r" then
modpol.instance.members = {}
modpol.orgs.reset()
print("Orgs and users reset")
modpol.interactions.dashboard(user)
elseif sel == "Q" or "q" then
return
else
print("Invalid input, try again")
modpol.interactions.dashboard(user)
end
end
--- Output: Displays a menu of org-specific commands to the user
-- @function modpol.interactions.org_dashboard
-- @param user (string)
@ -109,7 +146,7 @@ function modpol.interactions.org_dashboard(user, org_string)
print("Org: " .. org.name)
print("Parent: " .. parent)
print("Members: " .. table.concat(org.members, ", "))
print("Children: " .. table.concat(children, ", "))
print("Child orgs: " .. table.concat(children, ", "))
print("Modules: " .. table.concat(modules, ", "))
print("Pending: " .. process_msg)
print()
@ -132,25 +169,33 @@ function modpol.interactions.org_dashboard(user, org_string)
org:call_module(module_sel, user)
else
print("Error: Module not found.")
modpol.interactions.org_dashboard(user, org.id)
end
elseif sel == 'p' or sel == 'P' then
local processes = {}
print("All processes: (* indicates pending)")
for i,v in ipairs(org.processes) do
local active = ''
if org.pending[user] then
if org.pending[user][v.id] then
active = '*'
if v ~= "deleted" then
local active = ''
if org.pending[user] then
if org.pending[user][v.id] then
active = '*'
end
end
print("["..v.id.."] "..v.slug..active)
end
print("["..v.id.."] "..v.slug..active)
end
print()
print("Interact with which one (use [id] number)?")
local to_interact = io.read()
local process = org.processes[tonumber(to_interact)]
if not process then return end
if not process then
modpol.interactions.message(
user, "Not a pending process")
modpol.interactions.org_dashboard(user, org.id)
return
end
if org:has_pending_actions(user) then
if org.pending[user][process.id] then
org:interact(process.id, user)
@ -164,13 +209,46 @@ function modpol.interactions.org_dashboard(user, org_string)
end
end
-- Function: modpol.interactions.policy_dashboard
-- input: user (string), org_id (int), policy (string)
-- if policy is nil, enables creating a new policy
-- output: opens a dashboard for viewing/editing policy details
-- TODO
--- Function: modpol.interactions.user_dashboard
-- Displays a dashboard about a particular user
-- @param viewer Name of user viewing the dashboard (string)
-- @param user Name of user being viewed (string)
-- @param completion Optional function to call on Done button
function modpol.interactions.user_dashboard(viewer, user, completion)
local user_orgs = {}
local user_modules = {}
--- Output: Prints message to CLI
print("\n-=< USER DASHBOARD: "..user.." >=-")
print("User's orgs:")
for id, org in ipairs(modpol.orgs.array) do
if type(org) == "table" then
if org:has_member(user) then
print(org.name)
end
end
end
print()
print("Commands: (M)essage user, Enter when done")
local sel = io.read()
if sel == "M" or sel == "m" then
modpol.interactions.message_user(
viewer, user)
completion()
else
completion()
end
end
-- INTERACTION PRIMITIVES
-- ======================
--- Prints message to CLI.
-- Buttons: message, done
-- @function modpol.interactions.message
-- @param user (string)
-- @param message (string)
@ -178,11 +256,56 @@ function modpol.interactions.message(user, message)
print(user .. ": " .. message)
end
--- Output: Applies "func" to user input.
--- Function: modpol.interactions.message_user
-- Gets and sends a message from one user to another
-- @param sender Name of user sending (string)
-- @param recipient Name of user receiving (string)
function modpol.interactions.message_user(sender, recipient)
print("Enter your message for "..recipient..":")
local sel = io.read()
modpol.interactions.message(
recipient,
sel.." [from "..sender.."]")
end
--- Function: modpol.interactions.display
-- Displays complex data to a user
-- @param user Name of target user (string)
-- @param title Title of display (string)
-- @param message Content of message (string or table of strings)
-- @param done Optional function for what happens when user is done
function modpol.interactions.display(user, title, message, completion)
local output = ""
output = "\n-=< "..title.." >=-\n\n"
if type(message) == "table" then
output = table.concat(message,"\n")
elseif type(message) == "string" then
output = message
elseif type(message) == "number" then
output = message
else
modpol.interactions.message(
self.initiator, "Error: message not typed for display")
modpol.interactions.message(
self.initiator, "Error: input not typed for display")
if completion then completion() else
modpol.intereactions.dashboard(user)
end
end
print(message)
print("\nEnter to continue")
io.read()
if completion then completion() else
modpol.intereactions.dashboard(user)
end
end
-- Applies "func" to user input
-- Func input: user input (string)
-- @function modpol.interactions.text_query
-- @param user (string)
-- @param query (string)
-- @param User (string)
-- @param Query (string)
-- @param func (function)
function modpol.interactions.text_query(user, query, func)
print(user .. ": " .. query)
@ -197,8 +320,6 @@ end
-- @param label (string)
-- @param options (table of strings)
-- @param func (choice) (function)
function modpol.interactions.dropdown_query(user, label, options, func)
-- set up options
local options_display = ""
@ -211,7 +332,7 @@ function modpol.interactions.dropdown_query(user, label, options, func)
options_display = options_display .. "Select number:"
if options_number == 0 then
print("Error: No options given for dropdown")
return nil
return nil
end
-- begin displaying
print(user .. ": " .. label)
@ -234,12 +355,60 @@ function modpol.interactions.dropdown_query(user, label, options, func)
end
end
--- Output: Applies "func" to user input.
-- Func input: user input (string: y/n)
-- @function modpol.binary_poll_user(user, question)
-- @param user (string)
-- @param question (string)
-- @param func (function)
--- Allows user to select from a set of options
-- @function modpol.interactions.checkbox_query
-- @param user Name of user (string)
-- @param label Query for user before options (string)
-- @param options Table of options and their checked status in the form {{"option_1_string", true}, {"option_2_string", false}}
-- @param func Function to be called with param "input", made up of the corrected table in the same format as the param options
function modpol.interactions.checkbox_query(
user, label, options, func)
-- set up options
local options_display = ""
local options_number = 0
for i,v in ipairs(options) do
local checked = false
if v[2] then checked = true end
if checked then
checked = "x"
else
checked = " "
end
options_display = options_display..i..". ["..
checked.."] "..v[1].."\n"
options_number = options_number + 1
end
if options_number == 0 then
print("Error: No options given for dropdown")
return nil
end
options_display = options_display..
"List comma-separated options to flip (e.g., 1,2,5):"
-- begin displaying
print(user .. ": " .. label)
print(options_display)
-- read input and produce output
local answer = io.read()
local answer_table = {}
for match in (answer..","):gmatch("(.-)"..",") do
table.insert(answer_table, tonumber(match))
end
local result_table = modpol.util.copy_table(options)
for i,v in ipairs(answer_table) do
if result_table[v] then
-- flip the boolean on selected options
result_table[v][2] = not result_table[v][2]
end
end
func(result_table)
end
-- Function: modpol.interactions.binary_poll_user
-- Params: user (string), question (string), func (function)
-- func input: user input (string: y/n)
-- Output: Applies "func" to user input
function modpol.interactions.binary_poll_user(user, question, func)
local query = "Poll for " .. user .. " (y/n): ".. question
local answer
@ -280,3 +449,9 @@ end
-- output: gets question from initiator, asks all org members, broadcasts answers
-- TESTING
--testing command
function modpol.msg(text)
modpol.interactions.message("TEST MSG",text)
end

View File

@ -28,7 +28,6 @@ function add_child_org_consent:initiate(result)
modpol.interactions.org_dashboard(
self.initiator, self.org.name)
self.org:delete_process(self.id)
if result then result() end
return
elseif modpol.orgs.get_org(input) then
modpol.interactions.message(
@ -45,7 +44,7 @@ function add_child_org_consent:initiate(result)
self.initiator,
"Proposed child org: " .. input)
-- initiate consent process
self.org:call_module(
self:call_module(
"consent",
self.initiator,
{
@ -70,7 +69,8 @@ function add_child_org_consent:create_child_org()
modpol.interactions.message_org(
self.initiator,
self.org.name,
"Child org created: "..self.data.child_name)
"Consent reached: created child org "
..self.data.child_name)
if self.data.result then self.data.result() end
self.org:delete_process(self.id)
end

View File

@ -0,0 +1,169 @@
--- change_modules
-- @module change_modules
-- Depends on consent
local change_modules = {
name = "Change modules (consent)",
slug = "change_modules",
desc = "Add or remove modules from the org with member consent",
hide = false;
}
change_modules.data = {
result = nil
}
change_modules.config = {
}
function change_modules:initiate(result)
self.data.result = result
-- Step 1: add or remove?
modpol.interactions.dropdown_query(
self.initiator, "Module change options:",
{"Add module","Remove module"},
function(input)
if input == "Add module" then
self:add_module()
elseif input == "Remove module" then
self:remove_module()
end
end
)
end
function change_modules:add_module()
-- prepare module options
local available_modules = modpol.util.copy_table(modpol.modules)
for k,org_mod in pairs(self.org.modules) do
if available_modules[org_mod.slug] then
available_modules[org_mod.slug] = nil
end end
-- present module options
local modules_list = {}
for k,v in pairs(available_modules) do
table.insert(modules_list,v.name)
end
if #modules_list == 0 then
modpol.interactions.message(
self.initiator, "Org has all modules")
modpol.interactions.org_dashboard(
self.initiator, self.org.id)
if self.data.result then self.data.result() end
self.org:delete_process(self.id)
return
end
table.sort(modules_list)
-- now ask which to add
modpol.interactions.dropdown_query(
self.initiator, "Choose a module to add:",
modules_list,
function(mod_choice)
-- confirm choice
modpol.interactions.binary_poll_user(
self.initiator,
"Confirm: propose to add module \"" ..
mod_choice .. "\"?",
function(input)
if input == "Yes" then
self:propose_change("add",mod_choice)
modpol.interactions.org_dashboard(
self.initiator, self.org.id)
else
self:add_module()
end
end
)
end
)
end
function change_modules:remove_module()
-- prepare module options
local available_modules = {}
for k,org_mod in pairs(self.org.modules) do
if not org_mod.hide then
available_modules[org_mod.slug] = modpol.util.copy_table(org_mod)
end end
local modules_list = {}
local modules_count = 0
for k,v in pairs(available_modules) do
table.insert(modules_list,v.name)
modules_count = modules_count + 1
end
-- abort if no modules to remove
if modules_count == 0 then
modpol.interactions.message(
self.initiator, "Org has no modules")
modpol.interactions.org_dashboard(
self.initiator, self.org.id)
if self.data.result then self.data.result() end
self.org:delete_process(self.id)
return
end
table.sort(modules_list)
-- now ask which to remove
modpol.interactions.dropdown_query(
self.initiator, "Choose a module to remove:",
modules_list,
function(mod_choice)
-- confirm choice
modpol.interactions.binary_poll_user(
self.initiator,
"Confirm: propose to remove module \"" .. mod_choice .. "\"?",
function(input)
if input == "Yes" then
self:propose_change("remove",mod_choice)
modpol.interactions.org_dashboard(
self.initiator, self.org.id)
else
self:remove_module()
end
end
)
end
)
end
--- propose_change
-- @field type "add" or "remove"
function change_modules:propose_change(type, mod_text)
self:call_module(
"consent",self.initiator,
{
prompt = "Do you consent to "..type..
" this module in org "..self.org.name..
":\n"..mod_text,
votes_required = #self.org.members
},
function()
if type == "add" then
for k,v in pairs(modpol.modules) do
if v.name == mod_text then
table.insert(self.org.modules,v)
end
end
modpol.interactions.message_org(
self.initiator,self.org.id,
"Consent reached:\nAdding \""
..mod_text.."\" to org "..self.org.name)
elseif type == "remove" then
local i = 0
for k,v in pairs(self.org.modules) do
i = i + 1
if v.name == mod_text then
self.org.modules[k] = nil
end
end
modpol.interactions.message_org(
self.initiator,self.org.id,
"Consent reached:\nRemoving \""
..mod_text.."\" from org "..self.org.name)
end
end)
if self.data.result then self.data.result() end
self.org:delete_process(self.id)
end
--- (Required) Add to module table
modpol.modules.change_modules = change_modules

View File

@ -10,7 +10,10 @@ local change_modules = {
}
change_modules.data = {
result = nil
result = nil,
modules_before = {},
modules_after = {},
summary = "",
}
change_modules.config = {
@ -21,151 +24,93 @@ change_modules.config = {
-- @function change_modules:initiate
function change_modules:initiate(result)
self.data.result = result
-- Step 1: add or remove?
modpol.interactions.dropdown_query(
self.initiator, "Module change options:",
{"Add module","Remove module"},
self.data.add_modules = {}
self.data.remove_modules = {}
local modules_before = {}
local modules_after = {}
-- generate self.config.modules table
for k, module in pairs(modpol.modules) do
if not modpol.modules[module.slug].hide then
local in_org = false
if self.org.modules[module.slug] then
in_org = true
end
table.insert(
modules_before,
{module.name.." ["..module.slug.."]", in_org})
end
end
-- send query to user
modpol.interactions.checkbox_query(
self.initiator,
"Check the modules to activate in this org:",
modules_before,
function(input)
if input == "Add module" then
self:add_module()
elseif input == "Remove module" then
self:remove_module()
end
end
)
end
function change_modules:add_module()
-- prepare module options
local available_modules = modpol.util.copy_table(modpol.modules)
for k,org_mod in pairs(self.org.modules) do
if available_modules[org_mod.slug] then
available_modules[org_mod.slug] = nil
end end
-- present module options
local modules_list = {}
for k,v in pairs(available_modules) do
table.insert(modules_list,v.name)
end
if #modules_list == 0 then
modpol.interactions.message(
self.initiator, "Org has all modules")
modpol.interactions.org_dashboard(
self.initiator, self.org.id)
if self.data.result then self.data.result() end
self.org:delete_process(self.id)
return
end
table.sort(modules_list)
-- now ask which to add
modpol.interactions.dropdown_query(
self.initiator, "Choose a module to add:",
modules_list,
function(mod_choice)
-- confirm choice
modpol.interactions.binary_poll_user(
self.initiator,
"Confirm: propose to add module \"" ..
mod_choice .. "\"?",
function(input)
if input == "Yes" then
self:propose_change("add",mod_choice)
modpol.interactions.org_dashboard(
self.initiator, self.org.id)
-- identify changes
modules_after = input
for i,v in ipairs(modules_after) do
if v[2] ~= modules_before[i][2] then
if v[2] then
table.insert(self.data.add_modules, v[1])
else
self:add_module()
table.insert(self.data.remove_modules, v[1])
end
end
)
end
)
end
function change_modules:remove_module()
-- prepare module options
local available_modules = {}
for k,org_mod in pairs(self.org.modules) do
if not org_mod.hide then
available_modules[org_mod.slug] = modpol.copy_table(org_mod)
end end
local modules_list = {}
local modules_count = 0
for k,v in pairs(available_modules) do
table.insert(modules_list,v.name)
modules_count = modules_count + 1
end
-- abort if no modules to remove
if modules_count == 0 then
modpol.interactions.message(
self.initiator, "Org has no modules")
modpol.interactions.org_dashboard(
self.initiator, self.org.id)
if self.data.result then self.data.result() end
self.org:delete_process(self.id)
return
end
table.sort(modules_list)
-- now ask which to remove
modpol.interactions.dropdown_query(
self.initiator, "Choose a module to remove:",
modules_list,
function(mod_choice)
-- confirm choice
modpol.interactions.binary_poll_user(
self.initiator,
"Confirm: propose to remove module \"" .. mod_choice .. "\"?",
function(input)
if input == "Yes" then
self:propose_change("remove",mod_choice)
modpol.interactions.org_dashboard(
self.initiator, self.org.id)
else
self:remove_module()
end
end
)
end
)
end
--- propose_change
-- @field type "add" or "remove"
function change_modules:propose_change(type, mod_text)
self.org:call_module(
"consent",self.initiator,
{
prompt = "Do you consent to "..type..
" this module in org "..self.org.name..
":\n"..mod_text,
votes_required = #self.org.members
},
function()
if type == "add" then
for k,v in pairs(modpol.modules) do
if v.name == mod_text then
table.insert(self.org.modules,v)
end
end
modpol.interactions.message_org(
self.initiator,self.org.id,
"Consent reached:\nAdding \""
..mod_text.."\" to org "..self.org.name)
elseif type == "remove" then
modpol.msg("Removing!")
local i = 0
for k,v in pairs(self.org.modules) do
i = i + 1
if v.name == mod_text then
modpol.msg("got it!")
self.org.modules[k] = nil
end
end
modpol.interactions.message_org(
self.initiator,self.org.id,
"Consent reached:\nRemoving \""
..mod_text.."\" from org "..self.org.name)
end
-- abort if no changes
if #self.data.add_modules == 0
and #self.data.remove_modules == 0 then
modpol.interactions.message(
self.initiator, "No module changes proposed")
modpol.interactions.org_dashboard(
self.initiator, self.org.id)
self.org:delete_process(self.id)
return
end
-- proceed with consent
local query = "Accept module changes in org "..
self.org.name.."?"
self.data.summary = ""
if #self.data.add_modules > 0 then
self.data.summary = self.data.summary.."\nAdd: "..
table.concat(self.data.add_modules,", ")
elseif #self.data.remove_modules > 0 then
self.data.summary = "\nRemove: "..
table.concat(self.data.remove_modules,", ")
end
self:call_module(
"consent",
self.initiator,
{
prompt = query..self.data.summary,
votes_required = #self.org.members
},
function()
self:implement_change()
end)
modpol.interactions.org_dashboard(
self.initiator, self.org.id)
end)
end
function change_modules:implement_change()
for i,v in ipairs(self.data.add_modules) do
local slug = string.match(v,"%[(.+)%]")
self.org.modules[slug] =
modpol.util.copy_table(modpol.modules[slug])
table.sort(self.org.modules)
end
for i,v in ipairs(self.data.remove_modules) do
local slug = string.match(v,"%[(.+)%]")
self.org.modules[slug] = nil
table.sort(self.org.modules)
end
-- announce and shut down
modpol.interactions.message_org(
self.initiator,
self.org.id,
"Module changes applied to org "..self.org.name..":"..
self.data.summary)
if self.data.result then self.data.result() end
self.org:delete_process(self.id)
end

View File

@ -2,9 +2,9 @@
-- @module consent
local consent = {
name = "Consent process utility",
name = "Consent process",
slug = "consent",
desc = "A module other modules use for consent decisions",
desc = "A utility module other modules use for consent decisions",
hide = true
}
@ -26,7 +26,7 @@ function consent:initiate(result)
if self.org:get_member_count() == 0 then
if self.data.result then
self.data.result() end
self.org:wipe_pending_actions(self.id)
self.org:delete_process(self.id)
else
-- otherwise, create poll
for id, member in pairs(self.org.members) do
@ -47,14 +47,19 @@ function consent:callback(member)
if resp == "Yes" then
self.data.votes = self.data.votes + 1
end
modpol.interactions.message_org(
"consent", self.org.id,
member.." decided "..resp.." on: "..
self.config.prompt.." ("..self.data.votes..
"/"..self.config.votes_required..")"
)
if self.data.votes >= self.config.votes_required then
if self.data.result then
self.data.result() end
self.org:wipe_pending_actions(self.id)
self.org:delete_process(self.id)
end
modpol.interactions.org_dashboard(
member, self.org.name)
member, self.org.id)
end
)
end

View File

@ -0,0 +1,53 @@
--- create_token
-- @module create_token
-- depends on tokenomics
local create_token = {
name = "Create a token (consent)",
slug = "create_token",
desc = "With org consent, creates an org token",
hide = false;
}
--- (Required) Data for module
-- Variables that module uses during the course of a process
-- Can be blank
create_token.data = {
}
create_token.config = {
token_name = ""
}
--- (Required): initiate function
-- @param result (optional) Callback if this module is embedded in other modules
-- @function initiate
function create_token:initiate(result)
modpol.interactions.text_query(
self.initiator,
"Token name (alpha-numeric, no spaces):",
function(input)
self.config.token_name = input
self:call_module(
"tokenomics",
self.initiator,
{
consent = true,
token_slug = self.config.token_name
},
function(input2)
modpol.interactions.org_dashboard(
self.initiator, self.org.name)
if result then result() end
-- call this wherever process might end:
self.org:delete_process(self.id)
end
)
modpol.interactions.org_dashboard(
self.initiator, self.org.name)
end
)
end
--- (Required) Add to module table
modpol.modules.create_token = create_token

View File

@ -0,0 +1,57 @@
--- defer_consent
-- @module defer_consent
--- (Required): data table containing name and description of the module
-- @field name "Human-readable name (parens OK, no brackets)"
-- @field slug "Same as module class name"
-- @field desc "Description of the module"
-- @field hide "Whether this is a hidden utility module"
local defer_consent = {
name = "Defer consent",
slug = "defer_consent",
desc = "Defers consent on a decision to another org",
hide = true;
}
--- (Required) Data for module
-- Variables that module uses during the course of a process
-- Can be blank
defer_consent.data = {
}
--- (Required): config for module
-- @field defer_org Name or ID of target org
-- @field votes_required Threshold passed on to `consent`
-- @field prompt String passed on to `consent`
defer_consent.config = {
defer_org = "Root",
votes_required = 1,
prompt = "Do you consent?"
}
--- (Required): initiate function
-- @param result (optional) Callback if this module is embedded in other modules
-- @function initiate
function defer_consent:initiate(result)
local defer_org = modpol.orgs.get_org(self.config.defer_org)
if not defer_org then
modpol.interactions.message(
self.initiator, "Target org not found, aborting")
self.org:delete_process(self.id)
else
defer_org:call_module(
"consent", self.initiator,
{
votes_required = self.config.votes_required,
prompt = self.config.prompt
},
function()
if result then result() end
end)
end
if result then result() end
self.org:delete_process(self.id)
end
--- (Required) Add to module table
modpol.modules.defer_consent = defer_consent

View File

@ -0,0 +1,70 @@
--- display_processes
-- @module display_processes
local display_processes = {
name = "Display processes",
slug = "display_processes",
desc = "Presents a detailed list of org processes",
hide = false;
}
--- (Required) Data for module
-- Variables that module uses during the course of a process
-- Can be blank
display_processes.data = {
}
display_processes.config = {
}
--- (Required): initiate function
-- @param result (optional) Callback if this module is embedded in other modules
-- @function initiate
function display_processes:initiate(result)
local display_table = {}
for k,v in pairs(self.org.processes) do
if v ~= "deleted" then
local input = v.id..": "..v.slug
table.insert(display_table, input)
input = "Org: "..v.org.name..
", initiator: "..v.initiator
table.insert(display_table, input)
if v.config
and modpol.util.num_pairs(v.config) > 0 then
table.insert(display_table, "Policies:")
for k2,v2 in pairs(v.config) do
local v2_string = ""
if type(v2) ~= "string"
and type(v2) ~= "table" then
v2_string = tostring(v2)
elseif type(v2) == "table" then
v2_string = tostring(v2)
else
v2_string = "Could not render"
end
input = k2..": "..v2_string
table.insert(display_table, input)
end
end
table.insert(display_table, "\n")
end
end
local output = table.concat(display_table,"\n")
if #display_table == 0 then
output = "No processes found"
end
modpol.interactions.display(
self.initiator,
"Processes in org "..self.org.name,
output,
function()
modpol.interactions.org_dashboard(
self.initiator, self.org.id)
if result then result() end
self.org:delete_process(self.id)
end
)
end
--- (Required) Add to module table
modpol.modules.display_processes = display_processes

View File

@ -28,7 +28,7 @@ function join_org_consent:initiate(result)
self.org:delete_process(self.id)
else
self.data.result = result
self.org:call_module(
self:call_module(
"consent",
self.initiator,
{

View File

@ -0,0 +1,49 @@
--- @module randomizer
-- A utility module that outputs a random result from a set of options
local randomizer = {
name = "Randomizer",
slug = "randomizer",
desc = "A utility module other modules use for random decisions",
hide = true
}
randomizer.data = {
}
-- options_table should be a table of strings
randomizer.config = {
options_table = {},
num_results = 1,
result_table = {}
}
function randomizer:initiate(result)
self.data.result = result
self.data.options_table = modpol.util.copy_table(self.config.options_table)
-- if options table is empty, randomizer returns that
if #self.data.options_table == 0 or self.config.num_results == 0 then
if self.data.result then
self.data.result({}) end
self.org:delete_process(self.id)
else
-- otherwise, choose a random result
self.random_loop()
end
end
-- returns result_table
function randomizer:random_loop()
self.data.results = 0
if results == self.config.num_results then
self.data.result(self.data.result_table)
else
math.randomseed(os.time())
local index = math.random(self.data.options_table)
table.insert(self.data.result_table, self.data.options_table[index])
table.remove(self.data.options_table, index)
self.data.results = self.data.results + 1
end
end
modpol.modules.randomizer = randomizer

View File

@ -42,7 +42,7 @@ function remove_child_consent:initiate(result)
children,
function(input)
self.data.child_to_remove = modpol.orgs.get_org(input)
self.org:call_module(
self:call_module(
"consent",
self.initiator,
{

View File

@ -35,7 +35,7 @@ function remove_member_consent:initiate(result)
self.org.members,
function(input)
self.data.member_to_remove = input
self.org:call_module(
self:call_module(
"consent",
self.initiator,
{

View File

@ -28,14 +28,14 @@ function remove_org_consent:initiate(result)
self.org:delete_process(self.id)
else
self.data.result = result
self.org:call_module(
self:call_module(
"consent",
self.initiator,
{
prompt = "Remove org " .. self.org.name .. "?",
votes_required = #self.org.members
},
function ()
function()
self:complete()
end
)

View File

@ -0,0 +1,113 @@
--- remove_process
-- @module remove_process
local remove_process = {
name = "Remove process",
slug = "remove_process",
desc = "User can remove own processes, consent required for those of others",
hide = false;
}
--- (Required) Data for module
-- Variables that module uses during the course of a process
-- Can be blank
remove_process.data = {
}
remove_process.config = {
}
--- (Required): initiate function
-- @param result (optional) Callback if this module is embedded in other modules
-- @function initiate
function remove_process:initiate(result)
-- prepare process options
local available_processes = {}
for k,process in pairs(self.org.processes) do
if process ~= "deleted" then
available_processes[process.id] = process
end
end
local process_list = {}
local process_count = 0
for k,v in pairs(available_processes) do
local mine = ""
if v.initiator == self.initiator then mine = "*" end
table.insert(process_list,"["..v.id.."] "..v.slug..mine)
process_count = process_count + 1
end
-- abort if no processes to remove
if process_count == 0 then
modpol.interactions.message(
self.initiator, "Org has no modules")
modpol.interactions.org_dashboard(
self.initiator, self.org.id)
if result then result() end
self.org:delete_process(self.id)
return
end
table.sort(process_list)
-- now ask which to remove
modpol.interactions.dropdown_query(
self.initiator, "Choose a process to remove (* marks yours, no consent required):",
process_list,
function(process_choice)
-- confirm choice
local process_id = tonumber(
string.match(process_choice, "%d+"))
local process_mine = string.match(process_choice,
"%*")
modpol.interactions.binary_poll_user(
self.initiator,
"Confirm: Remove process \""..
process_choice .. "\"?",
function(input)
if input == "Yes" then
if process_mine then
self.org:delete_process_tree(process_id)
modpol.interactions.message(
self.initiator,
"Removed process: "..process_choice)
modpol.interactions.org_dashboard(
self.initiator, self.org.id)
if result then result() end
self.org:delete_process(self.id)
else
self:call_module(
"consent",
self.initiator,
{
prompt = "Approve removal of process "..process_choice.."?",
votes_required = #self.org.members
},
function(input)
modpol.interactions.message_org(
self.initiator,
self.org.id,
"Removing process: "..
process_choice)
self.org:delete_process_tree(process_id)
modpol.interactions.org_dashboard(
self.initiator, self.org.id)
if result then result() end
self.org:delete_process(self.id)
end
)
end
modpol.interactions.org_dashboard(
self.initiator, self.org.id)
else
modpol.interactions.org_dashboard(
self.initiator, self.org.id)
if result then result() end
self.org:delete_process(self.id)
end
end
)
end
)
end
--- (Required) Add to module table
modpol.modules.remove_process = remove_process

View File

@ -49,7 +49,7 @@ function rename_org_consent:initiate(result)
"Proposed to change name of org " ..
self.org.name .. " to " .. input)
-- initiate consent process
self.org:call_module(
self:call_module(
"consent",
self.initiator,
{

View File

@ -0,0 +1,77 @@
--- send_token
-- @module send_token
-- depends on tokenomics
local send_token = {
name = "Send tokens",
slug = "send_token",
desc = "Send tokens to another user",
hide = false;
}
--- (Required) Data for module
-- Variables that module uses during the course of a process
-- Can be blank
send_token.data = {
}
send_token.config = {
token_name = ""
}
--- (Required): initiate function
-- @param result (optional) Callback if this module is embedded in other modules
-- @function initiate
function send_token:initiate(result)
local token_list = {}
if self.org.tokens then
for k,v in pairs(self.org.tokens) do
table.insert(token_list, k)
end
end
if token_list == {} then
modpol.interactions.message(
self.initiator,
"No tokens in org")
modpol.interactions.org_dashboard(
self.initiator, self.org.name)
self.org:delete_process(self.id)
return
else
modpol.interactions.dropdown_query(
self.initiator,
"Which token do you want to send?",
token_list,
function(input_token)
modpol.interactions.dropdown_query(
self.initiator,
"Who do you want to send to?",
modpol.util.copy_table(self.org.members),
function(input_recipient)
modpol.interactions.text_query(
self.initiator,
"How much do you want to give (a number)?",
function(input_amount)
modpol.modules.tokenomics.transfer(
self.org.id,
input_token,
self.initiator,
input_recipient,
input_amount
)
modpol.interactions.org_dashboard(
self.initiator, self.org.name)
-- close process
if result then result() end
self.org:delete_process(self.id)
end
)
end
)
end
)
end
end
--- (Required) Add to module table
modpol.modules.send_token = send_token

View File

@ -43,10 +43,13 @@ function module_template:initiate(result)
-- call interaction functions here!
-- concluding functions:
-- call these wherever process might end;
-- first, where appropriate, return users to dashboards.
-- second, result:
-- may need to put result in self.data.result
-- if process ends in another function
-- call this when module is successful (not for abort):
if result then result() end
-- third, delete the process
-- call this wherever process might end:
self.org:delete_process(self.id)
end

View File

@ -0,0 +1,213 @@
--- tokenomics
-- @module tokenomics
-- Depends on consent
local tokenomics = {
name = "Tokenomics",
slug = "tokenomics",
desc = "A simple library for token economics",
hide = true;
}
--- (Required) Data for module
-- Variables that module uses during the course of a process
-- Can be blank
tokenomics.data = {
result = nil
}
--- (Required): config for module
-- @field consent Require consent to create?
-- @field token_variables the data that goes into the token
-- @field token_slug A no-spaces slug for the token
-- @field initial_treasury Quantity in org treasury
-- @field negative_spend Boolean: can users spend negative tokens? (for mutual credit)
-- @field balances Table of user balances
tokenomics.config = {
consent = false,
token_slug = "token",
token_variables = {
treasury = 0,
negative_spend = true,
balances = {}
}
}
--- (Required): initiate function: creates a token in an org
-- set up the token data structure
-- create an org treasury
-- @param result (optional) Callback if this module is embedded in other modules
-- @function initiate
function tokenomics:initiate(result)
-- TODO need to create a series of interactions to get the info from users
self.data.result = result
if self.org.tokens and self.org.tokens[slug] then
modpol.interactions.message(
self.initiator, "Token slug taken, aborting")
self.org:delete_process(self.id)
else
if self.config.consent then
self:call_module(
"consent",
self.initiator,
{
prompt = "Create token "..
self.config.token_slug.."?",
votes_required = #self.org.members
},
function()
modpol.interactions.message_org(
self.initiator, self.org.id,
"Consent reached: creating token "..
self.config.token_slug)
self:create_token()
end
)
else
self:create_token()
end
end
end
function tokenomics:create_token()
if not self.org.tokens then self.org.tokens = {} end
self.org.tokens[self.config.token_slug] =
self.config.token_variables
self.org:record("Created token "..self.config.token_slug,
self.slug)
modpol.interactions.message_org(
self.initiator, self.org.id,
"Created token "..self.config.token_slug)
if self.data.result then self.data.result() end
-- call this wherever process might end:
self.org:delete_process(self.id)
end
-----------------------------------------
-- Utility functions
-- all need to account for the fact that some users may not yet have balances
-- all need to write to persistent data
-- amount can be positive or negative (except transfer)
-- returns balance
-- if no user, get treasury balance
-- @param org Name (string) or id (num)
-- @param token Slug (string)
-- @param user Name (string)
function tokenomics.balance(org, token, user)
local this_org = modpol.orgs.get_org(org)
if not this_org[token] then return nil, "Token not found" end
if not user then
return this_org[token].treasury
end
if not this_org[user] then
return nil, "User not found"
else
local user_balance = this_org[token].balances[user]
if user_balance then
return user_balance
else
return 0
end
end
end
-- @param org Org name (string) or id (number)
-- @param token Token slug (string)
function tokenomics.change_balance(org, token, user, amount)
local this_org = modpol.orgs.get_org(org)
if not this_org then
return nil, "Cannot change balance: Org not found"
elseif not this_org.tokens then
return nil, "Cannot change balance: no tokens found"
elseif not this_org.tokens[token] then
return nil, "Cannot change balance: token not found"
elseif not this_org.members[user] then
return nil, "Cannot change balance: user not in org"
elseif not tonumber(amount) then
return nil, "Cannot change balance: invalid amount"
else
local old_balance = this_org.tokens[token].balances[user]
if not old_balance then old_balance = 0 end
local new_balance = old_balance + amount
this_org.tokens[token].balances[user] = new_balance
local msg = "Your balance of token "..token..
" changed from "..old_balance.." to "..new_balance
modpol.interactions.message(user, msg)
self.org:record(
"Changed token balance for "..user,self.slug)
end
end
-- @param amount Positive number
function tokenomics.transfer(org, token, sender, recipient, amount)
local sender_balance = tokenomics.balance(org, token, sender)
local recipient_balance = tokenomics.balance(org, token, recipient)
if not sender_balance or recipient_balance then
return nil, "Transfer failed, user not found"
else
sender_balance = sender_balance - amount
recipient_balance = recipient_balance + amount
local neg_spend = modpol.orgs.get_org(org).tokens[token].negative_spend
if sender_balance < 0 and not neg_spend then
return nil, "Transfer failed, negative spend not allowed"
else
tokenomics.change_balance(
org, token, sender, sender_balance)
tokenomics.change_balance(
org, token, recipient, recipient_balance)
return "Transfer complete"
end
end
end
-- @param amount Can be positive or negative, assumes flow from treasury to recipient
function tokenomics.treasury_transfer(org, token, recipient, amount)
local this_org = modpol.orgs.get_org(org)
if not this_org then
return nil, "Cannot transfer treasury: Org not found"
elseif not this_org.tokens then
return nil, "Cannot transfer treasury: no tokens found"
elseif not this_org.tokens[token] then
return nil, "Cannot transfer treasury: token not found"
elseif not this_org.members[user] then
return nil, "Cannot transfer treasury: user not in org"
elseif not tonumber(amount) then
return nil, "Cannot change balance: invalid amount"
else
local new_treasury = this_org.tokens[token].treasury - amount
local new_recipient_balance = tokenomics.balance(org, token, recipient) + amount
if new_treasury < 0 and not this_org.tokens[token].negative_spend then
return nil, "Transfer failed, negative spend not allowed"
elseif new_recipient_balance < 0 and not this_org.tokens[token].negative_spend then
return nil, "Transfer failed, negative spend not allowed"
else
this_org.tokens[token].treasury = new_treasury
self.org:record("Treasury payment",self.slug)
tokenomics.change_balance(org, token, recipient, amount)
end
end
end
-- creates new tokens in the org treasury
function tokenomics.issue(org, token, amount)
local this_org = modpol.orgs.get_org(org)
if not this_org then
return nil, "Cannot transfer treasury: Org not found"
elseif not this_org.tokens then
return nil, "Cannot transfer treasury: no tokens found"
elseif not this_org.tokens[token] then
return nil, "Cannot transfer treasury: token not found"
elseif not tonumber(amount) then
return nil, "Cannot change balance: invalid amount"
else
this_org.tokens[token].treasury =
this_org.tokens[token].treasury + amount
self.org:record("Issued tokes to treasury","tokenomics")
end
end
------------------------------------------
--- (Required) Add to module table
modpol.modules.tokenomics = tokenomics

View File

@ -76,6 +76,8 @@ end
--- Deletes all orgs except for the
-- @function modpol.orgs.reset
function modpol.orgs.reset()
local instance_members =
modpol.util.copy_table(modpol.instance.members)
for id, org in ipairs(modpol.orgs.array) do
if id > 1 then
modpol.orgs.array[id] = "removed"
@ -84,9 +86,9 @@ function modpol.orgs.reset()
modpol.orgs.array[1] = nil
modpol.instance = modpol.orgs.init_instance()
modpol.instance.members = instance_members
modpol.ocutil.log('Reset all orgs')
modpol.ocutil.log('All orgs reset')
modpol.orgs:record('Resetting all orgs', 'org_reset')
end

View File

@ -7,33 +7,18 @@
-- @param intiator Initiator for module
-- @param config Config for module
-- @param result
function modpol.orgs:call_module(module_slug, initiator, config, result)
function modpol.orgs:call_module(module_slug, initiator, config, result, parent_id)
if not modpol.modules[module_slug] then
modpol.ocutil.log('Error in ' .. self.name .. ':call_module -> module "' .. module_slug .. '" not found')
return
end
local empty_index = nil
-- linear search for empty process slots (lazy deletion)
for k, v in ipairs(self.processes) do
if v == 'deleted' then
empty_index = k
break
end
end
local index
-- attempts to fill empty spots in list, otherwise appends to end
if empty_index then
index = empty_index
else
index = #self.processes + 1
end
local index = #self.processes + 1
local module = modpol.modules[module_slug]
-- sets default values for undeclared config variables
if #module.config > 0 then
if modpol.util.num_pairs(module.config) > 0 and config then
for k, v in pairs(module.config) do
if config[k] == nil then
config[k] = v
@ -47,24 +32,61 @@ function modpol.orgs:call_module(module_slug, initiator, config, result)
initiator = initiator,
org = self,
id = index,
parent_id = parent_id,
children = {},
config = config,
data = module.data,
data = modpol.util.copy_table(module.data),
slug = module_slug
}
-- call module wrapper for modules, passes its id to child process when called
function new_process:call_module(module_slug, initiator, config, result)
local child_id = self.org:call_module(module_slug, initiator, config, result, self.id)
table.insert(self.children, child_id)
end
setmetatable(new_process, new_process.metatable)
self.processes[index] = new_process
self.processes[index]:initiate(result)
local msg = "Initiating "..module_slug..
" process id "..index.." in org "..self.name
return index
end
--- Delete process by id
-- @function modpol.orgs:delete_process
-- @param id Id of process
function modpol.orgs:delete_process(id)
self.processes[id] = 'deleted'
function modpol.orgs:get_root_process(id)
local process = self.processes[id]
while (process.parent_id) do
process = self.processes[process.parent_id]
end
return process
end
function modpol.orgs:delete_process(id)
local process = self.processes[id]
if process and process ~= "deleted" then
-- recursively deletes any children
if #process.children > 0 then
for i, child_id in pairs(process.children) do
self:delete_process(child_id)
end
end
local msg = "Deleting " .. self.processes[id].slug .. " process id "..id.." in org "..self.name
modpol.ocutil.log(msg)
self:record(msg, self.processes[id].slug)
self:wipe_pending_actions(id)
-- sets process to 'deleted' in process table
self.processes[id] = 'deleted'
end
end
--- Delete process tree by id
-- @function modpol.orgs:delete_process_tree
-- @param id Id of process tree
function modpol.orgs:delete_process_tree(id)
self:delete_process(self:get_root_process(id).id)
end
--- Add a new pending action
@ -124,8 +146,15 @@ function modpol.orgs:interact(process_id, user)
local process = self.processes[process_id]
if self.pending[user] then
local callback = self.pending[user][process_id]
if callback then
if callback and process ~= "deleted" then
-- get data in case callback ends process
local slug = self.processes[process_id].slug
-- run callback
process[callback](process, user)
-- record org data
local msg = "Updating "..slug..
" process id "..process_id.." in org "..self.name
self:record(msg, slug)
end
end
end

View File

@ -0,0 +1,27 @@
-- /users.lua
-- User-related functions for Modular Politics
-- ===================================================================
-- Function: modpol.list_users
-- Params: org
-- Outputs: Table of user names
--
-- This may be overwritten by the platform-specific interface
modpol.list_users = function(org)
local users = {}
if (org == nil) then -- no specified org; all players
if modpol.orgs["instance"]
and modpol.orgs["instance"]["members"] then
-- if instance exists and has membership
users = modpol.orgs["instance"]["members"]
else
users = {}
end
else -- if an org is specified
if (modpol.orgs[org] ~= nil) then -- org exists
users = modpol.orgs[org]["members"]
end
end
return users
end

View File

@ -9,8 +9,14 @@ modpol.util = {}
-- @return copy of table
function modpol.util.copy_table(t)
local t2 = {}
for k,v in pairs(t) do
t2[k] = v
if pairs(t) then
for k,v in pairs(t) do
if type(v) == "table" then
t2[k] = modpol.util.copy_table(v)
else
t2[k] = v
end
end
end
return t2
end

View File

@ -9,11 +9,7 @@ local localdir = minetest.get_modpath("modpol") .. "/modpol_minetest"
--overrides
dofile (localdir .. "/overrides/interactions.lua")
--testing command for "singleplayer"
function modpol.msg(text)
modpol.interactions.message("singleplayer",text)
end
dofile (localdir .. "/overrides/users.lua")
-- ===================================================================
-- Minetest Chatcommands

View File

@ -34,8 +34,8 @@ regchat(
"mptest", {
privs = {privs=true},
func = function(user)
modpol.instance.members = modpol.list_users()
modpol.orgs.reset()
modpol.instance:add_member(user)
modpol.interactions.dashboard(user)
return true, "Reset orgs"
end,

View File

@ -20,7 +20,7 @@ priv_to_org.config = {
function priv_to_org:initiate(result)
local player_privs = minetest.get_player_privs(self.initiator)
-- construct table for display
local player_privs_table = {"View..."}
local player_privs_table = {}
for k,v in pairs(player_privs) do
if player_privs[k] then
table.insert(player_privs_table,k)

View File

@ -49,13 +49,14 @@ function modpol.interactions.dashboard(user)
local user_orgs = modpol.orgs.user_orgs(user)
local all_users = modpol.instance:list_members()
-- pending list
local user_pending = {"View..."}
local user_pending_count = 0
local user_pending = {}
for k,v in ipairs(modpol.orgs.array) do
if v.pending and v.pending[user] then
modpol.msg(v.name)
table.insert(user_pending, v.name)
user_pending_count = user_pending_count + 1
if modpol.util.num_pairs(v.pending[user]) ~= 0 then
table.insert(user_pending, v.name)
user_pending_count = user_pending_count + 1
end
end
end
@ -63,15 +64,15 @@ function modpol.interactions.dashboard(user)
local formspec = {
"formspec_version[4]",
"size[10,8]",
"label[0.5,0.5;M O D P O L]",
"hypertext[0.5,0.5;9,1;title;<big>Org dashboard</big>]",
"label[0.5,2;All orgs:]",
"dropdown[2,1.5;7,0.8;all_orgs;"..formspec_list(all_orgs)..";;]",
"dropdown[2.5,1.5;7,0.8;all_orgs;View...,"..formspec_list(all_orgs)..";;]",
"label[0.5,3;Your orgs:]",
"dropdown[2,2.5;7,0.8;user_orgs;"..formspec_list(user_orgs)..";;]",
"dropdown[2.5,2.5;7,0.8;user_orgs;View...,"..formspec_list(user_orgs)..";;]",
"label[0.5,4;All users:]",
"dropdown[2,3.5;7,0.8;all_users;"..formspec_list(all_users)..";;]",
"dropdown[2.5,3.5;7,0.8;all_users;View...,"..formspec_list(all_users)..";;]",
"label[0.5,5;Pending ("..user_pending_count.."):]",
"dropdown[2,4.5;7,0.8;pending;"..formspec_list(user_pending)..";;]",
"dropdown[2.5,4.5;7,0.8;pending;View...,"..formspec_list(user_pending)..";;]",
"button[0.5,7;1,0.8;refresh;Refresh]",
"button_exit[8.5,7;1,0.8;close;Close]",
}
@ -88,10 +89,22 @@ minetest.register_on_player_receive_fields(function (player, formname, fields)
minetest.close_formspec(pname, formname)
elseif fields.refresh then
modpol.interactions.dashboard(pname)
-- Put all dropdowns at the end
-- Put all dropdowns at the end
elseif fields.all_users
and fields.all_users ~= "View..." then
modpol.interactions.user_dashboard(
pname,
fields.all_users,
function()
modpol.interactions.dashboard(pname)
end
)
elseif fields.all_orgs or fields.user_orgs or fields.pending then
local org_name = fields.all_orgs or fields.user_orgs or fields.pending
modpol.interactions.org_dashboard(pname, org_name)
if org_name ~= "View..." then
modpol.interactions.org_dashboard(
pname, org_name)
end
end
end
end)
@ -114,12 +127,15 @@ function modpol.interactions.org_dashboard(user, org_string)
end
return ""
end
-- identify parent
local parent = modpol.orgs.get_org(org.parent)
if parent then parent = parent.name
else parent = "none" end
-- prepare members menu
local members = org.members
-- prepare children menu
local children = {}
for k,v in ipairs(org.children) do
@ -127,7 +143,6 @@ function modpol.interactions.org_dashboard(user, org_string)
table.insert(children, this_child.name)
end
table.sort(children)
table.insert(children,1,"View...")
-- prepare modules menu
local modules = {}
@ -140,21 +155,21 @@ function modpol.interactions.org_dashboard(user, org_string)
end
end
table.sort(modules)
table.insert(modules,1,"View...")
-- prepare pending menu
local pending = {}
local num_pending = 0
if org.pending[user] then
for k,v in pairs(org.pending[user]) do
local pending_string = org.processes[k].name
.." ["..k.."]"
table.insert(pending, pending_string)
num_pending = num_pending + 1
if org.processes[k] ~= "deleted" then
local pending_string = org.processes[k].name
.." ["..k.."]"
table.insert(pending, pending_string)
num_pending = num_pending + 1
end
end
end
table.sort(pending)
table.insert(pending,1,"View...")
-- set player context
local user_context = {}
@ -164,17 +179,17 @@ function modpol.interactions.org_dashboard(user, org_string)
local formspec = {
"formspec_version[4]",
"size[10,8]",
"label[0.5,0.5;Org: "..
minetest.formspec_escape(org.name)..membership_toggle(org.name).."]",
"label[0.5,1;Parent: "..parent..membership_toggle(parent).."]",
"hypertext[0.5,0.5;9,1;title;<big>Org: <b>"..
minetest.formspec_escape(org.name).."</b>"..membership_toggle(org.name).."</big>]",
"label[0.5,1.25;Parent: "..parent..membership_toggle(parent).."]",
"label[0.5,2;Members:]",
"dropdown[2,1.5;7,0.8;user_orgs;"..formspec_list(org.members)..";;]",
"label[0.5,3;Children:]",
"dropdown[2,2.5;7,0.8;children;"..formspec_list(children)..";;]",
"dropdown[2.5,1.5;7,0.8;members;View...,"..formspec_list(members)..";;]",
"label[0.5,3;Child orgs:]",
"dropdown[2.5,2.5;7,0.8;children;View...,"..formspec_list(children)..";;]",
"label[0.5,4;Modules:]",
"dropdown[2,3.5;7,0.8;modules;"..formspec_list(modules)..";;]",
"dropdown[2.5,3.5;7,0.8;modules;View...,"..formspec_list(modules)..";;]",
"label[0.5,5;Pending ("..num_pending.."):]",
"dropdown[2,4.5;7,0.8;pending;"..formspec_list(pending)..";;]",
"dropdown[2.5,4.5;7,0.8;pending;View...,"..formspec_list(pending)..";;]",
"button[0.5,7;1,0.8;refresh;Refresh]",
"button[8.5,7;1,0.8;back;Back]",
}
@ -197,10 +212,20 @@ minetest.register_on_player_receive_fields(function (player, formname, fields)
elseif fields.back then
modpol.interactions.dashboard(pname)
elseif fields.refresh then
modpol.interactions.org_dashboard(pname,org.name)
modpol.interactions.org_dashboard(pname, org.name)
-- Put all dropdowns at the end
-- Receiving modules
-- Receiving modules
elseif fields.members
and fields.members ~= "View..." then
modpol.interactions.user_dashboard(
pname,
fields.members,
function()
modpol.interactions.org_dashboard(
pname, org.name)
end
)
elseif fields.modules
and fields.modules ~= "View..." then
local module = nil
@ -217,10 +242,13 @@ minetest.register_on_player_receive_fields(function (player, formname, fields)
function(input)
if input == "Yes" then
org:call_module(module.slug, pname)
elseif input == "No" then
modpol.interactions.org_dashboard(
pname, org.id)
end
end)
end
-- Receiving pending
elseif fields.pending
and fields.pending ~= "View..." then
@ -240,23 +268,66 @@ minetest.register_on_player_receive_fields(function (player, formname, fields)
end
end)
--- Function: modpol.interactions.user_dashboard
-- Displays a dashboard about a particular user
-- @param viewer Name of user viewing the dashboard (string)
-- @param user Name of user being viewed (string)
-- @param completion Optional function to call on Done button
function modpol.interactions.user_dashboard(viewer, user, completion)
local user_orgs = modpol.orgs.user_orgs(user)
-- Function: modpol.interactions.policy_dashboard
-- input: user (string), org_id (int), policy (string)
-- output: opens a dashboard for viewing/editing policy details
-- TODO
function modpol.interactions.policy_dashboard(
user, org_id, policy)
modpol.interactions.message(
user,
"Not yet implemented: " .. policy)
-- set player context
local user_context = {}
user_context["viewer"] = viewer
user_context["user"] = user
user_context["completion"] = completion
_contexts[viewer] = user_context
-- set up formspec
local formspec = {
"formspec_version[4]",
"size[10,8]",
"hypertext[0.5,0.5;9,1;title;<big>User: <b>"..user.."</b></big>]",
"label[0.5,2;User's orgs:]",
"dropdown[2.5,1.5;7,0.8;user_orgs;View...,"..formspec_list(user_orgs)..";;]",
"button[0.5,7;1.5,0.8;message;Message]",
"button_exit[8.5,7;1,0.8;close;Close]",
}
local formspec_string = table.concat(formspec, "")
-- present to player
minetest.show_formspec(viewer, "modpol:user_dashboard", formspec_string)
end
-- receive input
minetest.register_on_player_receive_fields(function (player, formname, fields)
if formname == "modpol:user_dashboard" then
local contexts = _contexts[player:get_player_name()]
-- check fields
if nil then
elseif fields.message then
modpol.interactions.message_user(
contexts.viewer, contexts.user
)
elseif fields.back then
if contexts.completion then
completion()
else
modpol.interactions.dashboard(
contexts.viewer)
end
-- dropdown fields
elseif fields.user_orgs
and fields.user_orgs ~= "View..." then
modpol.interactions.org_dashboard(
contexts.viewer, fields.user_orgs)
end
end
end)
-- INTERACTION FUNCTIONS
-- =====================
-- INTERACTION PRIMITIVES
-- ======================
-- Function: modpol.interactions.message
-- Produces a brief message to a user
-- input: user (string), message (string)
-- output: displays message to specified user
function modpol.interactions.message(user, message)
@ -265,6 +336,72 @@ function modpol.interactions.message(user, message)
end
end
--- Function: modpol.interactions.message_user
-- Gets and sends a message from one user to another
-- @param sender Name of user sending (string)
-- @param recipient Name of user receiving (string)
function modpol.interactions.message_user(sender, recipient)
modpol.interactions.text_query(
sender,
"Message for "..recipient..":",
function(input)
modpol.interactions.message(
recipient,
input.." [from "..sender.."]")
end
)
end
--- Function: modpol.interactions.display
-- Displays complex data to a user
-- @param user Name of target user (string)
-- @param title Title of display (string)
-- @param message Content of message (string or table of strings)
-- @param completion Optional function for what happens when user is done
function modpol.interactions.display(
user, title, message, completion)
-- set up contexts
_contexts[user]["completion"] = completion
-- set up output
local output = ""
if type(message) == "table" then
output = table.concat(message,"\n")
elseif type(message) == "string" then
output = message
elseif type(message) == "number" then
output = message
else
modpol.interactions.message(
self.initiator, "Error: message not typed for display")
if completion then completion() else
modpol.intereactions.dashboard(user)
end
return
end
-- set up formspec
local formspec = {
"formspec_version[4]",
"size[10,8]",
"label[0.5,0.5;"..title.."]",
"hypertext[0.5,1;9,5.5;display;<global background=black margin=10>"..output.."]",
"button_exit[8.5,7;1,0.8;done;Done]",
}
local formspec_string = table.concat(formspec, "")
-- present to player
minetest.show_formspec(user, "modpol:display", formspec_string)
end
-- receive fields
minetest.register_on_player_receive_fields(function (player, formname, fields)
local pname = player:get_player_name()
if formname == "modpol:display" then
if fields.done and _contexts[pname].completion then
minetest.close_formspec(pname, formname)
_contexts[pname].completion()
end
end
end)
-- Function: modpol.interactions.text_query
-- Overrides function at modpol/interactions.lua
-- input: user (string), query (string), func (function)
@ -275,7 +412,7 @@ function modpol.interactions.text_query(user, query, func)
"formspec_version[4]",
"size[10,4]",
"label[0.5,1;", minetest.formspec_escape(query), "]",
"field[0.5,1.25;9,0.8;input;;]",
"field[0.5,1.25;9,0.8;input;;]",
"button[0.5,2.5;1,0.8;yes;OK]",
}
local formspec_string = table.concat(formspec, "")
@ -309,14 +446,12 @@ end)
-- func input: choice (string)
-- output: calls func on user choice
function modpol.interactions.dropdown_query(user, label, options, func)
-- Add "View..." to the top of the list
table.insert(options,1,"View...")
-- set up formspec
local formspec = {
"formspec_version[4]",
"size[10,4]",
"label[0.5,1;"..minetest.formspec_escape(label).."]",
"dropdown[0.5,1.25;9,0.8;input;"..formspec_list(options)..";;]",
"dropdown[0.5,1.25;9,0.8;input;View...,"..formspec_list(options)..";;]",
"button[0.5,2.5;1,0.8;cancel;Cancel]",
}
local formspec_string = table.concat(formspec, "")
@ -329,7 +464,7 @@ end
-- receive fields
minetest.register_on_player_receive_fields(function (player, formname, fields)
if formname == "modpol:dropdown_query" then
local pname = player:get_player_name()
local pname = player:get_player_name()
if fields.cancel then
minetest.close_formspec(pname, formname)
elseif fields.input == "View..." then
@ -351,6 +486,82 @@ minetest.register_on_player_receive_fields(function (player, formname, fields)
end
end)
--- Function: modpol.interactions.checkbox_query
-- Allows user to select from a set of options
-- @param user Name of user (string)
-- @param label Query for user before options (string)
-- @param options table of options and their checked status in the form {{"option_1_string", true}, {"option_2_string", false}}
-- @param func function to be called with param "input", made up of the corrected table in the same format as the param options
function modpol.interactions.checkbox_query(
user, label, options, func)
-- set up formspec
-- prepare options
local vertical = 0
local checkbox_options = {}
for i,v in ipairs(options) do
local fs_line = ""
vertical = i * .5
fs_line = "checkbox[0,"..vertical..";checkbox_"..i..";"..
minetest.formspec_escape(v[1])..";"..
tostring(v[2]).."]"
table.insert(checkbox_options, fs_line)
end
local max = vertical * 4
local bar_height = vertical / 2
local formspec = {
"formspec_version[4]",
"size[10,8]",
"label[0.5,0.5;"..label.."]",
"scrollbaroptions[arrows=default;max="..max..";smallstep=10;largestep=100;thumbsize="..bar_height.."]",
"scrollbar[9,1;0.3,5.5;vertical;scroller;0]",
"scroll_container[0.5,1;9,5.5;scroller;vertical]",
}
-- insert options
for i,v in ipairs(checkbox_options) do
table.insert(formspec, v)
end
table.insert(formspec,"scroll_container_end[]")
table.insert(formspec,"button[0.5,7;1.5,0.8;submit;Submit]")
table.insert(
formspec,"button_exit[8,7;1.5,0.8;cancel;Cancel]")
local formspec_string = table.concat(formspec, "")
-- present to players
minetest.show_formspec(user, "modpol:checkbox_query", formspec_string)
-- put func in _contexts
if _contexts[user] == nil then _contexts[user] = {} end
_contexts[user]["checkbox_query_func"] = func
_contexts[user]["checkbox_query_result"] =
modpol.util.copy_table(options)
end
-- receive fields
minetest.register_on_player_receive_fields(function (player, formname, fields)
if formname == "modpol:checkbox_query" then
local pname = player:get_player_name()
-- start checking fields
if fields.cancel then
minetest.close_formspec(pname, formname)
elseif fields.submit then
-- send in result
minetest.close_formspec(pname, formname)
_contexts[pname].checkbox_query_func(
_contexts[pname].checkbox_query_result)
else
for k,v in pairs(fields) do
-- identify checkbox actions and flip bool
if string.find(k,"checkbox_") then
local index = tonumber(
string.match(k,"%d+"))
_contexts[pname].checkbox_query_result[index][2] =
not _contexts[pname].checkbox_query_result[index][2]
end
end
end
end
end)
-- Function: modpol.binary_poll_user(user, question, function)
-- Overrides function at modpol/interactions.lua
-- Params: user (string), question (string), func (function)
@ -388,3 +599,10 @@ minetest.register_on_player_receive_fields(function (player, formname, fields)
minetest.close_formspec(pname, formname)
end
end)
-- TESTING
--testing command for "singleplayer"
function modpol.msg(text)
modpol.interactions.message("singleplayer",text)
end

View File

@ -0,0 +1,20 @@
-- ===================================================================
--- Overwrites function at /users.lua.
-- if nil, lists instance members; if an org name, lists its members
-- @function modpol.list_users
-- @param org
-- @return a table with names of players currently in the game
modpol.list_users = function(org)
local users = {}
if (org == nil) then -- no specified org; all players
for _,player in ipairs(minetest.get_connected_players()) do
local name = player:get_player_name()
table.insert(users,name)
end
else -- if an org is specified
if (modpol.orgs[org] ~= nil) then -- org exists
users = modpol.orgs[org]["members"]
end
end
return users
end