From 05af3222eeede23daff3bb1b12f3bfc5fb3d4610 Mon Sep 17 00:00:00 2001 From: Auri Date: Thu, 23 Sep 2021 14:08:58 -0700 Subject: [PATCH] Document StringParser because otherwise I'd have no idea what it's doing in two months. --- src/game/atlas/TextureAtlas.cpp | 2 +- src/lua/register/RegisterBlock.h | 97 +++--- src/util/StringParser.h | 298 +++++++++++++++--- .../auri_hot_wheel/script/server/main.lua | 1 - .../zeus/mods/zeus_flowers/script/flowers.lua | 10 +- .../mods/zeus_flowers/script/models/hash.lua | 43 ++- .../mods/zeus_flowers/textures/clover.png | Bin 0 -> 8763 bytes .../mods/zeus_world/script/biomes/plains.lua | 25 +- 8 files changed, 354 insertions(+), 122 deletions(-) create mode 100644 subgames/zeus/mods/zeus_flowers/textures/clover.png diff --git a/src/game/atlas/TextureAtlas.cpp b/src/game/atlas/TextureAtlas.cpp index 27ecc18e..774b2b2a 100644 --- a/src/game/atlas/TextureAtlas.cpp +++ b/src/game/atlas/TextureAtlas.cpp @@ -60,7 +60,7 @@ TextureAtlas::TextureAtlas(uvec2 size) : * @param tex - The string of the texture to convert. */ - parser.addLiteralFnCtx([](TexParser::Ctx& ctx, string tex) { + parser.addLiteralFnCtx([](TexParser::Ctx& ctx, string tex) { return TexParser::Data { std::make_shared(ctx.atlas.getBytesOfTex(tex)) }; }); diff --git a/src/lua/register/RegisterBlock.h b/src/lua/register/RegisterBlock.h index 238af2c2..6e1ffbb5 100644 --- a/src/lua/register/RegisterBlock.h +++ b/src/lua/register/RegisterBlock.h @@ -13,7 +13,17 @@ #include "game/atlas/ServerDefinitionAtlas.h" namespace RegisterBlock { + namespace { + struct TintParserData { + string tex; + optional tint; + optional mask; + }; + + using TintParser = StringParser; + TintParser parser {}; + bool parserReady = false; /** * Takes a lua selection box table list, and returns a vector of selection boxes. @@ -37,44 +47,6 @@ namespace RegisterBlock { return boxes; } - - /** - * Given a textures string, attempts to find a tint() texture modifier and modifies the inputted parameters - * to describe it. Does nothing to the passed in variables if there is tint() is not used. - * - * @param texture - The texture string, will be replaced with the inner string if tint() is used. - * @param blendInd - A variable reference that will be assigned the index of the tint blend, if there is one. - * @param blendMask - A variable reference to the blend mask texture, which will be assigned if one is found. - */ - - static inline void getMeshPartTexture(std::string& texture, unsigned int& blendInd, std::string& blendMask) { - if (strncmp(texture.data(), "tint(", 5) == 0 && texture.find_last_of(')') != std::string::npos) { - // Biome tinting time - texture.erase(std::remove_if(texture.begin(), texture.end(), isspace), texture.end()); - - std::string::size_type paramsBegin = texture.find_first_of('('); - std::string::size_type paramsEnd = texture.find_last_of(')'); - - std::string paramsString = texture.substr(paramsBegin + 1, paramsEnd - paramsBegin - 1); - - std::vector params; - std::string::size_type pos; - while ((pos = paramsString.find(',')) != std::string::npos) { - params.push_back(paramsString.substr(0, pos)); - paramsString.erase(0, pos + 1); - } - params.push_back(paramsString); - - if (params.size() < 2) - throw std::runtime_error("Invalid biome tint values. Must have at least 2 params."); - - texture = params[1]; - blendInd = atoi(params[0].data()) + 1; //TODO: support multiple blend colors - blendMask = (params.size() >= 3 ? params[2] : ""); - } - } - - /** * Creates near and far models for a block based on the passed in parameters. * @@ -141,7 +113,7 @@ namespace RegisterBlock { } } - // Parse through all of the parts and add them to the model + // Parse through all the parts and add them to the model auto partsOpt = modelTable.get>("parts"); if (!partsOpt) throw std::runtime_error("blockmodel is missing parts table"); partsOpt->for_each([&](sol::object key, sol::object value) { @@ -183,20 +155,17 @@ namespace RegisterBlock { // Get the part's texture int tex = std::max(static_cast(meshPartTable.get_or("tex", 1)), 1); - - auto texture = textures[std::min(tex - 1, (int) textures.size() - 1)]; - unsigned int blendInd = 0; - std::string blendMask = ""; - getMeshPartTexture(texture, blendInd, blendMask); + let data = parser.parse(textures[std::min(tex - 1, (int) textures.size() - 1)]); + u32 blendInd = data.tint ? *data.tint + 1 : 0; // Add texture refs to blockModel if the textures table is provided std::shared_ptr textureRef = nullptr, blendMaskRef = nullptr; if (atlas) { - textureRef = (*atlas)[texture]; + textureRef = (*atlas)[data.tex]; model.textureRefs.insert(textureRef); - if (blendInd && !blendMask.empty()) { - blendMaskRef = (*atlas)[blendMask]; + if (blendInd && data.mask) { + blendMaskRef = (*atlas)[*data.mask]; model.textureRefs.insert(blendMaskRef); } } @@ -259,14 +228,12 @@ namespace RegisterBlock { std::vector> blendMaskRefs; for (auto i = 0; i < lowdef_textures.size(); i++) { - std::string texture = lowdef_textures[i]; - unsigned int blendInd = 0; - std::string blendMask = ""; - getMeshPartTexture(texture, blendInd, blendMask); + let data = parser.parse(lowdef_textures[i]); + u32 blendInd = data.tint ? *data.tint + 1 : 0; - textureRefs.push_back((*atlas)[texture]); + textureRefs.push_back((*atlas)[data.tex]); blendInds.push_back(blendInd); - blendMaskRefs.push_back(blendMask != "" ? (*atlas)[blendMask] : nullptr); + blendMaskRefs.push_back(data.mask ? (*atlas)[*data.mask] : nullptr); } farModel = BlockModel::createCube(textureRefs, blendInds, blendMaskRefs); @@ -413,6 +380,27 @@ namespace RegisterBlock { } } + /** + * Initializes the parser with the necessary parse functions. + * In the future, this could be cleaned up, but as it is it's totally fine. + */ + + static void initParser() { + if (parserReady) return; + + parser.setUnknownFnsAreLiteral(true); + + parser.addFn>("tint", + [](u32 tint, TintParserData tex, optional mask) { + return TintParserData { tex.tex, tint, mask ? optional(mask->tex) : std::nullopt }; + }); + + parser.addLiteralFn([](string tex) { + return TintParserData { tex, std::nullopt, std::nullopt }; + }); + + parserReady = true; + } /** * Server method to register a block. Calls registerBlock with the necessary parameters. @@ -424,11 +412,11 @@ namespace RegisterBlock { */ static void server(sol::table& core, ServerSubgame& game, const std::string& identifier) { + initParser(); registerBlock(core["registered_blocks"], core["registered_blockmodels"], identifier, game.getDefs(), nullptr); } - /** * Client method to register a block. Calls registerBlock with the necessary parameters. * Registers a block to the DefinitionAtlas. @@ -439,6 +427,7 @@ namespace RegisterBlock { */ static void client(sol::table& core, LocalSubgame& game, const std::string& identifier) { + initParser(); registerBlock(core["registered_blocks"], core["registered_blockmodels"], identifier, game.getDefs(), &game.textures); } diff --git a/src/util/StringParser.h b/src/util/StringParser.h index d228e527..c9ab29d4 100644 --- a/src/util/StringParser.h +++ b/src/util/StringParser.h @@ -5,102 +5,193 @@ #include "util/Types.h" +/** SFINAE Helpers. */ namespace { + /** Struct used for identifying variants in SFINAE. */ template struct is_variant : std::false_type {}; template struct is_variant> : std::true_type {}; + /** Const bool used for identifying variants in SFINAE. */ template inline constexpr bool is_variant_v = is_variant::value; + /** Struct used for identifying optionals in SFINAE. */ template struct is_optional : std::false_type {}; template struct is_optional> : std::true_type {}; + /** Const bool used for identifying optionals in SFINAE. */ template inline constexpr bool is_optional_v = is_optional::value; } +/** + * Exposes a method to parse a string containing recursive functions, + * e.g: "tint(0, crop(0, 0, 16, 16, my_texture))". + * Functions are defined using template parameters and coerced from the string, + * executed in the order that they are used in the string and used to create an output. + * + * @tparam R - The type that parse() and all declared functions will return. + * @tparam C - An optional context type, which an instance of can be passed to parse() and accessed in functions. + */ + template class StringParser { -public: - typedef R Data; - typedef C Ctx; - class Fn; - - using EXEC_ARGS = const vec&; - using EXEC_FN = std::function; - - struct Fn { - Fn(EXEC_FN exec): exec(exec) {}; - - R operator()(C& ctx, EXEC_ARGS args) const { - return exec(ctx, args); - } - - private: - EXEC_FN exec; - }; + /** A type alias for a vector of string parameters that will be passed into a Fn. */ + using STR_ARGS = const vec&; + + /** A wrapped function that will accept an STR_ARGS and a Context, and execute a defined function with them. */ + using PARSE_FN = std::function; + +public: + + /** The data type of this parser. */ + typedef R Data; + + /** The context type of this parser. Will be nullptr_t if no context type is specified. */ + typedef C Ctx; explicit StringParser() = default; + /** If set to true, when an unknown function is read it will be passed to the literal function as a string. */ + void setUnknownFnsAreLiteral(bool state) { + unknownFnsAreLiteral = state; + } + + /** + * Adds a function to the functions map. + * For a function to be valid, it must only have parameters that are + * integral, floating point, strings, the Data type, or optionals & variants of them. + * It must also return a Data type. The Args type parameter must match the function's types. + * If the function needs access to the Context, use addFnCtx() instead. + * + * @tparam Args - The argument types of the function. + * @param name - The name of the function. + * @param fn - The function lambda. + */ + template void addFn(const string& name, const Func& fn) { - functions.emplace(name, [=, this](C& ctx, EXEC_ARGS strArgs) { + functions.emplace(name, [=, this](C& ctx, STR_ARGS strArgs) { std::tuple args = {}; parseStrArgs>(strArgs, args, ctx); return std::apply(fn, args); }); } + /** + * Adds a function to the functions map that has access to the Ctx. + * The same restrictions for a function apply, but the function + * must also accept a reference to a Ctx before all other arguments. + * This parameter should not be specified in Args. + * + * @tparam Args - The argument types of the function. + * @param name - The name of the function. + * @param fn - The function lambda. + */ + template void addFnCtx(const string& name, const Func& fn) { - functions.emplace(name, [=, this](C& ctx, EXEC_ARGS strArgs) { + functions.emplace(name, [=, this](C& ctx, STR_ARGS strArgs) { std::tuple args = {}; parseStrArgs>(strArgs, args, ctx); return std::apply(fn, std::tuple_cat(std::tuple(ctx), args)); }); } - template - void addLiteralFn(const Func& fn) { - addFn("_", fn); + /** + * Shortcut to add the literal function, which is called when a string literal is found. + * This is shorthand for calling addFn("_", ...). + * The literal function must only accept a single string parameter. + * + * @param fn - The literal function lambda. + */ + + template + inline void addLiteralFn(const Func& fn) { + addFn("_", fn); } - template - void addLiteralFnCtx(const Func& fn) { - addFnCtx("_", fn); + /** + * Shortcut to add the literal function that has access to the Ctx. + * The same restrictions for a literal function apply, but the function + * must also accept a reference to a Ctx before the string parameter. + * + * @param fn - The literal function lambda. + */ + + template + inline void addLiteralFnCtx(const Func& fn) { + addFnCtx("_", fn); } + /** + * Parses a string using the functions defined. + * This method may only be called if no context was specified. + * + * @param str - The string to parse. + * @returns the parsed result. + */ + const R parse(string str) const { const nullptr_t ctx {}; str.erase(std::remove_if(str.begin(), str.end(), isspace), str.end()); return std::move(parseRaw(str, const_cast(ctx))); } + /** + * Parses a string using the functions defined. + * + * @param str - The string to parse. + * @param ctx - The context to parse with, passed to all functions that use Ctx. + * @returns the parsed result. + */ + const R parse(string str, C& ctx) const { str.erase(std::remove_if(str.begin(), str.end(), isspace), str.end()); return std::move(parseRaw(str, ctx)); } private: - const R parseRaw(const std::string_view str, C& ctx = {}) const { + + /** + * Parses a the string segment provided as a function or string literal. + * + * @param str - The string segment to parse. + * @param ctx - The context to parse with. + * @returns the parsed result. + */ + + inline const R parseRaw(const std::string_view str, C& ctx) const { if (strIsFunction(str)) { let func = parseFunction(str); const let f = functions.find(string(func.first)); - if (f == functions.end()) throw std::invalid_argument("Unknown function '" + string(func.first) + "'!"); - return f->second(ctx, func.second); - } - else { - const let& f = functions.find("_"); - if (f == functions.end()) throw std::invalid_argument("No default function handler!"); - return f->second(ctx, { str }); + if (f == functions.end()) { + if (!unknownFnsAreLiteral) throw std::invalid_argument( + "Unknown function '" + string(func.first) + "'."); + } + else return f->second(ctx, func.second); } + + const let& f = functions.find("_"); + if (f == functions.end()) throw std::invalid_argument("Literal specified with no literal function."); + return f->second(ctx, { str }); } + /** + * Given a string segment starting and a start position pointing to an open parenthesis, + * finds the index of matching closing parenthesis. + * + * @param str - The string segment to search through. + * @param start - The position of the opening parenthesis to start at. + * @returns the index of the start parenthesis' closing parenthesis. + * @throws if the parentheses are unbalanced. + */ + const usize findClosingParen(const std::string_view& str, usize start) const { usize levels = 0; for (usize i = start + 1; i < str.size(); i++) { @@ -114,17 +205,29 @@ private: throw std::invalid_argument("Mismatched parentheses."); } + /** + * Checks if the string segment provided is a function call. + * + * @param str - the string segment to check. + * @returns a boolean indicating if the string segment is a function call. + */ + const bool strIsFunction(const std::string_view& str) const { return str.find_first_of('(') != string::npos; } + /** + * Parses a function and finds its name and parameters. + * Assumes that the string is a function, which should be checked beforehand with strIsFunction. + * + * @param str - The string to parse as a function. + * @returns a pair containing the function name, and a vector of parameter strings. + */ + const std::pair> parseFunction(const std::string_view& str) const { let nextParen = str.find_first_of('('); let nextComma = str.find_first_of(','); - if (nextParen == string::npos || (nextComma != string::npos && nextComma < nextParen)) - throw std::invalid_argument("Not a function"); - let name = str.substr(0, nextParen); vec args {}; @@ -144,26 +247,87 @@ private: return { name, args }; } + /** + * Parses a string argument from an STR_ARGS. + * + * @tparam T - std::string + * @tparam I - The index of the parameter to parse in the args vector. + * @param args - An STR_ARGS to pull the parameter from. + * @returns the string value. + * @throws if the parameter cannot be interpreted as a string, or if there were not enough parameters. + */ + template , bool> = true> - T parseStrArg(EXEC_ARGS args, C&) { + T parseStrArg(STR_ARGS args, C&) { if (I >= args.size()) throw std::invalid_argument("Not enough parameters."); return string(args[I]); } + /** + * Parses an integral argument from an STR_ARGS. + * + * @tparam T - An integral type, e.g u8, i32, usize... + * @tparam I - The index of the parameter to parse in the args vector. + * @param args - An STR_ARGS to pull the parameter from. + * @returns the integral value. + * @throws if the parameter cannot be interpreted as an integer, or if there were not enough parameters. + */ + template , bool> = true> - T parseStrArg(EXEC_ARGS args, C&) { + T parseStrArg(STR_ARGS args, C&) { if (I >= args.size()) throw std::invalid_argument("Not enough parameters."); return std::stoi(args[I].data()); } + /** + * Parses a floating point argument from an STR_ARGS. + * + * @tparam T - A floating point type, i.e f32 or f64. + * @tparam I - The index of the parameter to parse in the args vector. + * @param args - An STR_ARGS to pull the parameter from. + * @returns the floating point value. + * @throws if the parameter cannot be interpreted as a floating point, or if there were not enough parameters. + */ + template , bool> = true> - T parseStrArg(EXEC_ARGS args, C&) { + T parseStrArg(STR_ARGS args, C&) { if (I >= args.size()) throw std::invalid_argument("Not enough parameters."); return std::stod(args[I].data()); } + /** + * Parses a Data type argument from an STR_ARGS. + * A parse function will have a Data type argument if it wants to accept the output from another function. + * + * @tparam T - The Data type. + * @tparam I - The index of the parameter to parse in the args vector. + * @param args - An STR_ARGS to pull the parameter from. + * @param ctx - The context to pass into the parse functions. + * @returns the Data result from whatever inner functions were executed. + * @throws if the parameter cannot be parsed as a Data type, or if there were not enough parameters. + */ + + template , bool> = true> + T parseStrArg(STR_ARGS args, C& ctx) { + if (I >= args.size()) throw std::invalid_argument("Not enough parameters."); + return parseRaw(parseStrArg(args, ctx), ctx); + } + + /** + * Compile-time iterates over a variant type, checking if the parameter specified + * matches any of the types in it. If one matches, it is parsed and returned. + * + * @tparam T - A variant type containing other parsable types. + * @tparam I - The index of the parameter to parse in the **args vector.** + * @tparam VI - The index of the variant to try to parse. + * @param args - An STR_ARGS to pull the parameter from. + * @param ctx - The context to pass into the parse functions. + * @returns a variant containing the first type that the parameter can be parsed as. + * @throws if the parameter cannot be parsed as any of the variant types. + */ + template - T parseStrVariantArg(EXEC_ARGS args, C& ctx) { + inline T parseStrVariantArg(STR_ARGS args, C& ctx) { try { return parseStrArg, I>(args, ctx); } @@ -176,29 +340,63 @@ private: } } + /** + * Parses a variant argument from an STR_ARGS. + * The variant can contain any other parsable types. + * + * @tparam T - The variant type. + * @tparam I - The index of the parameter to parse in the args vector. + * @param args - An STR_ARGS to pull the parameter from. + * @param ctx - The context to pass into the parse functions. + * @returns a variant containing the first type that the parameter can be parsed as. + * @throws if the parameter cannot be parsed as any of the variant types, or if there were not enough parameters. + */ + template, bool> = true> - T parseStrArg(EXEC_ARGS args, C& ctx) { + T parseStrArg(STR_ARGS args, C& ctx) { if (I >= args.size()) throw std::invalid_argument("Not enough parameters."); return parseStrVariantArg(args, ctx); } + /** + * Parses an optional argument from an STR_ARGS. + * The optional can contain any parsable type, including variants and other optionals.* + * * but why the hell would you do that? + * + * @tparam T - The optional type. + * @tparam I - The index of the parameter to parse in the args vector. + * @param args - An STR_ARGS to pull the parameter from. + * @param ctx - The context to pass into the parse function. + * @returns an optional that may contain a value. + */ + template, bool> = true> - T parseStrArg(EXEC_ARGS args, C& ctx) { + T parseStrArg(STR_ARGS args, C& ctx) { if (I >= args.size() || args[I].empty()) return {}; return parseStrArg(args, ctx); } - template , bool> = true> - T parseStrArg(EXEC_ARGS args, C& ctx) { - if (I >= args.size()) throw std::invalid_argument("Not enough parameters."); - return parseRaw(parseStrArg(args, ctx), ctx); - } + /** + * Compile-time iterates over a tuple of the argument types and parses them, + * inserting the parsed parameters into the args tuple referenced. + * + * @tparam T - The tuple type of the arguments. + * @tparam I - The index of the current parameter being parsed. Used for iterating. + * @param strArgs - The STR_ARGS storing the string parameters. + * @param args - The args tuple to insert the parsed arguments into. + * @param ctx - The context to pass into parse functions. + * @throws if any of the parameters cannot be parsed, see parseStrArg implementations for details. + */ template - void parseStrArgs(EXEC_ARGS strArgs, T& args, C& ctx) { + inline void parseStrArgs(STR_ARGS strArgs, T& args, C& ctx) { std::get(args) = parseStrArg, I>(strArgs, ctx); if constexpr (I + 1 < std::tuple_size_v) parseStrArgs(strArgs, args, ctx); } - std::unordered_map functions {}; + /** A map of parse functions, indexed by their name. */ + std::unordered_map functions {}; + + /** If true, unknown functions will be parsed as string literals, if false, an error will be thrown. */ + bool unknownFnsAreLiteral = false; }; \ No newline at end of file diff --git a/subgames/zeus/mods/auri_hot_wheel/script/server/main.lua b/subgames/zeus/mods/auri_hot_wheel/script/server/main.lua index 8ca3c07e..bd62515f 100644 --- a/subgames/zeus/mods/auri_hot_wheel/script/server/main.lua +++ b/subgames/zeus/mods/auri_hot_wheel/script/server/main.lua @@ -1,5 +1,4 @@ zepha.bind('new_player', function(player) - print('added inventories :)') local inv = player:get_inventory() inv:add_list('hot_wheel_1', 5, 5) inv:add_list('hot_wheel_2', 5, 5) diff --git a/subgames/zeus/mods/zeus_flowers/script/flowers.lua b/subgames/zeus/mods/zeus_flowers/script/flowers.lua index 52e70742..d81a7747 100644 --- a/subgames/zeus/mods/zeus_flowers/script/flowers.lua +++ b/subgames/zeus/mods/zeus_flowers/script/flowers.lua @@ -41,9 +41,13 @@ zepha.register_block("zeus:flowers:clover", { solid = false, model = "zeus:flowers:hash", textures = { --- "tint(0, crop(0, 0, 16, 16, zeus:flowers:clover))", --- "tint(0, crop(16, 0, 16, 16, zeus:flowers:clover))", --- "tint(0, crop(32, 0, 16, 16, zeus:flowers:clover))" + "tint(0, crop(0, 0, 16, 16, zeus:flowers:clover))", + "tint(0, crop(16, 0, 16, 16, zeus:flowers:clover))", + "tint(0, crop(32, 0, 16, 16, zeus:flowers:clover))", + "tint(0, crop(0, 16, 16, 8, zeus:flowers:clover))", + "tint(0, crop(16, 16, 16, 8, zeus:flowers:clover))", + "tint(0, crop(0, 24, 16, 8, zeus:flowers:clover))", + "tint(0, crop(16, 24, 16, 8, zeus:flowers:clover))" }, light_propagates = true, lowdef_render = false, diff --git a/subgames/zeus/mods/zeus_flowers/script/models/hash.lua b/subgames/zeus/mods/zeus_flowers/script/models/hash.lua index 546b655a..3d3e66a1 100644 --- a/subgames/zeus/mods/zeus_flowers/script/models/hash.lua +++ b/subgames/zeus/mods/zeus_flowers/script/models/hash.lua @@ -3,7 +3,20 @@ -- with multiple layers of topfaces. -- +local offset_amp = 0.2 +local amp = 0.025 + zepha.register_blockmodel('zeus:flowers:hash', { + mesh_mods = { + { + type = 'offset_x', + amplitude = offset_amp, + }, + { + type = 'offset_z', + amplitude = offset_amp, + } + }, parts = { { face = 'nocull', @@ -13,6 +26,10 @@ zepha.register_blockmodel('zeus:flowers:hash', { 4/16, 0, 1, 1, 1, 4/16, 0.5, 1, 1, 0, 4/16, 0.5, 0, 0, 0 + }, + shader_mod = { + type = 'sway_attached', + amplitude = amp } }, { face = 'nocull', @@ -22,6 +39,10 @@ zepha.register_blockmodel('zeus:flowers:hash', { 12/16, 0, 1, 0, 1, 12/16, 0, 0, 1, 1, 12/16, 0.5, 0, 1, 0 + }, + shader_mod = { + type = 'sway_attached', + amplitude = amp } }, { face = 'nocull', @@ -31,6 +52,10 @@ zepha.register_blockmodel('zeus:flowers:hash', { 1, 0, 12/16, 1, 1, 1, 0.5, 12/16, 1, 0, 0, 0.5, 12/16, 0, 0 + }, + shader_mod = { + type = 'sway_attached', + amplitude = amp } }, { face = 'nocull', @@ -40,6 +65,10 @@ zepha.register_blockmodel('zeus:flowers:hash', { 0, 0.5, 4/16, 1, 0, 1, 0.5, 4/16, 0, 0, 1, 0, 4/16, 0, 1 + }, + shader_mod = { + type = 'sway_attached', + amplitude = amp } }, { face = 'nocull', @@ -49,6 +78,10 @@ zepha.register_blockmodel('zeus:flowers:hash', { 0, 4/16, 1, 0, 1, 1, 4/16, 1, 1, 1, 1, 4/16, 0, 1, 0 + }, + shader_mod = { + type = 'sway_full_block', + amplitude = amp } }, { @@ -59,7 +92,11 @@ zepha.register_blockmodel('zeus:flowers:hash', { 0, 3/16, 1, 0, 1, 1, 3/16, 1, 1, 1, 1, 3/16, 0, 1, 0 - } + }, + shader_mod = { + type = 'sway_full_block', + amplitude = amp + } }, { face = 'nocull', @@ -69,6 +106,10 @@ zepha.register_blockmodel('zeus:flowers:hash', { 0, 2/16, 1, 0, 1, 1, 2/16, 1, 1, 1, 1, 2/16, 0, 1, 0 + }, + shader_mod = { + type = 'sway_full_block', + amplitude = amp } } } diff --git a/subgames/zeus/mods/zeus_flowers/textures/clover.png b/subgames/zeus/mods/zeus_flowers/textures/clover.png new file mode 100644 index 0000000000000000000000000000000000000000..7999a133cafbcc9985a3810e750810e09d268536 GIT binary patch literal 8763 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