123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468 |
- local modpath = minetest.get_modpath(minetest.get_current_modname())
- local namegen = {
- _VERSION = 'LuaNameGen - Lua Name Generator v1.2.0',
- _DESCRIPTION = 'A name generator written in Lua for use with Minetest mods',
- _URL = 'https://github.com/FaceDeer/namegen',
- _LICENSE = [[
- MIT LICENSE
- Copyright (c) 2017 Lucas Siqueira, FaceDeer
- Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
- The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
- ]]
- }
- local RangedTable = dofile(modpath .. "/rangedtable.lua")
- -- ======================
- -- CONSTANTS
- -- ======================
- -- set this to true for a debug/verbose mode
- local DEBUG = false
- -- ======================
- -- VARIABLES
- -- ======================
- -- the table containing the generators
- local namegen_generators_list = {}
- -- ======================
- -- UTILITIES
- -- ======================
- local function file_exists(name)
- local f=io.open(name,"r")
- if f~=nil then io.close(f) return true else return false end
- end
- local function get_path(filename)
- if filename:find("%.cfg$") then
- return PATH .. filename
- else
- return PATH .. filename .. ".cfg"
- end
- end
- local function split(str)
- local t = {}
- for v in string.gmatch(str:gsub(",", ""), "%S+") do
- t[#t + 1] = v
- end
- return t
- end
- local function ng_debug(str, ...)
- if DEBUG == true then
- if ... then
- minetest.debug(string.format(str, ...))
- else
- minetest.debug(str)
- end
- end
- end
- -- ======================
- -- PARSERS
- -- ======================
- local function parse_rules(rules)
- local rules_weight = {}
- local rule_pattern = [[^%%*(%d*)(%S+)]]
- for _, v in pairs(rules) do
- local chance, rule = string.match(v, rule_pattern)
- ng_debug([[chance: "%s", rule: "%s"]], chance, rule)
- chance = chance == "" and 100 or tonumber(chance)
- rules_weight[#rules_weight + 1] = {chance, rule}
- end
- ng_debug("rules by weight " .. dump(rules_weight))
- return RangedTable(rules_weight)
- end
- local function parse_property(name, value, parser_data)
- local v = name == "name" and value or split(value)
- if name == "name" then
- ng_debug([[parse_property: "%s"]], value)
- parser_data["name"] = v
- elseif name == "syllablesStart" then
- parser_data["start"] = v
- elseif name == "syllablesMiddle" then
- parser_data["middle"] = v
- elseif name == "syllablesEnd" then
- parser_data["end"] = v
- elseif name =="syllablesPre" then
- parser_data["pre"] = v
- elseif name =="syllablesPost" then
- parser_data["post"] = v
- elseif name == "phonemesVocals" then
- parser_data["vocals"] = v
- elseif name =="phonemesConsonants" then
- parser_data.consonants = v
- elseif name == "rules" then
- parser_data["rules"] = parse_rules(v)
- elseif name == "illegal" then
- -- /* illegal strings are converted to lowercase */
- parser_data.illegal = split(string.lower(value))
- else
- local cg = string.match(name, "^customGroup(%a)")
- if cg then
- parser_data["cg" .. cg:lower()] = v
- else
- ng_debug("parse_property else")
- return false
- end
- end
- return true
- end
- local function parse_lines(lines)
- ng_debug("starting `parse_lines`")
- local data
- local name_pattern = [[name ?"(.+)" ?{]]
- local property_pattern = [[ +(.+) = "(.+)"]]
- local end_body_pattern = [[}]]
- local body = false
- for line in lines do
- local name = string.match(line, name_pattern)
- if name ~= nil then
- namegen_generators_list[name] = {}
- data = namegen_generators_list[name]
- parse_property("name", name, data)
- body = true
- elseif body == true then
- if string.match(line, end_body_pattern) then
- body = false
- else
- local name, value = string.match(line, property_pattern)
- if name and value then
- parse_property(name, value, data)
- end
- end
- end
- end
- ng_debug("ending `parse_lines`")
- end
- -- ======================
- -- WORD VALIDATION
- -- ======================
- -- check for occurrences of triple characters (case-insensitive)
- local function word_has_triples(str)
- local str = str:lower()
- for i = 1, #str - 2 do
- local a = str:sub(i, i)
- local b = str:sub(i+1, i+1)
- local c = str:sub(i+2, i+2)
- if a == b and a == c then
- return true
- end
- end
- return false
- end
- -- check for occurrences of illegal strings (case-insensitive)
- local function word_has_illegal(data, str)
- local str = str:lower()
- if not data.illegal then return false end
- for i = 1, #data.illegal do
- if str:find(data.illegal[i]) then
- return true
- end
- end
- return false
- end
- -- check for repeated syllables (case-insensitive)
- local function word_repeated_syllables(str)
- local word = str:lower():gsub("['%-_]", "")
- for step = 2, math.min(5, math.floor(#str / 2)) do
- for i = 1, #word - step + 1 do
- local search = word:sub(i, i + step - 1)
- local sub = word:sub(i + step, (i + step) + step - 1)
- if search == sub then
- return true
- end
- ng_debug("not repeated", str, step, search, sub)
- end
- end
- return false
- end
- -- verify if the word passes the above checks
- local function word_is_ok(data, str)
- return ((#str > 0) and
- not word_has_triples(str) and
- not word_has_illegal(data, str) and
- not word_repeated_syllables(str))
- end
- -- removes double, leading and ending spaces
- local function word_prune_spaces(str)
- str = str:gsub(" +$", "")
- str = str:gsub("^ +", "")
- str = str:gsub(" +", " ")
- return str
- end
- local function get_lst_from_token(token, data)
- if token == 'P' then
- ng_debug("token case 1")
- return data["pre"]
- elseif token == 's' then
- ng_debug("token case 2")
- return data["start"]
- elseif token == 'm' then
- ng_debug("token case 3")
- return data["middle"]
- elseif token == 'e' then
- ng_debug("token case 4")
- return data["end"]
- elseif token == 'p' then
- ng_debug("token case 5")
- return data["post"]
- elseif token == 'v' then
- ng_debug("token case 6")
- return data["vocals"]
- elseif token == 'c' then
- ng_debug("token case 7")
- return data["consonants"]
- elseif token == '?' then
- ng_debug("token case 8")
- return ((random(1, 2) == 1) and data.vocals or
- data.consonants)
- elseif token >= "A" and token < "P" then
- ng_debug("token case 9")
- return data["cg" .. token:lower()]
- elseif token == "'" then
- ng_debug("token case 10")
- return {"'"}
- end
- end
- local function generate_custom(name, rule)
- local random = math.random
- local data = namegen_generators_list[name]
- if data == nil then
- error(string.format("The name \"%s\" has not been found.\n",name))
- end
- -- start name generation
- local buf, i, it
- repeat
- buf = ""
- i = 1
- while i <= #rule do
- it = rule:sub(i, i)
- -- append normal character
- if ((it >= 'a' and it <= 'z') or
- (it >= 'A' and it <= 'Z') or
- it == '\'' or it == '-')
- then
- ng_debug("buf case 1")
- buf = buf .. it
- elseif it == '/' then
- -- special character
- i = i + 1
- ng_debug("buf case 2")
- buf = buf .. it
- elseif it == '_' then
- -- convert underscore to space
- ng_debug("buf case 3")
- buf = buf .. " "
- -- interpret a wildcard
- elseif it == '$' then
- local chance = 100;
- i = i + 1
- local it = rule:sub(i, i)
- ng_debug("buf case 4, it: %s", it)
- -- food for the randomiser
- if it >= '0' and it <= '9' then
- ng_debug("buf case 4.1")
- chance = 0
- while it >= '0' and it <= '9' do
- chance = chance * 10
- chance = chance + tonumber(it)
- i = i + 1
- it = rule:sub(i, i);
- end
- end
- -- evaluate the wildcard according to its chance
- if chance >= random(100) then
- ng_debug("buf case 4.2")
- local lst = get_lst_from_token(it, data)
- if lst == nil then
- error(string.format(
- [[Wrong rules syntax(it:"%s", rule:"%s")]],
- it, rule))
- end
- -- got the list, now choose something on it
- if #lst == 0 then
- error(string.format(
- "No data found in the requested string (wildcard %s). Check your name generation rule %s.",
- it,rule
- ))
- else
- buf = buf .. (lst[random(1, #lst)]:gsub('_', ' '))
- end
- end
- end
- ng_debug([[i: %d, buf: %s, it: %s, rule: %s]],
- i, buf, it, rule)
- i = i + 1
- end
- until word_is_ok(data, buf)
- ng_debug([[ending "generate_custom 'repeat ... until'"]])
- -- prune undesired spaces and return the name
- return word_prune_spaces(buf)
- end
- -- generate a new name with one of the rules from set
- local function generate(name)
- ng_debug([[starting "generate(%s)"]], name)
- local data = namegen_generators_list[name]
- if data == nil then
- error(string.format("The name \"%s\" has not been found.\n",name))
- end
- -- check if the rules list is present */
- if data.rules:size() == 0 then
- error("The rules list is empty!")
- end
- -- choose the rule */
- local res = generate_custom(name, data.rules:choice())
- ng_debug([[starting "generate(%s)"]], name)
- return res
- end
- local function possible_rules(str)
- local res = {[str] = true}
- while true do
- local changed = false
- local count = #res
- for rule, _ in pairs(res) do
- local chance_rule = rule:match(
- "%$?[%a_'%- ]*(%$%d+[%a_'%- ]+)%$*.*$")
- -- print(rule, chance_rule)
- if chance_rule then
- local a = rule:gsub(chance_rule, "")
- local b = rule:gsub("%$%d+", "$", 1)
- -- print(rule, a, b)
- res[rule] = nil
- res[a] = true
- res[b] = true
- changed = true
- break
- end
- end
- if not changed then break end
- end
- --[[
- print(str)
- for k, _ in pairs(res) do
- print(k)
- end
- ]]--
- return res
- end
- local function exhaust_rules(name)
- local data = namegen_generators_list[name]
- local rules = {}
- for v in data.rules:values() do
- rules[#rules + 1] = v
- end
- local plain_rules = {}
- for _, rule in ipairs(rules) do
- local possible = possible_rules(rule)
- for plain, _ in pairs(possible) do
- plain_rules[#plain_rules + 1] = plain
- end
- end
- return plain_rules
- end
- local function map_all(fcn, tab, idx, ...)
- -- http://stackoverflow.com/a/13059680/5496529
- if idx < 1 then
- fcn(...)
- else
- local t = tab[idx]
- for i = 1, #t do map_all(fcn, tab, idx-1, t[i], ...) end
- end
- end
- local function exhaust_set(name)
- local names = {}
- local data = namegen_generators_list[name]
- local rules = exhaust_rules(name)
- local function combine(...)
- local t = {...}
- local str
- for i, v in ipairs(t) do
- if v ~= nil and v ~= "" then
- str = (str and (str .. v) or v)
- end
- end
- --print(str)
- str = word_prune_spaces(str)
- names[str] = str
- end
- for _, rule in ipairs(rules) do
- local groups = {}
- local tokens = split(rule:gsub("%$", " "))
- for _, token in ipairs(tokens) do
- for c in string.gmatch(token, ".") do
- local lst = (get_lst_from_token(c, data) or
- {[1] = c:gsub("_", " ")})
- if lst == nil then
- error("invalid list", c)
- end
- -- print("\ntoken", token, "c", c, "lst", inspect(lst))
- groups[#groups + 1] = lst
- end
- end
- map_all(combine, groups, #groups)
- end
- return names
- end
- local function get_sets()
- local t = {}
- for k, _ in pairs(namegen_generators_list) do
- if namegen_generators_list[k].rules then
- t[#t + 1] = k
- end
- end
- return t
- end
- -- ------------------------------
- -- publicly available functions
- -- ------------------------------
- namegen.get_sets = get_sets
- namegen.parse_lines = parse_lines
- namegen.generate = generate
- namegen.generate_custom = generate_custom
- namegen.exhaust_set = exhaust_set
- namegen.exhaust_rules = exhaust_rules
- return namegen
|