diff --git a/.gitignore b/.gitignore index b6e4761..b9002d8 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,9 @@ __pycache__/ *.py[cod] *$py.class +recipes.json +backup/ + # C extensions *.so diff --git a/README.md b/README.md index c5c361f..dbc32a0 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,21 @@ # LuckyColorRecipeCalculator A luckycolor recipe calculator for islet to get maximum protection. + +# What is this? + +Islet is a minecraft server that have been playing lately and it has a special armor system. In short, only leather armor could reduce the damage and the degree of the reduction depends on the manhattan distance between the color of the leather armor and a randomly generated color named "luckycolor". + +To this end, inspired by [ANRAR4](https://github.com/ANRAR4)'s repo [ANRAR4/DyeLeatherArmor](https://github.com/ANRAR4/DyeLeatherArmor), I re-write the code that calculate the craftable colors in c++ and wrote a python script that helps to find the closest color to "luckycolor". + +# How to use? + +## I just want to calculat my luckycolor: +1. decompress the file ```./data/recipes.7z```, you will get ```recipes.json```. Put it in the current floder. +2. run ```python find_recipe.py``` + +## How can I get recipe.json from scratch: +1. compile ```leatherColorCombinationsCalculator.cpp``` with ```g++ -O3 -Wall leatherColorCombinationsCalculator.cpp -lpthread -o leatherColorCombinationsCalculator``` + +2. run ```./leatherColorCombinationsCalculator ```, it will produce ```layer1-14.bin``` (the generated file is also provided in ./data/layers.7z) + +3. run ```python generate_recipes.py``` to generate ```recipes.json```. diff --git a/colors.py b/colors.py new file mode 100644 index 0000000..bfa9c7f --- /dev/null +++ b/colors.py @@ -0,0 +1,106 @@ +from base64 import decode, encode +from torch import full + + +mc_colors = [ + ['红色染料', '#B02E26', 'red'], + ['绿色染料', '#5E7C16', 'green'], + ['紫色染料', '#8932B8', 'purple'], + ['青色染料', '#169C9C', 'cyan'], + ['淡灰色染料', '#9D9D97', 'silver'], + ['灰色染料', '#474F52', 'gray'], + ['粉红色染料', '#F38BAA', 'pink'], + ['黄绿色染料', '#80C71F', 'lime'], + ['黄色染料', '#FED83D', 'yellow'], + ['淡蓝色染料', '#3AB3DA', 'lightBlue'], + ['品红色染料', '#C74EBD', 'magenta'], + ['橙色染料', '#F9801D', 'orange'], + ['黑色染料', '#1D1D21', 'black'], + ['棕色染料', '#835432', 'brown'], + ['蓝色染料', '#3C44AA', 'blue'], + ['白色染料', '#F9FFFE', 'white'] +] + +colornames = [ + "red", "green", "purple", "cyan", + "silver", "gray", "pink", "lime", + "yellow", "lightBlue", "magenta", "orange", + "black", "brown", "blue", "white" +] + +def encode_color(r, g, b): + color = '#{r}{g}{b}'.format( + r = hex(r)[2:].rjust(2, '0'), + g = hex(g)[2:].rjust(2, '0'), + b = hex(b)[2:].rjust(2, '0'), + ) + return color + +def decode_color(color:str): + color = color.strip('#') + color = int(color, base=16) + r = (color>>16) & 0xff + g = (color>>8) & 0xff + b = color & 0xff + return [r, g, b] + +def parse_mc_color(colors): + parsed_color = {} + for color in colors: + name, value, key = color + assert len(value) == 7 + r, g, b = decode_color(value) + parsed_color[key] = { + "name": name, + "key": key, + "value": [r, g, b] + } + return parsed_color + +colors = parse_mc_color(mc_colors) + +def calculate_fused_color(colors): + if len(colors) == 0: + return None + # calculate average color + avg_color = [] + num_colors = len(colors) + for channel in zip(*colors): + channel_avg = sum(channel) // num_colors + avg_color.append(channel_avg) + # calculate gain factor. + avg_max = sum([max(x) for x in colors])/num_colors + max_avg = max(avg_color) + # print('avgcolor:', avg_color) + # print('avgmax:', avg_max) + # print('maxavg:', max_avg) + gain_factor = avg_max / max_avg + fused_color = [int(x*gain_factor) for x in avg_color] + return fused_color + +def get_color_by_recipe(recipe, current_color=None): + color_list = [] + for key, count in recipe.items(): + if count > 0: + color_list += [colors[key]['value']]*count + if current_color is not None: + color_list.append(current_color) + return color_list + + + +if __name__ == '__main__': + # color = get_color_by_recipe({ + # "purple": 1, + # "blue": 1, + # "white": 6 + # }) + # print('color:', color) + # print(calculate_fused_color(color)) + + color = [123, 59, 71] + encoded = encode_color(*color) + decoded = decode_color(encoded) + print('color:', color) + print('encoded:', encoded) + print('decoded:', decoded) \ No newline at end of file diff --git a/data/layers.7z b/data/layers.7z new file mode 100644 index 0000000..44a738b Binary files /dev/null and b/data/layers.7z differ diff --git a/data/recipes.7z b/data/recipes.7z new file mode 100644 index 0000000..a40a2a3 Binary files /dev/null and b/data/recipes.7z differ diff --git a/find_recipe.py b/find_recipe.py new file mode 100644 index 0000000..37d76ef --- /dev/null +++ b/find_recipe.py @@ -0,0 +1,156 @@ +from base64 import decode +from utils import load_dict +from colors import colors, colornames, decode_color +import math + +def calculate_leather_color(recipes): + cache = {} + for layer in recipes: + for color in recipes[layer]: + cache[color] = { + "color": decode_color(color), + "step": layer + } + return cache + +def color_distance(c1, c2): + return sum([abs(x - y) for x, y in zip(c1, c2)]) + +def find_closest_color(cache, color, max_step=-1): + min_dist = 10000 + closest_color = None + for name, this_color in cache.items(): + if max_step > 0 and this_color['step'] > max_step: + continue + this_dist = color_distance(color, this_color['color']) + if this_dist < min_dist: + min_dist = this_dist + closest_color = name + return closest_color, min_dist + +def decode_recipe(encoded): + encoded = int(encoded, base=16) + counts = [] + while len(counts) < 16: + counts.append(encoded%9) + encoded //=9 + return {name:count for name, count in zip(colornames, counts) if count > 0} + +def query_recipe(recipes, name): + # search color layer. + recipe = None + for layer in recipes: + recipe = recipes[layer].get(name, None) + if recipe is not None: + break + if recipe is None: + raise ValueError('Color not craftable.') + base_color, recipe = recipe + recipe = decode_recipe(recipe) + if base_color == '#000000': + return [recipe] + else: + steps = query_recipe(recipes, base_color) + steps.append(recipe) + return steps + +def explain_recipe(steps): + infos = [] + for i, step in enumerate(steps): + info = ','.join(['{0}*{1}'.format(colors[name]['name'], count) for name, count in step.items()]) + infos.append(info) + print('->'.join(infos)) + +def calculate_multipler(diff, enchant=False): + diff = 256 if diff > 256 else diff + multiplier = None + if enchant: + multiplier = 1 - (0.50) * (256.0 - diff)*0.5 / 256.0 + else: + multiplier = 1 - (0.50) * (256.0 - diff) / 256.0 + multiplier = multiplier * math.log(80) / math.log(125) # single cloth. + return multiplier + + +def show_protection(diff): + print('============= 减伤系数 =============') + multiplier = calculate_multipler(diff, enchant=False) + multiplier_full = multiplier**4 + multiplier_elytra = multiplier**3 * math.log(432) / math.log(125) + info = '\n'.join([ + "无附魔情况下,单件衣服减伤系数为:{0}".format(multiplier), + "无附魔情况下,全套衣服减伤系数为:{0}".format(multiplier_full), + "无附魔情况下,三件衣服+鞘翅减伤系数为:{0}".format(multiplier_elytra), + ]) + print(info) + multiplier = calculate_multipler(diff, enchant=True) + multiplier_full = multiplier**4 + multiplier_elytra = multiplier**3 * math.log(432) / math.log(125) + info = '\n'.join([ + "有附魔情况下,单件衣服减伤系数为:{0}".format(multiplier), + "有附魔情况下,全套衣服减伤系数为:{0}".format(multiplier_full), + "有附魔情况下,三件衣服+鞘翅减伤系数为:{0}".format(multiplier_elytra), + ]) + print(info) + print('在有保护类附魔存在时,实际减伤表格如下所示。其中横排分别表示单件、全套、有鞘翅三种情况,纵列分别表示保护系数为0/1/2/3/4/5') + print('(保护系数的计算:上限为5,其中保护四记为1,火焰、爆炸、弹射物保护对其对应的伤害时记为2,摔落保护对摔落保护记为3,每件衣服的保护系数可以直接相加且上限为5,超过5记为5)') + multiplier_protection = [1 - x*4/25 for x in range(6)] + multiplier_protect_enchant = [multiplier*this_protection for this_protection in multiplier_protection] + multiplier_full_protect_enchant = [multiplier_full*this_protection for this_protection in multiplier_protection] + multiplier_elytra_protect_enchant = [multiplier_elytra*this_protection for this_protection in multiplier_protection] + info = '\n'.join([ + ' '.join(['%.4f']*6)%tuple(multiplier_protect_enchant), + ' '.join(['%.4f']*6)%tuple(multiplier_full_protect_enchant), + ' '.join(['%.4f']*6)%tuple(multiplier_elytra_protect_enchant) + ]) + print(info) + + +if __name__ == '__main__': + print('正在加载配方数据...') + recipes = load_dict('./recipes.json') + print('配方数据库加载完成!正在准备数据...') + leather_color_cache = calculate_leather_color(recipes) + print('数据准备完毕,共加载{0}种可合成颜色。'.format(len(leather_color_cache))) + + note = '\n'.join( + [ + '请输入你的幸运色,用空格分隔。', + '如你的幸运色为100, 200, 100, 请输入 100 200 100。', + '默认所有颜色可用,若你希望限制合成的步骤数量,可以在幸运色后追加一个数字表示每种颜色的最大合成步骤', + '如 100 200 100 2 表示给出的配方中最多进行两次合成。' + ]) + print(note) + + while True: + luckycolor = None + max_step = -1 + while luckycolor is None: + try: + luckycolor_str = input('>>> ') + luckycolor = luckycolor_str.split() + luckycolor = [int(x) for x in luckycolor] + if len(luckycolor) < 3 or len(luckycolor) > 4: + raise ValueError + if len(luckycolor) == 4: + max_step = luckycolor[3] + luckycolor = luckycolor[:3] + except Exception: + print('幸运色颜色格式错误qwq') + luckycolor = None + + print('正在寻找最接近的可合成颜色...') + name, dist = find_closest_color(leather_color_cache, luckycolor, max_step=max_step) + color = decode_color(name) + print('最接近的可合成颜色为: {0}{1},曼哈顿距离误差为:{2}'.format( + name, tuple(color), dist + )) + + show_protection(dist) + + print('查找配方中...') + crafting_steps = query_recipe(recipes, name) + print('合成配方如下:') + explain_recipe(crafting_steps) + + diff --git a/generate_recipes.py b/generate_recipes.py new file mode 100644 index 0000000..b64e441 --- /dev/null +++ b/generate_recipes.py @@ -0,0 +1,65 @@ +import os +import struct +from utils import save_dict +from colors import encode_color, colornames, colors + + +def decode_color_from_bytes(buffer): + color = struct.unpack('L', buffer)[0] + r = (color>>16) & 0xff + g = (color>>8) & 0xff + b = color & 0xff + return r, g, b + +def decode_recipe_from_bytes(buffer): + encoded = struct.unpack('Q', buffer)[0] + # recipe = [] + # while len(recipe) < 16: + # recipe.append(encoded%9) + # encoded //=9 + # return {name:count for name, count in zip(colornames, recipe) if count > 0} + return hex(encoded)[2:].rjust(16, '0') + +def load_recipe(filepath): + file = open(filepath, 'rb') + while True: + base_color = file.read(4) + if base_color == b'': + break + recipe = file.read(8) + if recipe == b'': + raise ValueError + crafted_color = file.read(4) + + base_color = decode_color_from_bytes(base_color) + recipe = decode_recipe_from_bytes(recipe) + crafted_color = decode_color_from_bytes(crafted_color) + yield base_color, recipe, crafted_color + +def convert_recipes(fmt='./layer{0}.bin', save_path='./recipes.json'): + i = 1 + print('checking', fmt.format(i)) + recipe_db = {} + counter = 0 + while os.path.exists(fmt.format(i)): + path = fmt.format(i) + print('loading ', path) + recipes = load_recipe(path) + this_converted_recipe = {} + this_counter = 0 + for base_color, recipe, crafted_color in recipes: + crafted_color = encode_color(*crafted_color) + base_color = encode_color(*base_color) + this_converted_recipe[crafted_color] = [base_color, recipe] + this_counter += 1 + recipe_db[str(i)] = this_converted_recipe + print('layer {0} has {1} colors.'.format(i, this_counter)) + counter += this_counter + i += 1 + print('{0} colors loaded in total.'.format(counter)) + print('writing result to', save_path) + save_dict(save_path, recipe_db, indent=None) + + +if __name__ == '__main__': + convert_recipes() \ No newline at end of file diff --git a/leatherColorCombinationsCalculator.cpp b/leatherColorCombinationsCalculator.cpp new file mode 100644 index 0000000..e19f259 --- /dev/null +++ b/leatherColorCombinationsCalculator.cpp @@ -0,0 +1,404 @@ +#include +#include +#include +#include +#include +#include +#include +#include + +template +void printVector(std::vector vec){ + for (size_t i = 0; i < vec.size(); i++) + { + std::cout< +void printMatrix(std::vector> mat){ + for (size_t i = 0; i < mat.size(); i++) + { + printVector(mat[i]); + } +} + +class Color +{ + public: + Color():r(0), g(0), b(0){} + Color(const Color& color) { + r = color.r; + g = color.g; + b = color.b; + } + Color(uint32_t color){ + Color c = Color::decode(color); + r = c.r; + g = c.g; + b = c.b; + } + Color(int _r, int _g, int _b){ + r = uint8_t(_r); + g = uint8_t(_g); + b = uint8_t(_b); + } + + uint32_t encode(void) const{ + return (uint32_t(r)<<16) + (uint32_t(g)<<8) + uint32_t(b); + } + static Color decode(uint32_t val) { + uint8_t r = (val>>16) & 0xff; + uint8_t g = (val>>8) & 0xff; + uint8_t b = val & 0xff; + return Color(r, g, b); + } + void show(void) { + std::cout<<"(r, g, b) = ("<& current, std::vector>& ans){ + if (choice <= 0) { + std::vector compose; + for (size_t i = 0; i < current.size(); i++) + { + compose.push_back(current[current.size() - 1 - i]); + } + ans.push_back(compose); + return; + } + if (total<=0 || choice <=0 || choice > total) return; + // do not choose current number + generateCompose(total-1, choice, current, ans); + // choose current number. + current.push_back(total); + generateCompose(total-1, choice-1, current, ans); + current.pop_back(); +} + +std::vector composeToRecipe(const std::vector& compose, int num) { + std::vector pre = compose; + std::vector post = compose; + pre.insert(pre.begin(), 0); + post.push_back(num); + std::vector recipe; + for (size_t i = 1; i < pre.size(); i++) + { + recipe.push_back(post[i] - pre[i] - 1); + } + return recipe; +} + +class Recipe { + public: + Recipe(const std::vector& _counts){ + counts = _counts; + } + + public: + uint64_t encode(void) { + uint64_t val = 0; + uint64_t bits = 1; + for (size_t i = 0; i < counts.size(); i++) + { + val += bits * counts[i]; + bits *= 9; + } + return val; + } + static Recipe decode(uint64_t encoded) { + std::vector counts; + for (size_t i = 0; i < 16; i++) + { + counts.push_back(int(encoded%9)); + encoded /= 9; + } + return Recipe(counts); + } + void preCraft(const std::vector& colorlist) { + for (size_t i = 0; i < counts.size(); i++) + { + for (int j = 0; j < counts[i]; j++) + { + table.addColor(colorlist[i]); + } + } + } + + Color getTableColor(void) { + return table.craft(); + } + + Color getCraftedArmorColor(const Color armorColor) { + ColorCraftingTable temp = table; + if (!(armorColor == Color(0, 0, 0))) { + temp.addColor(armorColor); + } + return temp.craft(); + } + + std::vector getCounts(void){ + return counts; + } + + private: + std::vector counts; + ColorCraftingTable table; + +}; + +void dumpPossibleRecipe(const std::string& path) { + std::vector> ans; + std::vector current; + std::fstream recipes; + recipes.open(path, std::fstream::out|std::fstream::binary); + generateCompose(24, 16, current, ans); + size_t counts = 0; + for (size_t i = 0; i < ans.size(); i++) + { + Recipe recipe(composeToRecipe(ans[i], 25)); + uint64_t encoded = recipe.encode(); + if (encoded == 0) continue; // this recipe is empty. + recipes.write((char*) &encoded, sizeof(encoded)); + counts += 1; + } + recipes.close(); + std::cout<<"we have write "< +class SyncholizedHeapBitset { + public: + SyncholizedHeapBitset(void) { + bitmap = new std::bitset; + } + ~SyncholizedHeapBitset(void) { + delete bitmap; + } + + bool test(size_t pos) {return bitmap->test(pos);} + void set(size_t pos, bool val) {bitmap->set(pos, val);} + size_t count(void) {return bitmap->count();} + + private: + std::bitset* bitmap; +}; + +void searchBlock( + std::mutex& lock, + std::vector& recipes,// readonly + SyncholizedHeapBitset<(2<<24)>& colormap, // thread-safe + std::vector& oldColors, // read-only + std::vector& newColors, //write-only + std::fstream& searched, // write-only. + size_t start, size_t end +) { + for (size_t i = start; i < end; i++) + { + for(size_t j = 0; j < recipes.size(); j++) { + Color crafted = recipes[j].getCraftedArmorColor(oldColors[i]); + uint32_t encodedColor = crafted.encode(); + if (!colormap.test(encodedColor)) { + lock.lock(); // for thread safety. + if (!colormap.test(encodedColor)) { // double check after locked to avoid duplication. + colormap.set(encodedColor, true); + newColors.push_back(crafted); + uint32_t encodedOldColor = oldColors[i].encode(); + searched.write((const char*) &encodedOldColor, sizeof(encodedOldColor)); + uint64_t encodedRecipe = recipes[j].encode(); + searched.write((const char*) &encodedRecipe, sizeof(encodedRecipe)); + searched.write((const char*) &encodedColor, sizeof(encodedColor)); + } + lock.unlock(); + } + } + } +} + +void searchAllColors(const std::vector& colorlist, int numThreads=8, int blockSize=32) { + std::cout<<"preparing..."< recipes; + std::vector> ans; + std::vector current; + std::cout<<"Generating compose..."< colormap; + std::vector oldColors; + // color #0x000000 do not exist in minecraft, so it is okay to be used as empty color. + oldColors.push_back(Color(uint32_t(0))); + std::mutex lock; + + size_t layer = 1; + while (oldColors.size() > 0) { + std::cout<<"processing layer "< newColors; + auto start = std::chrono::high_resolution_clock::now(); + auto last = start; + for (size_t i = 0; i < oldColors.size(); i+=numThreads*blockSize) + { + // create threads and dispatch jobs. + std::vector threads; + for(int j = 0; j < numThreads; j++) { + int start = i + j*blockSize; + int end = i + (j+1)*blockSize; + if (end > (int) oldColors.size()) { + end = (int) oldColors.size(); + } + if (start > end) {start = end;} + std::thread* t = new std::thread( + searchBlock, + std::ref(lock), + std::ref(recipes), + std::ref(colormap), + std::ref(oldColors), + std::ref(newColors), + std::ref(output), + start, end + ); + threads.push_back(t); + } + // wait for all threads to finish. + for(size_t j = 0; j < threads.size(); j++) { + threads[j]->join(); + delete threads[j]; + threads[j] = nullptr; + } + auto now = std::chrono::high_resolution_clock::now(); + std::chrono::duration diff = now - start; + std::chrono::duration interval = now - last; + if (interval.count() >= 5) { + last = now; + std::cout<<"progress: "< colors = { + Color(176, 46, 38), + Color(94, 124, 22), + Color(137, 50, 184), + Color(22, 156, 156), + Color(157, 157, 151), + Color(71, 79, 82), + Color(243, 139, 170), + Color(128, 199, 31), + Color(254, 216, 61), + Color(58, 179, 218), + Color(199, 78, 189), + Color(249, 128, 29), + Color(29, 29, 33), + Color(131, 84, 50), + Color(60, 68, 170), + Color(249, 255, 254), + }; + + // useless in this script, but useful for interpreting encoded recipe. + std::vector keys = { + "red", "green", "purple", "cyan", + "silver", "gray", "pink", "lime", + "yellow", "lightBlue", "magenta", "orange", + "black", "brown", "blue", "white" + }; + + // parse numThreads. + if (argc == 2) { + int numThreads = std::stoi(std::string(argv[1])); + searchAllColors(colors, numThreads); + } else { + std::cout<<"Usage: ./leatherColorCombinationsCalculator "<