From fa5c63cfc815ade8859c2cf552f4f1a25b4845ae Mon Sep 17 00:00:00 2001 From: sfan5 Date: Thu, 24 Dec 2020 16:22:08 +0100 Subject: [PATCH] Rewrite colors.txt generation script for more functions and better usability --- autogenerating-colors.txt | 132 -------------------------- util/dumpnodes/init.lua | 61 +++++++++++++ util/dumpnodes/mod.conf | 2 + util/generate_colorstxt.py | 183 +++++++++++++++++++++++++++++++++++++ 4 files changed, 246 insertions(+), 132 deletions(-) delete mode 100644 autogenerating-colors.txt create mode 100644 util/dumpnodes/init.lua create mode 100644 util/dumpnodes/mod.conf create mode 100755 util/generate_colorstxt.py diff --git a/autogenerating-colors.txt b/autogenerating-colors.txt deleted file mode 100644 index 6fb60c0..0000000 --- a/autogenerating-colors.txt +++ /dev/null @@ -1,132 +0,0 @@ -==FILE== mods/dumpnodes/init.lua -local function nd_get_tiles(nd) - return nd.tiles or nd.tile_images -end - -local function nd_get_tile(nd, n) - local tile = nd_get_tiles(nd)[n] - if type(tile) == 'table' then - tile = tile.name - end - return tile -end - -local function pairs_s(dict) - local keys = {} - for k in pairs(dict) do - table.insert(keys, k) - end - table.sort(keys) - return ipairs(keys) -end - -minetest.register_chatcommand("dumpnodes", { - params = "", - description = "", - func = function(player, param) - local n = 0 - local ntbl = {} - for _, nn in pairs_s(minetest.registered_nodes) do - local nd = minetest.registered_nodes[nn] - local prefix, name = nn:match('(.*):(.*)') - if prefix == nil or name == nil then - print("ignored(1): " .. nn) - else - if ntbl[prefix] == nil then - ntbl[prefix] = {} - end - ntbl[prefix][name] = true - end - end - local out, err = io.open('nodes.txt', 'wb') - if not out then - return true, "io.open(): " .. err - end - for _, prefix in pairs_s(ntbl) do - out:write('# ' .. prefix .. '\n') - for _, name in pairs_s(ntbl[prefix]) do - local nn = prefix .. ":" .. name - local nd = minetest.registered_nodes[nn] - if nd.drawtype == 'airlike' or nd_get_tiles(nd) == nil then - print("ignored(2): " .. nn) - else - local tl = nd_get_tile(nd, 1) - tl = (tl .. '^'):match('(.-)^') -- strip modifiers - out:write(nn .. ' ' .. tl .. '\n') - n = n + 1 - end - end - out:write('\n') - end - out:close() - return true, n .. " nodes dumped." - end, -}) -==FILE== avgcolor.py -#!/usr/bin/env python -import sys -from math import sqrt -from PIL import Image - -if len(sys.argv) < 2: - print("Prints average color (RGB) of input image") - print("Usage: %s " % sys.argv[0]) - exit(1) - -inp = Image.open(sys.argv[1]).convert('RGBA') -ind = inp.load() - -cl = ([], [], []) -for x in range(inp.size[0]): - for y in range(inp.size[1]): - px = ind[x, y] - if px[3] < 128: continue # alpha - cl[0].append(px[0]**2) - cl[1].append(px[1]**2) - cl[2].append(px[2]**2) - -if len(cl[0]) == 0: - print("Didn't find average color for %s" % sys.argv[1], file=sys.stderr) - print("0 0 0") -else: - cl = tuple(sqrt(sum(x)/len(x)) for x in cl) - print("%d %d %d" % cl) -==SCRIPT== -#!/bin/bash -e -AVGCOLOR_PATH=/path/to/avgcolor.py -GAME_PATH=/path/to/minetest_game -MODS_PATH= # path to "mods" folder, only set if you have loaded mods -NODESTXT_PATH=./nodes.txt -COLORSTXT_PATH=./colors.txt - -while read -r line; do - set -- junk $line; shift - if [[ -z "$1" || $1 == "#" ]]; then - echo "$line"; continue - fi - tex=$(find $GAME_PATH -type f -name "$2") - [[ -z "$tex" && -n "$MODS_PATH" ]] && tex=$(find $MODS_PATH -type f -name "$2") - if [ -z "$tex" ]; then - echo "skip $1: texture not found" >&2 - continue - fi - echo "$1" $(python $AVGCOLOR_PATH "$tex") - echo "ok $1" >&2 -done < $NODESTXT_PATH > $COLORSTXT_PATH -# Use nicer colors for water and lava: -sed -re 's/^default:((river_)?water_(flowing|source)) [0-9 ]+$/default:\1 39 66 106 128 224/g' $COLORSTXT_PATH -i -sed -re 's/^default:(lava_(flowing|source)) [0-9 ]+$/default:\1 255 100 0/g' $COLORSTXT_PATH -i -# Add transparency to glass nodes and xpanes: -sed -re 's/^default:(.*glass) ([0-9 ]+)$/default:\1 \2 64 16/g' $COLORSTXT_PATH -i -sed -re 's/^doors:(.*glass[^ ]*) ([0-9 ]+)$/doors:\1 \2 64 16/g' $COLORSTXT_PATH -i -sed -re 's/^xpanes:(.*(pane|bar)[^ ]*) ([0-9 ]+)$/xpanes:\1 \3 64 16/g' $COLORSTXT_PATH -i -# Delete some usually hidden nodes: -sed '/^doors:hidden /d' $COLORSTXT_PATH -i -sed '/^fireflies:firefly /d' $COLORSTXT_PATH -i -sed '/^butterflies:butterfly_/d' $COLORSTXT_PATH -i -==INSTRUCTIONS== -1) Make sure avgcolors.py works (outputs the usage instructions when run) -2) Add the dumpnodes mod to Minetest -3) Create a world and load dumpnodes & all mods you want to generate colors for -4) Execute /dumpnodes ingame -5) Run the script to generate colors.txt (make sure to adjust the PATH variables at the top) diff --git a/util/dumpnodes/init.lua b/util/dumpnodes/init.lua new file mode 100644 index 0000000..c494752 --- /dev/null +++ b/util/dumpnodes/init.lua @@ -0,0 +1,61 @@ +local function get_tile(tiles, n) + local tile = tiles[n] + if type(tile) == 'table' then + return tile.name + end + return tile +end + +local function pairs_s(dict) + local keys = {} + for k in pairs(dict) do + keys[#keys+1] = k + end + table.sort(keys) + return ipairs(keys) +end + +minetest.register_chatcommand("dumpnodes", { + description = "Dump node and texture list for use with minetestmapper", + func = function() + local ntbl = {} + for _, nn in pairs_s(minetest.registered_nodes) do + local prefix, name = nn:match('(.*):(.*)') + if prefix == nil or name == nil then + print("ignored(1): " .. nn) + else + if ntbl[prefix] == nil then + ntbl[prefix] = {} + end + ntbl[prefix][name] = true + end + end + local out, err = io.open(minetest.get_worldpath() .. "/nodes.txt", 'wb') + if not out then + return true, err + end + local n = 0 + for _, prefix in pairs_s(ntbl) do + out:write('# ' .. prefix .. '\n') + for _, name in pairs_s(ntbl[prefix]) do + local nn = prefix .. ":" .. name + local nd = minetest.registered_nodes[nn] + local tiles = nd.tiles or nd.tile_images + if tiles == nil or nd.drawtype == 'airlike' then + print("ignored(2): " .. nn) + else + local tex = get_tile(tiles, 1) + tex = (tex .. '^'):match('%(*(.-)%)*^') -- strip modifiers + if tex:find("[combine", 1, true) then + tex = tex:match('.-=([^:]-)') -- extract first texture + end + out:write(nn .. ' ' .. tex .. '\n') + n = n + 1 + end + end + out:write('\n') + end + out:close() + return true, n .. " nodes dumped." + end, +}) diff --git a/util/dumpnodes/mod.conf b/util/dumpnodes/mod.conf new file mode 100644 index 0000000..eaaee76 --- /dev/null +++ b/util/dumpnodes/mod.conf @@ -0,0 +1,2 @@ +name = dumpnodes +description = minetestmapper development mod (node dumper) diff --git a/util/generate_colorstxt.py b/util/generate_colorstxt.py new file mode 100755 index 0000000..ee7bfcb --- /dev/null +++ b/util/generate_colorstxt.py @@ -0,0 +1,183 @@ +#!/usr/bin/env python3 +import sys +import os.path +import getopt +import re +from math import sqrt +try: + from PIL import Image +except: + print("Could not load image routines, install PIL ('pillow' on pypi)!", file=sys.stderr) + exit(1) + +############ +############ +# Instructions for generating a colors.txt file for custom games and/or mods: +# 1) Add the dumpnodes mod to a Minetest world with the chosen game and mods enabled. +# 2) Join ingame and run the /dumpnodes chat command. +# 3) Run this script and poin it to the installation path of the game using -g, +# the path(s) where mods are stored using -m and the nodes.txt in your world folder. +# Example command line: +# ./util/generate_colorstxt.py --game /usr/share/minetest/games/minetest_game \ +# -m ~/.minetest/mods ~/.minetest/worlds/my_world/nodes.txt +# 4) Copy the resulting colors.txt file to your world folder or to any other places +# and use it with minetestmapper's --colors option. +########### +########### + +# minimal sed syntax, s|match|replace| and /match/d supported +REPLACEMENTS = [ + # Delete some nodes that are usually hidden + r'/^fireflies:firefly /d', + r'/^butterflies:butterfly_/d', + # Nicer colors for water and lava + r's/^(default:(river_)?water_(flowing|source)) [0-9 ]+$/\1 39 66 106 128 224/', + r's/^(default:lava_(flowing|source)) [0-9 ]+$/\1 255 100 0/', + # Transparency for glass nodes and panes + r's/^(default:.*glass) ([0-9 ]+)$/\1 \2 64 16/', + r's/^(doors:.*glass[^ ]*) ([0-9 ]+)$/\1 \2 64 16/', + r's/^(xpanes:.*(pane|bar)[^ ]*) ([0-9 ]+)$/\1 \3 64 16/', +] + +def usage(): + print("Usage: generate_colorstxt.py [options] [input file] [output file]") + print("If not specified the input file defaults to ./nodes.txt and the output file to ./colors.txt") + print(" -g / --game \t\tSet path to the game (for textures), required") + print(" -m / --mods \t\tAdd search path for mod textures") + print(" --replace \t\tLoad replacements from file (ADVANCED)") + +def collect_files(path): + dirs = [] + with os.scandir(path) as it: + for entry in it: + if entry.name[0] == '.': continue + if entry.is_dir(): + dirs.append(entry.path) + continue + if entry.is_file() and '.' in entry.name: + if entry.name not in textures.keys(): + textures[entry.name] = entry.path + for path2 in dirs: + collect_files(path2) + +def average_color(filename): + inp = Image.open(filename).convert('RGBA') + data = inp.load() + + c0, c1, c2 = [], [], [] + for x in range(inp.size[0]): + for y in range(inp.size[1]): + px = data[x, y] + if px[3] < 128: continue # alpha + c0.append(px[0]**2) + c1.append(px[1]**2) + c2.append(px[2]**2) + + if len(c0) == 0: + print(f"didn't find color for '{os.path.basename(filename)}'", file=sys.stderr) + return "0 0 0" + c0 = sqrt(sum(c0) / len(c0)) + c1 = sqrt(sum(c1) / len(c1)) + c2 = sqrt(sum(c2) / len(c2)) + return "%d %d %d" % (c0, c1, c2) + +def apply_sed(line, exprs): + for expr in exprs: + if expr[0] == '/': + if not expr.endswith("/d"): raise ValueError() + if re.search(expr[1:-2], line): + return '' + elif expr[0] == 's': + expr = expr.split(expr[1]) + if len(expr) != 4 or expr[3] != '': raise ValueError() + line = re.sub(expr[1], expr[2], line) + else: + raise ValueError() + return line +# + +try: + opts, args = getopt.getopt(sys.argv[1:], "hg:m:", ["help", "game=", "mods=", "replace="]) +except getopt.GetoptError as e: + print(str(e)) + exit(1) +if ('-h', '') in opts or ('--help', '') in opts: + usage() + exit(0) + +input_file = "./nodes.txt" +output_file = "./colors.txt" +texturepaths = [] + +try: + gamepath = next(o[1] for o in opts if o[0] in ('-g', '--game')) + if not os.path.isdir(os.path.join(gamepath, "mods")): + print(f"'{gamepath}' doesn't exist or does not contain a game.", file=sys.stderr) + exit(1) + texturepaths.append(os.path.join(gamepath, "mods")) +except StopIteration: + print("No game path set but one is required. (see --help)", file=sys.stderr) + exit(1) + +try: + tmp = next(o[1] for o in opts if o[0] == "--replace") + REPLACEMENTS.clear() + with open(tmp, 'r') as f: + for line in f: + if not line or line[0] == '#': continue + REPLACEMENTS.append(line.strip()) +except StopIteration: + pass + +for o in opts: + if o[0] not in ('-m', '--mods'): continue + if not os.path.isdir(o[1]): + print(f"Given path '{o[1]}' does not exist.'", file=sys.stderr) + exit(1) + texturepaths.append(o[1]) + +if len(args) > 2: + print("Too many arguments.", file=sys.stderr) + exit(1) +if len(args) > 1: + output_file = args[1] +if len(args) > 0: + input_file = args[0] + +if not os.path.exists(input_file) or os.path.isdir(input_file): + print(f"Input file '{input_file}' does not exist.", file=sys.stderr) + exit(1) + +# + +print(f"Collecting textures from {len(texturepaths)} path(s)... ", end="", flush=True) +textures = {} +for path in texturepaths: + collect_files(path) +print("done") + +print("Processing nodes...") +fin = open(input_file, 'r') +fout = open(output_file, 'w') +n = 0 +for line in fin: + line = line.rstrip('\r\n') + if not line or line[0] == '#': + fout.write(line + '\n') + continue + node, tex = line.split(" ") + if not tex or tex == "blank.png": + continue + elif tex not in textures.keys(): + print(f"skip {node} texture not found") + continue + color = average_color(textures[tex]) + line = f"{node} {color}" + #print(f"ok {node}") + line = apply_sed(line, REPLACEMENTS) + if line: + fout.write(line + '\n') + n += 1 +fin.close() +fout.close() +print(f"Done, {n} entries written.")