├── .github └── workflows │ ├── on_bookshelf_release.yml │ └── tick.yml ├── .gitignore ├── README.md ├── _old_template ├── build.py ├── clean.py ├── config.py ├── libs │ ├── datapack │ │ └── _README.md │ └── resource_pack │ │ └── _README.md ├── merge │ ├── datapack │ │ └── _README.md │ └── resource_pack │ │ └── _README.md ├── upgrade_build.py ├── user │ ├── _README.md │ ├── link.py │ ├── setup_database.py │ └── setup_external_database.py └── watcher.py ├── docs ├── 1_project_structure.md ├── 2_database_setup.md ├── 3_writing_to_files.md ├── 4_external_dependencies.md ├── 5_miscellaneous.md ├── 6_migration_guide.md ├── 7_continuous_delivery.md ├── images │ └── in_game_manual_example.png └── specific_guides │ └── adding_a_sword.md ├── extensive_template ├── assets │ ├── _README.md │ ├── compress_ogg.py │ ├── force_mono.py │ ├── manual_overrides │ │ └── _README.md │ ├── mp3_to_ogg.py │ ├── optimize_textures.py │ ├── original_icon.png │ ├── python_datapack_1024x1024.png │ ├── python_datapack_64x64.png │ ├── records │ │ ├── Shazinho.ogg │ │ └── _README.md │ ├── sounds │ │ └── _README.md │ └── textures │ │ ├── _README.md │ │ ├── deepslate_steel_ore.png │ │ ├── manual.png │ │ ├── raw_steel.png │ │ ├── raw_steel_block.png │ │ ├── shazinho.png │ │ ├── steel_axe.png │ │ ├── steel_block.png │ │ ├── steel_boots.png │ │ ├── steel_chestplate.png │ │ ├── steel_dust.png │ │ ├── steel_helmet.png │ │ ├── steel_hoe.png │ │ ├── steel_ingot.png │ │ ├── steel_layer_1.png │ │ ├── steel_layer_2.png │ │ ├── steel_leggings.png │ │ ├── steel_nugget.png │ │ ├── steel_ore.png │ │ ├── steel_pickaxe.png │ │ ├── steel_shovel.png │ │ ├── steel_stick.png │ │ ├── steel_sword.png │ │ ├── stone_rod.png │ │ ├── stone_stick.png │ │ └── super_stone.png ├── beet.yml ├── build │ ├── datapack │ │ ├── data │ │ │ └── _your_namespace │ │ │ │ ├── function │ │ │ │ ├── bookshelf │ │ │ │ │ └── test.mcfunction │ │ │ │ ├── calls │ │ │ │ │ └── smart_ore_generation │ │ │ │ │ │ ├── generate_ores.mcfunction │ │ │ │ │ │ └── veins │ │ │ │ │ │ ├── deepslate_steel_ore_0.mcfunction │ │ │ │ │ │ ├── deepslate_steel_ore_1.mcfunction │ │ │ │ │ │ └── steel_ore.mcfunction │ │ │ │ ├── path │ │ │ │ │ └── to │ │ │ │ │ │ └── a │ │ │ │ │ │ └── random │ │ │ │ │ │ └── function │ │ │ │ │ │ └── i │ │ │ │ │ │ └── guess.mcfunction │ │ │ │ └── v2.0.0 │ │ │ │ │ ├── load │ │ │ │ │ └── confirm_load.mcfunction │ │ │ │ │ ├── minute.mcfunction │ │ │ │ │ ├── second_5.mcfunction │ │ │ │ │ └── tick_2.mcfunction │ │ │ │ └── jukebox_song │ │ │ │ └── shazinho.json │ │ └── pack.mcmeta │ └── resource_pack │ │ ├── assets │ │ └── _your_namespace │ │ │ ├── equipment │ │ │ └── steel.json │ │ │ ├── sounds │ │ │ └── shazinho.ogg │ │ │ └── textures │ │ │ └── entity │ │ │ └── equipment │ │ │ ├── humanoid │ │ │ └── steel_layer_1.png.png │ │ │ └── humanoid_leggings │ │ │ └── steel_layer_2.png.png │ │ └── pack.mcmeta └── src │ ├── link.py │ └── setup_database.py └── python_package ├── .gitignore ├── 1_upgrades.py ├── 2_build.py ├── 3_upload.py ├── LICENSE ├── README.md ├── TODO.md ├── all_doctests.py ├── all_in_one.py ├── build_all_in_one.py ├── github_release.py ├── pyproject.toml ├── scripts ├── check_bookshelf_version.py └── on_bookshelf_release.py ├── src ├── python_datapack │ ├── __init__.py │ ├── __main__.py │ ├── __memory__.py │ ├── cli.py │ ├── compatibilities │ │ ├── main.py │ │ ├── neo_enchant.py │ │ └── simpledrawer.py │ ├── datapack │ │ ├── basic_structure.py │ │ ├── custom_block_ticks.py │ │ ├── custom_blocks.py │ │ ├── headers.py │ │ ├── lang.py │ │ ├── loading.py │ │ ├── loot_tables.py │ │ ├── main.py │ │ └── recipes.py │ ├── debug_info.py │ ├── enhance_config.py │ ├── finalyze.py │ ├── initialize.py │ ├── manual │ │ ├── assets │ │ │ ├── furnace.png │ │ │ ├── heavy_workbench.png │ │ │ ├── invisible_item.png │ │ │ ├── invisible_item_release.png │ │ │ ├── minecraft_font.ttf │ │ │ ├── none.png │ │ │ ├── none_release.png │ │ │ ├── pulverizing.png │ │ │ ├── shaped_2x2.png │ │ │ ├── shaped_3x3.png │ │ │ ├── simple_case_no_border.png │ │ │ ├── wiki_information.png │ │ │ ├── wiki_ingredient_of_craft.png │ │ │ ├── wiki_ingredient_of_craft_template.png │ │ │ └── wiki_result_of_craft.png │ │ ├── book_components.py │ │ ├── book_optimizer.py │ │ ├── craft_content.py │ │ ├── image_utils.py │ │ ├── iso_renders.py │ │ ├── main.py │ │ ├── other_utils.py │ │ ├── page_font.py │ │ ├── shared_import.py │ │ └── text_components.py │ ├── resource_pack │ │ ├── check_unused_textures.py │ │ ├── item_models.py │ │ ├── main.py │ │ ├── power_of_2.py │ │ ├── sounds.py │ │ └── source_lore_font.py │ ├── verify_database.py │ └── watcher.py └── stewbeet │ ├── __init__.py │ ├── __main__.py │ ├── cli.py │ ├── core │ ├── __init__.py │ ├── __memory__.py │ ├── constants.py │ ├── database_helper │ │ ├── __init__.py │ │ ├── completion.py │ │ ├── equipments.py │ │ ├── materials.py │ │ ├── records.py │ │ ├── smart_ore_generation.py │ │ └── text.py │ ├── ingredients.py │ └── utils │ │ └── io.py │ ├── dependencies │ ├── bookshelf.py │ ├── bookshelf_config.json │ ├── datapack │ │ ├── Bookshelf Bitwise.zip │ │ ├── Bookshelf Block.zip │ │ ├── Bookshelf Color.zip │ │ ├── Bookshelf Dump.zip │ │ ├── Bookshelf Environment.zip │ │ ├── Bookshelf Generation.zip │ │ ├── Bookshelf Health.zip │ │ ├── Bookshelf Hitbox.zip │ │ ├── Bookshelf Id.zip │ │ ├── Bookshelf Interaction.zip │ │ ├── Bookshelf Link.zip │ │ ├── Bookshelf Log.zip │ │ ├── Bookshelf Math.zip │ │ ├── Bookshelf Move.zip │ │ ├── Bookshelf Position.zip │ │ ├── Bookshelf Random.zip │ │ ├── Bookshelf Raycast.zip │ │ ├── Bookshelf Schedule.zip │ │ ├── Bookshelf Sidebar.zip │ │ ├── Bookshelf Spline.zip │ │ ├── Bookshelf String.zip │ │ ├── Bookshelf Time.zip │ │ ├── Bookshelf Tree.zip │ │ ├── Bookshelf Vector.zip │ │ ├── Bookshelf View.zip │ │ ├── Bookshelf Xp.zip │ │ ├── Common Signals.zip │ │ ├── Furnace NBT Recipes.zip │ │ ├── ItemIO.zip │ │ ├── SmartOreGeneration.zip │ │ ├── Smithed Crafter.zip │ │ └── Smithed Custom Block.zip │ ├── main.py │ └── resource_pack │ │ └── Smithed Crafter.zip │ └── plugins │ ├── archive │ ├── __init__.py │ └── make_archive.py │ ├── auto │ ├── headers │ │ ├── __init__.py │ │ └── object.py │ └── lang_file │ │ └── __init__.py │ ├── compatibilities │ ├── neo_enchant │ │ └── __init__.py │ └── simpledrawer │ │ └── __init__.py │ ├── compute_sha1 │ └── __init__.py │ ├── copy_to_destination │ └── __init__.py │ ├── custom_recipes │ └── __init__.py │ ├── datapack │ ├── custom_blocks │ │ └── __init__.py │ ├── loading │ │ └── __init__.py │ └── loot_tables │ │ └── __init__.py │ ├── finalyze │ ├── basic_datapack_structure │ │ └── __init__.py │ ├── check_unused_textures │ │ └── __init__.py │ ├── custom_blocks_ticking │ │ └── __init__.py │ └── dependencies │ │ └── __init__.py │ ├── ingame_manual │ └── __init__.py │ ├── initialize │ └── __init__.py │ ├── merge_smithed_weld │ ├── __init__.py │ └── weld.py │ └── resource_pack │ ├── check_power_of_2 │ └── __init__.py │ ├── item_models │ └── __init__.py │ └── sounds │ └── __init__.py └── upgrade.py /.github/workflows/on_bookshelf_release.yml: -------------------------------------------------------------------------------- 1 | 2 | name: Bookshelf latest release download 3 | 4 | on: 5 | workflow_call: 6 | workflow_dispatch: 7 | # Triggered by the tick.yml workflow 8 | 9 | jobs: 10 | process-release: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - name: Checkout repo 15 | uses: actions/checkout@v4 16 | 17 | - name: Set up Python 18 | uses: actions/setup-python@v5 19 | with: 20 | python-version: '3.10' 21 | 22 | - name: Install dependencies 23 | run: | 24 | pip install stouputils 25 | 26 | - name: Run the bookshelf release script 27 | run: python python_package/scripts/on_bookshelf_release.py 28 | 29 | - name: Commit and push changes 30 | env: 31 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 32 | run: | 33 | git config user.name github-actions 34 | git config user.email github-actions@github.com 35 | git add . 36 | git diff --cached --quiet || git commit -m "chore: 🔨 Updated Bookshelf modules" 37 | git push https://x-access-token:${GITHUB_TOKEN}@github.com/${{ github.repository }} HEAD:${{ github.ref_name }} 38 | 39 | -------------------------------------------------------------------------------- /.github/workflows/tick.yml: -------------------------------------------------------------------------------- 1 | 2 | name: Bookshelf version check 3 | 4 | on: 5 | schedule: 6 | # Run every 24 hours at midnight UTC 7 | - cron: '0 0 * * *' 8 | 9 | # Allow manual trigger 10 | workflow_dispatch: 11 | 12 | jobs: 13 | check-for-updates: 14 | runs-on: ubuntu-latest 15 | 16 | outputs: 17 | update_available: ${{ steps.check-version.outputs.update_available }} 18 | 19 | steps: 20 | - name: Checkout repo 21 | uses: actions/checkout@v4 22 | 23 | - name: Set up Python 24 | uses: actions/setup-python@v5 25 | with: 26 | python-version: '3.10' 27 | 28 | - name: Install dependencies 29 | run: | 30 | pip install stouputils 31 | 32 | - name: Check for new version 33 | id: check-version 34 | run: | 35 | # Run a Python script to check for new version 36 | # If a new version is found, set the output variable 37 | UPDATE_AVAILABLE=$(python python_package/scripts/check_bookshelf_version.py) 38 | echo "update_available=$UPDATE_AVAILABLE" >> $GITHUB_OUTPUT 39 | echo "Update available: $UPDATE_AVAILABLE" 40 | 41 | # New job to call the reusable workflow conditionally 42 | release: 43 | needs: check-for-updates 44 | if: needs.check-for-updates.outputs.update_available == 'true' 45 | uses: ./.github/workflows/on_bookshelf_release.yml 46 | 47 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | env 3 | venv 4 | .env 5 | .venv 6 | __pycache__ 7 | continuous_delivery/ 8 | upload.py 9 | perf.html 10 | .beet_cache 11 | 12 | -------------------------------------------------------------------------------- /_old_template/build.py: -------------------------------------------------------------------------------- 1 | 2 | # Install required package 3 | import sys 4 | import os 5 | try: 6 | from python_datapack import build_process 7 | except ImportError as e: 8 | import traceback 9 | traceback.print_exc() 10 | 11 | print("\npython_datapack package not found, installing it...") 12 | os.system(f"{sys.executable} -m pip install python_datapack") 13 | print("\npython_datapack package has been installed.\nPlease restart the build script!") 14 | sys.exit(-1) 15 | 16 | # Setup config 17 | from config import configuration 18 | 19 | # Import my code 20 | from user.setup_database import main as setup_database_main 21 | from user.setup_external_database import main as setup_external_database_main 22 | from user.link import main as link_main # Called near the end of the build process 23 | 24 | # Run build process 25 | if __name__ == "__main__": 26 | build_process(configuration, setup_database_main, setup_external_database_main, link_main) 27 | 28 | -------------------------------------------------------------------------------- /_old_template/clean.py: -------------------------------------------------------------------------------- 1 | 2 | # Imports 3 | from config import configuration 4 | import shutil 5 | import os 6 | 7 | # Delete build folder 8 | if configuration.get("build_folder") is not None: 9 | shutil.rmtree(configuration["build_folder"], ignore_errors = True) 10 | print(f"Deleted build folder '{configuration['build_folder']}'") 11 | 12 | # Delete manual folder 13 | if configuration.get("manual_path") is not None: 14 | shutil.rmtree(configuration["manual_path"], ignore_errors = True) 15 | print(f"Deleted manual folder '{configuration['manual_path']}'") 16 | 17 | # Delete database_debug, debug_manual, and cmd_cache json files 18 | for x in ["database_debug", "manual_debug", "cmd_cache"]: 19 | if configuration.get(x) is not None: 20 | if os.path.exists(configuration[x]): 21 | os.remove(configuration[x]) 22 | print(f"Deleted {x} file '{configuration[x]}'") 23 | 24 | -------------------------------------------------------------------------------- /_old_template/libs/datapack/_README.md: -------------------------------------------------------------------------------- 1 | 2 | ## libs/datapack folder 3 | Every .zip file will be recognized as a datapack and will be used for weld merging (if enabled) and copied to build destination (if enabled). 4 | 5 | -------------------------------------------------------------------------------- /_old_template/libs/resource_pack/_README.md: -------------------------------------------------------------------------------- 1 | 2 | ## libs/resource_pack folder 3 | Every .zip file will be recognized as a resource pack and will be used for weld merging (if enabled) and copied to build destination (if enabled). 4 | 5 | -------------------------------------------------------------------------------- /_old_template/merge/datapack/_README.md: -------------------------------------------------------------------------------- 1 | 2 | ## merge/datapack folder 3 | Every file and subfolders will be merged to the build datapack folder. 4 | 5 | -------------------------------------------------------------------------------- /_old_template/merge/resource_pack/_README.md: -------------------------------------------------------------------------------- 1 | 2 | ## merge/resource_pack folder 3 | Every file and subfolders will be merged to the build resource_pack folder. 4 | 5 | -------------------------------------------------------------------------------- /_old_template/upgrade_build.py: -------------------------------------------------------------------------------- 1 | 2 | # Simple command to upgrade the python package 3 | import sys 4 | import os 5 | os.system(f"{sys.executable} -m pip install --upgrade python_datapack") 6 | 7 | -------------------------------------------------------------------------------- /_old_template/user/_README.md: -------------------------------------------------------------------------------- 1 | 2 | # User folder 3 | ## setup_database.py 4 | This script is used to setup the database for the datapack. 5 | If your datapack needs to create custom items or blocks with recipes (or not), you must setup the dictionnary in this function. 6 | See the example in the file for more information. 7 | 8 | ## setup_external_database.py 9 | This script is used to setup the external database for the datapack. 10 | When you are using items or blocks from another datapack in recipes, you must setup the dictionnary in this function. 11 | The format is the same as the setup_database.py file. 12 | 13 | ## link.py 14 | This script is used to run your own python code before the build process finalize. 15 | You can use this script to dynamically write files in the build folder. 16 | If so, you must use the efficient `write_file` function in `python_datapack.utils.io` module. 17 | 18 | -------------------------------------------------------------------------------- /_old_template/user/link.py: -------------------------------------------------------------------------------- 1 | 2 | # Imports 3 | from beet import Context 4 | from python_datapack.utils.database_helper import * 5 | 6 | 7 | # Main function is run just before making finalyzing the build process (zip, headers, lang, ...) 8 | def main(config: dict) -> None: 9 | 10 | # Get the namespace and database (if needed, actually not needed here) 11 | database: dict[str, dict] = config["database"] 12 | namespace: str = config["namespace"] 13 | beet_ctx: Context = config["beet_ctx"] 14 | #beet_ctx.data[f"{namespace}:beet_test"] = Function("say beet!") 15 | print(beet_ctx.data.functions) 16 | 17 | # Generate ores in the world 18 | CustomOreGeneration.all_with_config(config, ore_configs = { 19 | "steel_ore": [ 20 | CustomOreGeneration( 21 | dimensions = ["minecraft:overworld","stardust:cavern","some_other:dimension"], 22 | maximum_height = 50, 23 | minimum_height = 0, 24 | veins_per_region = 2, 25 | vein_size_logic = 0.4, 26 | ) 27 | ], 28 | "deepslate_steel_ore": [ 29 | CustomOreGeneration( 30 | dimensions = ["minecraft:overworld"], 31 | maximum_height = 0, 32 | veins_per_region = 2, 33 | vein_size_logic = 0.4, 34 | ), 35 | CustomOreGeneration( 36 | dimensions = ["stardust:cavern"], 37 | maximum_height = 0, 38 | veins_per_region = 8, 39 | vein_size_logic = 0.8, 40 | ) 41 | ], 42 | }) 43 | 44 | 45 | # Add some commands when loading datapack 46 | write_load_file(config, """ 47 | # Add a message when loading 48 | say Here is a message when loading the datapack, located in `user/link.py` 49 | """) 50 | # write_versioned_file(config, "load/confirm_load", ...) <- This is the same as the previous line 51 | 52 | 53 | ## Clock functions 54 | # When you write to the following files: "tick_2", "second", "second_5", "minute"... the tick function will automatically call them, ex: 55 | write_versioned_function(config, "minute", f"execute if score #spam {namespace}.data matches 1 run say This is a message every minute\n") 56 | write_versioned_function(config, "second_5", f"execute if score #spam {namespace}.data matches 1 run say This is a SPAM message every 5 seconds\n") 57 | write_versioned_function(config, "tick_2", f"execute if score #spam {namespace}.data matches 1 run say This is a SPAM message every 2 ticks\n") 58 | # The two following functions calls are equivalent: 59 | # write_tick_file(config, ...) 60 | # write_versioned_file(config, "tick", ...) 61 | 62 | # Create a random function 63 | write_function(config, f"{namespace}:path/to/a/random/function/i/guess", "say Hello world!") 64 | 65 | # Call a bookshelf module (Every single module from https://docs.mcbookshelf.dev/en/latest/ is supported) 66 | write_function(config, f"{namespace}:bookshelf/test", """ 67 | # Once 68 | scoreboard players set $math.divide.x bs.in 9 69 | scoreboard players set $math.divide.y bs.in 5 70 | function #bs.math:divide 71 | tellraw @a [{"text": "9 / 5 = ", "color": "dark_gray"},{"score":{"name":"$math.divide", "objective": "bs.out"}, "color": "gold"}] 72 | """) 73 | 74 | pass 75 | 76 | -------------------------------------------------------------------------------- /_old_template/user/setup_database.py: -------------------------------------------------------------------------------- 1 | 2 | # Here is a useful link if you want to override autogenerated models: https://github.com/PixiGeko/Minecraft-default-assets/tree/latest/assets/minecraft/models/block 3 | 4 | # Import database helper and setup constants 5 | from python_datapack.utils.database_helper import * 6 | from python_datapack.utils.ingredients import * 7 | from python_datapack.constants import * 8 | 9 | # Configuration to generate everything about the material based on "steel_ingot" 10 | ORES_CONFIGS: dict[str, EquipmentsConfig|None] = { 11 | "steel_ingot": EquipmentsConfig( 12 | # This steel is equivalent to iron, 13 | equivalent_to = DEFAULT_ORE.IRON, 14 | 15 | # But, has more durability (3 times more) 16 | pickaxe_durability = 3 * VanillaEquipments.PICKAXE.value[DEFAULT_ORE.IRON]["durability"], 17 | 18 | # And, does 1 more damage per hit (mainhand), and has 0.5 more armor, and mines 20% faster (pickaxe) 19 | attributes = {"attack_damage": 1, "armor": 0.5, "mining_efficiency": 0.2} 20 | ), 21 | 22 | # Simple material stone, this will automatically detect stone stick and rod textures. 23 | "minecraft:stone": None, 24 | } 25 | 26 | # Main function should return a database 27 | def main(config: dict) -> dict[str, dict]: 28 | database: dict[str,dict] = {} 29 | namespace: str = config["namespace"] 30 | 31 | # Generate ores in database (add every stuff (found in the textures folder) related to the given materials, to the database) 32 | generate_everything_about_these_materials(config, database, ORES_CONFIGS) 33 | 34 | database["steel_ingot"][WIKI_COMPONENT] = [ 35 | {"text":"Here is an example of a wiki component, this text component will be displayed as a button in the manual.\n"}, 36 | {"text":"You can write anything you want here.","color":"yellow"}, 37 | ] 38 | 39 | # Generate custom disc records 40 | generate_custom_records(config, database, "auto") 41 | 42 | # Add a super stone block that can be crafted with 9 deepslate or stone, and has cobblestone as base block 43 | database["super_stone"] = { 44 | "id": CUSTOM_BLOCK_VANILLA, # Placeholder for the base block 45 | VANILLA_BLOCK: {"id": "minecraft:cobblestone", "apply_facing": False}, # Base block 46 | RESULT_OF_CRAFTING: [ # Crafting recipes (shaped and shapeless examples) 47 | {"type":"crafting_shaped","result_count":1,"group":"super_stone","category":"blocks","shape":["XXX","XXX","XXX"],"ingredients": {"X": ingr_repr("minecraft:stone")}}, 48 | {"type":"crafting_shapeless","result_count":1,"group":"super_stone","category":"blocks","ingredients": 9 * [ingr_repr("minecraft:deepslate")] }, 49 | ], 50 | # Exemple of recipe with vanilla result (not custom item) 51 | USED_FOR_CRAFTING: [ 52 | {"type":"smelting","result_count":1,"cookingtime":200,"experience":0.1,"group":"super_stone","category":"blocks","ingredient":ingr_repr("super_stone", namespace),"result":ingr_repr("minecraft:diamond")}, 53 | ] 54 | } 55 | 56 | # Don't forget to add the vanilla blocks for the custom blocks (not automatic even though they was recognized by generate_everything_about_these_materials()) 57 | database["steel_block"][VANILLA_BLOCK] = {"id": "minecraft:iron_block", "apply_facing": False} # Placeholder for the base block 58 | database["raw_steel_block"][VANILLA_BLOCK] = {"id": "minecraft:raw_iron_block", "apply_facing": False} # Placeholder for the base block 59 | 60 | # Add a recipe for the future generated manual (the manual recipe will show up in itself) 61 | manual_name: str = config.get("manual_name", "Manual") 62 | database["manual"] = { 63 | "id": "minecraft:written_book", "category": "misc", "item_name": f'"{manual_name}"', 64 | RESULT_OF_CRAFTING: [ 65 | # Put a book and a steel ingot in the crafting grid to get the manual 66 | {"type":"crafting_shapeless","result_count":1,"group":"manual","category":"misc","ingredients": [ingr_repr("minecraft:book"), ingr_repr("steel_ingot", namespace)]}, 67 | 68 | # Put the manual in the crafting grid to get the manual back (update the manual) 69 | {"type":"crafting_shapeless","result_count":1,"group":"manual","category":"misc","ingredients": [ingr_repr("manual", namespace)]}, 70 | ], 71 | } 72 | 73 | # Add item categories to the remaining items (should select 'shazinho' and 'super_stone') 74 | for item in database.values(): 75 | if not item.get("category"): 76 | item["category"] = "misc" 77 | 78 | # Final adjustments, you definitively should keep them! 79 | add_item_model_component(config, database, black_list = ["item_ids","you_don't_want","in_that","list"]) 80 | add_item_name_and_lore_if_missing(config, database) 81 | add_private_custom_data_for_namespace(config, database) # Add a custom namespace for easy item detection 82 | add_smithed_ignore_vanilla_behaviours_convention(database) # Smithed items convention 83 | print() 84 | 85 | # Return database 86 | return database 87 | 88 | -------------------------------------------------------------------------------- /_old_template/user/setup_external_database.py: -------------------------------------------------------------------------------- 1 | 2 | # Import database helper (from python_datapack, containing database helper functions) 3 | from python_datapack.utils.database_helper import * 4 | 5 | # WARNING: using an external database is currently very scuffed, 6 | # until I find a good way to include external datapack items in a recipe. 7 | 8 | # Main function should return a database 9 | def main(config: dict) -> dict[str, dict]: 10 | database: dict[str, dict] = {} 11 | 12 | # Add your code to set database 13 | pass 14 | 15 | # Return database 16 | return database 17 | 18 | -------------------------------------------------------------------------------- /_old_template/watcher.py: -------------------------------------------------------------------------------- 1 | 2 | # Install required packages 3 | import sys 4 | import os 5 | try: 6 | from python_datapack import check_config_format 7 | except ImportError: 8 | os.system(f"{sys.executable} -m pip install python_datapack watchdog") 9 | print("\nRequired modules have been installed.\nPlease restart the watcher script") 10 | sys.exit(-1) 11 | 12 | # Import configuration and watcher 13 | import stouputils as stp 14 | from config import configuration, ROOT 15 | from python_datapack.watcher import watcher 16 | 17 | 18 | # Main 19 | if __name__ == "__main__": 20 | if not check_config_format(configuration): 21 | stp.error("Invalid config format, please check the documentation") 22 | 23 | # Setup and start file watcher 24 | configs_to_get: list[str] = ["merge_folder", "assets_folder", "libs_folder"] 25 | to_watch: list[str] = [configuration[x] + '/' for x in configs_to_get if configuration.get(x, None) is not None] 26 | to_watch += [ 27 | f"{ROOT}/user/", 28 | f"{ROOT}/config.py", 29 | ] 30 | to_ignore: list[str] = [ 31 | "__pycache__/", 32 | ".git/", 33 | ".venv/", 34 | ] 35 | 36 | # Start the watcher 37 | watcher(to_watch, to_ignore, f"{ROOT}/build.py") 38 | 39 | -------------------------------------------------------------------------------- /docs/3_writing_to_files.md: -------------------------------------------------------------------------------- 1 | 2 | # 🗃️ Writing to files 3 | Dealing with files in any programming language can be somewhat annoying. When you want to prepend something to a file, you need to read the content, add the text your want to add and rewrite the file leading sometimes to excessive disk access and unreadable code 🌀.
4 | To avoid this issue, the `python_datapack` module brings a system for writing files along with utility functions through the `python_datapack.utils.io` import.
5 | `FILES_TO_WRITE`: This global variable is a dictionnary of files paths leading to the content of the file, meaning all files are stored into RAM. You can then use python string arithmetic and functions 💾. 6 | 7 | 8 | ## 📚 Reading and writing functions 9 | - `read_file()`: 📖 This function takes a file path and returns its content without raising any error if the file path doesn't exists 10 | - `write_file()`: ✍️ Taking the file path and its content, you can indicate if you want to prepend the content, or if you want to overwrite it. If you write to a json file but there are already content in it, both content will be merged unless you put the overwrite boolean to true. 11 | - `write_function()`: 📝 Write content to a function file using Minecraft function notation (e.g., "namespace:folder/function_name"). If no namespace is provided, it defaults to "minecraft". 12 | 13 | 14 | ## 🔧 Versioned functions 15 | As you may know, a few functions such as the load and tick functions 🗂️ are located in a folder containing the version of your datapack with a path like `build/datapack/data//function/v1.0.0/tick.mcfunction`.
16 | Writing the entire file path again and again can be sometimes annoying, so I created 3 functions to help you out: 17 | - `write_load_file()`: ⚡ Allows you to write directly to the load file. 18 | - `write_tick_file()`: ⏱️ Allows you to write directly to the tick file. 19 | - `write_versioned_file()`: ⏳ This function will automatically prepend the whole path to the folder and append `.mcfunction` at the end, so to write to the tick file for example you can give this relative path: `tick`. 20 | 21 | 22 | ## 🗑️ File Management 23 | The module provides several functions for managing files: 24 | - `delete_file()`: 🚫 Delete a single file from both the write queue and optionally from disk 25 | - `delete_files()`: 🧹 Delete multiple files that contain a specific string in their path 26 | - `write_all_files()`: 💾 Write all queued files to disk, optionally filtering by path (you'll probably don't need this unless you are debugging or use an external tool to read the files) 27 | 28 | 29 | ## 📚 Conclusion 30 | This guide was pretty much simple as there wasn't much to cover but I hope you understood it well!
31 | Thank you for reading 🙌. 32 | 33 | -------------------------------------------------------------------------------- /docs/4_external_dependencies.md: -------------------------------------------------------------------------------- 1 | 2 | # 🗃️ Using External Libraries 📚 3 | When creating datapacks, you might want to use items from other datapacks in your recipes or interact with their functionality.
4 | Or maybe, you want to use a library for your project.
5 | This documentation will explain how to do both! ✨ 6 | 7 | 8 | ## 🔗 Adding Dependencies 9 | In order to automatically check for the dependencies, you'll need to add them in your [`config.py`](../config.py) file. 10 | 11 | ```python 12 | DEPENDENCIES: dict[str, dict] = { 13 | # Automagically, the datapack will check for the presence of dependencies and their minimum required versions at runtime 14 | # The url is used when the dependency is not found to suggest where to get it 15 | # The version dict key contains the minimum required version of the dependency in [major, minor, patch] format 16 | # The main key is the dependency namespace to check for 17 | # The name can be whatever you want, it's just used in messages 18 | 19 | # Example for DatapackEnergy >= 1.8.0 20 | "energy": {"version":[1, 8, 0], "name":"DatapackEnergy", "url":"https://github.com/ICY105/DatapackEnergy"}, 21 | } 22 | ``` 23 | 🔍 By default, an example dependency is already added to the template as you can see above.
24 | The only purpose of this constant is to automatically check for the presence of the dependency and suggest where to get it if not found on load.
25 | ⚠️ DISCLAIMER: The dependency must use [LanternLoad](https://github.com/LanternMC/load) with [proper versioning](https://github.com/Gunivers/Bookshelf/issues/272) (e.g. `#your_namespace.major load.status` scoreboard, and same for `minor` and `patch`). 26 | 27 | Now, you can add the datapack/library to your `libs` folder and configure whether it should be merged with your pack using the `MERGE_LIBS` option.
28 | When building, the `libs/datapack` folder will be copied to the `BUILD_COPY_DESTINATIONS` folders (only datapacks, not resourcepacks). 29 | 30 | 31 | ## 📦 External Database Setup 32 | The 📝 [`setup_external_database.py`](../user/setup_external_database.py) script is used to define items from other datapacks that you want to use in your recipes 🛠️.
33 | This currently is the only use-case for the external database 📦.
34 | ⚠️ DISCLAIMER: This script is currently very scuffed, I'm still figuring out how to properly handle external items. 35 | 36 | 37 | ## 📚 Conclusion 38 | Now you know how to: 39 | - Add dependencies to your project and check for their presence at runtime 40 | - Configure whether libraries should be merged with your pack 41 | - Set up an external database to use items from other datapacks in your recipes 42 | This will help you create more complex datapacks that interact with other datapacks and libraries! 43 | Thank you for reading 🙌. 44 | 45 | -------------------------------------------------------------------------------- /docs/5_miscellaneous.md: -------------------------------------------------------------------------------- 1 | 2 | # 🔧 Miscellaneous Features 3 | This documentation covers various features and examples that didn't really fit in other categories. 4 | 5 | 6 | ## 🗄️ SimpleDrawer's Compacting Drawer 7 | If you're adding custom materials to your datapack, you might want to add compatibility for SimpleDrawer's compacting drawers 🗄️.
8 | By default, the process is handled automatically and will recognize patterns in your recipes to create compacting drawer recipes ✨.
9 | For instance, 10 | - If you have a steel block 🧊, steel ingots ⚔️ and steel nuggets 💎, having recipes that link them, 11 | - It will create a compacting drawer recipe that turns steel ingots into a steel block and steel nuggets into a steel ingot. 12 | - The recipe ratio is by default `1:9:81`, but the program will detect the ratio used in your recipes and use the same one. 13 | - For instance, if you have a `1:4:16` ratio defined in your recipes, it will create a `1:4:16` ratio for the compacting drawer recipe. 14 | 15 | In case you don't want this behavior, you can still use `delete_files("simpledrawer/")` during link process to remove the compacting drawer recipes.
16 | They are generated at [`your_namespace:calls/simpledrawer/material`](../build/datapack/data/your_namespace/function/calls/simpledrawer/material.mcfunction) 📁. 17 | 18 | 19 | ## 🔮 SimplEnergy's Pulverizer Items 20 | If you're adding custom materials to your datapack, and you have a dust item, you might want to add compatibility for SimplEnergy's pulverizer 🔮.
21 | By default, the process is handled automatically and will recognize patterns in your recipes to create pulverizer recipes ✨.
22 | For instance, 23 | - If you have raw steel 🔩 (or steel ore) and steel ingots ⚔️, 24 | - It will create a pulverizer recipe that turns raw steel into 2x steel dust 💧. 25 | - Those 2 steel dust can be smelted into one steel ingot each. 26 | - Additionally, you can pulverize 1x steel ingot into 1x steel dust. 27 | 28 | In case you don't want this behavior, you can still use `delete_files("simplenergy/")` during link process to remove the pulverizer recipes.
29 | They are generated at [`your_namespace:calls/simplenergy/pulverizer_recipes`](../build/datapack/data/your_namespace/function/calls/simplenergy/pulverizer_recipes.mcfunction) 📁. 30 | 31 | 32 | ## 🔓 Recipe Unlocking 33 | By default, vanilla recipes (ones that don't use custom items in ingredients) will be automatically unlocked for players whenever they discover an ingredient of said recipe.
34 | This behavior can be changed by deleting the advancement [`unlock_recipes.json`](../build/datapack/data/your_namespace/advancement/unlock_recipes.json) along with the reward function [`unlock_recipes.mcfunction`](../build/datapack/data/your_namespace/function/advancements/unlock_recipes.mcfunction) 📁. 35 | 36 | 37 | ## ⏰ Clock Functions for Custom Blocks 38 | When creating custom blocks ⚡, you often need to run functions every tick or at regular intervals ⏰.
39 | For all custom blocks, the program will check if a function named `tick` or `second` is present in the custom block's folder 📁.
40 | If it is, the custom block will be granted additional tags on placement to identify it as a ticking block 🏷️.
41 | Then, proper functions will be created to run the `tick` or `second` code at the right time for each detected custom block 🔄.
42 | For instance, 43 | - If you want an electric furnace to run a function every tick ⚡, you simply add a [`tick.mcfunction`](https://github.com/Stoupy51/SimplEnergy/blob/main/build/datapack/data/simplenergy/function/custom_blocks/electric_furnace/tick.mcfunction) file in the electric furnace's folder 📁.
44 | - Same goes for a `second.mcfunction` file, if you want the custom block to run a function every 20 ticks ⏳. 45 | - The program will do everything else automatically, so you don't have to worry about a thing ✨. 46 | 47 | 48 | ## 📚 Conclusion 49 | You now know how to: 50 | - Create compacting drawer recipes for SimpleDrawer 51 | - Add custom dust items for SimplEnergy's pulverizer 52 | - Control recipe unlocking through advancements 53 | - Set up clock functions for custom blocks 54 | 55 | These features will help you create more complex and interactive datapacks!
56 | Thank you for reading 🙌 57 | 58 | -------------------------------------------------------------------------------- /docs/images/in_game_manual_example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stoupy51/PythonDatapackTemplate/701c87f48c817e69aa45ff247f33eeac2841cff2/docs/images/in_game_manual_example.png -------------------------------------------------------------------------------- /docs/specific_guides/adding_a_sword.md: -------------------------------------------------------------------------------- 1 | 2 | # ⚔️ Adding a Custom Sword 3 | This guide will show you two different ways to add a custom sword named `test_sword` to your project.
4 | First, you'll have to put the `test_sword.png` texture file in the [`assets/textures`](../../assets/textures/) folder.
5 | Then, you'll have to add it to the database with one of the following methods through the [`setup_database.py`](../../user/setup_database.py) script.
6 | I **highly recommend** using the second method, as it is easier and faster. 7 | 8 | 9 | ## 📝 Method 1: Direct Database Entry 10 | The first method involves directly defining the sword in your database.
11 | I'll go progressively through the code, explaining each part.
12 | Item definition should be written in the `main()` function: 13 | ```py 14 | ... 15 | def main(): 16 | ... 17 | database["test_sword"] = {...} 18 | ... 19 | ``` 20 | 21 | The simplest way is to simply define the item in the database like this: 22 | ```py 23 | database["test_sword"] = {"id": "minecraft:diamond_sword"} 24 | ``` 25 | This will automatically generate the sword with the same properties as the diamond sword and use the `test_sword.png` texture. 26 | 27 | You may want to customize the properties of the sword, such as its durability or attributes.
28 | You can do this by adding more keys to the dictionary: 29 | ```py 30 | database["test_sword"] = { 31 | "id": "minecraft:diamond_sword", # Base item 32 | 33 | # Components that will be added to the item like '/give @s diamond_sword[max_damage=1000,attribute_modifiers=[...]]' 34 | "max_damage": 1000, # Custom durability 35 | "attribute_modifiers": [{ # Custom attributes, here we add 8 attack damage 36 | "type": "attack_damage", 37 | "amount": 8, 38 | "operation": "add_value", 39 | "slot": "mainhand", 40 | "id": "minecraft:base_attack_damage" 41 | }] 42 | } 43 | ``` 44 | 45 | Finally, if you come up with a fully custom sword, you can define it like this: 46 | ```py 47 | database["test_sword"] = { 48 | # Base item definition 49 | "id": "minecraft:diamond_sword", # Using diamond sword as base 50 | "category": "combat", # For in-game manual organization 51 | 52 | # Item properties 53 | "max_damage": 1000, # Durability (lower than diamond sword) 54 | "attribute_modifiers": [{ # Combat stats 55 | "type": "attack_damage", 56 | "amount": 8, # Base damage (7 = diamond sword + 1) 57 | "operation": "add_value", 58 | "slot": "mainhand", 59 | "id": f"{namespace}:attack_damage.mainhand" 60 | }], 61 | 62 | # Crafting recipe 63 | RESULT_OF_CRAFTING: [{ 64 | "type": "crafting_shaped", 65 | "category": "combat", 66 | "group": "test_sword", 67 | "shape": ["X", "X", "S"], 68 | "ingredients": { 69 | "X": ingr_repr("test_ingot", namespace), # Your custom material (assuming you have a material named "test_ingot") 70 | "S": ingr_repr("minecraft:stick") # Vanilla stick 71 | } 72 | }] 73 | } 74 | ``` 75 | 76 | 77 | ## 🛠️ Method 2: Using EquipmentsConfig 78 | The second method uses the `EquipmentsConfig` helper to automatically generate equipment: 79 | 80 | ```py 81 | ORES_CONFIGS: dict[str, EquipmentsConfig|None] = { 82 | 83 | # "test_ingot" is the name of the material but doesn't have to exist. 84 | # The 'generate_everything_about_these_materials()' function will try to find every stuff related to it in the textures folder tho, such as a "test_sword.png" texture. 85 | "test_ingot": EquipmentsConfig( 86 | # Base this material's properties on diamond 87 | equivalent_to = DEFAULT_ORE.DIAMOND, 88 | 89 | # Durability is calculated based on the pickaxe durability of the material it's equivalent to. 90 | # Even though you might not use pickaxes, the durability is still calculated based on the pickaxe durability. 91 | # Here, we make all our stuff 3 times more durable than iron (including the "test_sword" that we'll get) 92 | pickaxe_durability = 3 * VanillaEquipments.PICKAXE.value[DEFAULT_ORE.IRON]["durability"], 93 | 94 | # Custom attributes: Adds +1 attack damage to every mainhand item, +0.5 armor points to every armor, and increases mining speed by 20% to every mainhand item (pickaxe, axe, hoe, ...) 95 | attributes = {"attack_damage": 1, "armor": 0.5, "mining_efficiency": 0.2} 96 | ) 97 | } 98 | 99 | ... 100 | 101 | # Generate everything about test_ingot (including sword) 102 | generate_everything_about_these_materials(config, database, ORES_CONFIGS) 103 | ``` 104 | 105 | 106 | ## 🔄 Comparison 107 | ### Method 1: Direct Database Entry 108 | **Pros:** 109 | - ✅ Complete control over every property 110 | - ✅ Clear and explicit definition 111 | - ✅ Can create unique items not based on vanilla equipment 112 | 113 | **Cons:** 114 | - ❌ More code to write 115 | - ❌ Need to manually set all properties 116 | - ❌ Need to remember attribute IDs and formats 117 | 118 | ### Method 2: EquipmentsConfig 119 | **Pros:** 120 | - ✅ Automatically generates all equipment types 121 | - ✅ Less code to write 122 | - ✅ Consistent with vanilla equipment patterns 123 | - ✅ Handles all attribute IDs automatically 124 | 125 | **Cons:** 126 | - ❌ There are no cons 127 | 128 | 129 | ## 📚 Conclusion 130 | You now know two ways to add custom swords to your datapack! 131 | - Use **EquipmentsConfig** for complete material sets 132 | - Use **Direct Database Entry** for unique items 133 | 134 | Choose the method that best fits your needs! 🎯
135 | Thank you for reading 🙌 136 | 137 | -------------------------------------------------------------------------------- /extensive_template/assets/_README.md: -------------------------------------------------------------------------------- 1 | 2 | ## Assets folder 3 | This folder should contains sounds and textures folders along with "original_icon.png" that will be used as the icon for datapack and resource pack. 4 | 5 | -------------------------------------------------------------------------------- /extensive_template/assets/compress_ogg.py: -------------------------------------------------------------------------------- 1 | 2 | # Script that compress all ogg files to 64 kbps by default 3 | # Requires ffmpeg to be installed. 4 | 5 | import os 6 | import subprocess 7 | from multiprocessing import Pool 8 | COMPRESSION = "64k" 9 | 10 | def compress_file(args): 11 | src, dst = args 12 | previous_size = os.path.getsize(src) 13 | subprocess.run(["ffmpeg", "-i", src, "-c:a", "libvorbis", "-b:a", COMPRESSION, dst], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) 14 | 15 | # Remove original & rename temp 16 | file_size = os.path.getsize(dst) 17 | if file_size < previous_size: 18 | os.remove(src) 19 | os.rename(dst, src) 20 | print(f"Compressed file '{src}' from {previous_size} to {file_size} bytes") 21 | else: 22 | os.remove(dst) 23 | print(f"New file '{src}' is bigger than the original ({previous_size} > {file_size})") 24 | 25 | if __name__ == "__main__": 26 | py_path = os.path.dirname(os.path.abspath(__file__)) 27 | 28 | files_to_compress = [] 29 | for root, _, files in os.walk(py_path): 30 | for file in files: 31 | if file.endswith(".ogg"): 32 | src = f"{root}/{file}" 33 | dst = src.replace(".ogg", ".temp.ogg") 34 | files_to_compress.append((src, dst)) 35 | 36 | # Compress 37 | cpu_count = os.cpu_count() // 2 + 1 38 | with Pool(processes = cpu_count) as pool: 39 | pool.map(compress_file, files_to_compress) 40 | print("Compression finished!") 41 | 42 | -------------------------------------------------------------------------------- /extensive_template/assets/force_mono.py: -------------------------------------------------------------------------------- 1 | 2 | # Script that forces all .ogg files to use mono channel. 3 | # Requires ffmpeg to be installed. 4 | 5 | import os 6 | import subprocess 7 | from multiprocessing import Pool 8 | from compress_ogg import COMPRESSION 9 | 10 | def convert_file(args): 11 | src, dst = args 12 | previous_size = os.path.getsize(src) 13 | subprocess.run(["ffmpeg", "-i", src, "-b:a", COMPRESSION, "-ac", "1", dst], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) 14 | 15 | # Remove original & rename temp 16 | file_size = os.path.getsize(dst) 17 | if file_size < previous_size: 18 | os.remove(src) 19 | os.rename(dst, src) 20 | print(f"Mono file '{src}' got from {previous_size} to {file_size} bytes") 21 | 22 | if __name__ == "__main__": 23 | py_path = os.path.dirname(os.path.abspath(__file__)) 24 | 25 | files_to_compress = [] 26 | for root, _, files in os.walk(py_path): 27 | for file in files: 28 | if file.endswith(".ogg"): 29 | src = f"{root}/{file}" 30 | dst = src.replace(".ogg", ".temp.ogg") 31 | files_to_compress.append((src, dst)) 32 | 33 | # Compress 34 | cpu_count = os.cpu_count() // 2 + 1 35 | with Pool(processes = cpu_count) as pool: 36 | pool.map(convert_file, files_to_compress) 37 | print("Compression finished!") 38 | 39 | -------------------------------------------------------------------------------- /extensive_template/assets/manual_overrides/_README.md: -------------------------------------------------------------------------------- 1 | 2 | ## Manual overrides folder 3 | This folder is used to override the manual assets. Every file in this folder will replace the [original ones](https://github.com/Stoupy51/python_datapack/tree/main/src/python_datapack/manual/assets) as long as the file name is the same.
4 | For instance, if you want to replace the `shaped_3x3.png` file, you need to create a `shaped_3x3.png` file in this folder.
5 | 6 | -------------------------------------------------------------------------------- /extensive_template/assets/mp3_to_ogg.py: -------------------------------------------------------------------------------- 1 | 2 | # Script that converts all mp3 files in and bellow the current directory to ogg files. 3 | # Requires ffmpeg to be installed. 4 | 5 | import os 6 | import subprocess 7 | 8 | py_path = os.path.dirname(os.path.abspath(__file__)) 9 | 10 | for root, _, files in os.walk(py_path): 11 | for file in files: 12 | if file.endswith(".mp3"): 13 | src = f"{root}/{file}" 14 | dst = src.replace(".mp3", ".ogg") 15 | subprocess.run(["ffmpeg", "-i", src, dst]) 16 | print("Conversion finished!") 17 | 18 | -------------------------------------------------------------------------------- /extensive_template/assets/optimize_textures.py: -------------------------------------------------------------------------------- 1 | 2 | # Get start time & Enable colors in Windows 10 console 3 | import os 4 | import stouputils as stp 5 | os.system("color") 6 | 7 | @stp.measure_time(message="Optimized textures") 8 | def main(): 9 | 10 | # For each texture in the textures folder, optimize it without loosing any quality 11 | from PIL import Image 12 | for root, _, files in os.walk("./"): 13 | for file in files: 14 | if not file.endswith(".png"): 15 | continue 16 | filepath = f"{root}/{file}" 17 | 18 | # Load image 19 | image = Image.open(filepath) 20 | image = image.convert("RGBA") 21 | pixels = image.load() 22 | width, height = image.size 23 | 24 | # Optimize image 25 | for x in range(width): 26 | for y in range(height): 27 | r, g, b, a = pixels[x, y] 28 | if a == 0: 29 | pixels[x, y] = (0, 0, 0, 0) 30 | 31 | # Save image 32 | image.save(filepath) 33 | stp.info(f"Optimized '{file}'") 34 | 35 | if __name__ == "__main__": 36 | main() 37 | 38 | -------------------------------------------------------------------------------- /extensive_template/assets/original_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stoupy51/PythonDatapackTemplate/701c87f48c817e69aa45ff247f33eeac2841cff2/extensive_template/assets/original_icon.png -------------------------------------------------------------------------------- /extensive_template/assets/python_datapack_1024x1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stoupy51/PythonDatapackTemplate/701c87f48c817e69aa45ff247f33eeac2841cff2/extensive_template/assets/python_datapack_1024x1024.png -------------------------------------------------------------------------------- /extensive_template/assets/python_datapack_64x64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stoupy51/PythonDatapackTemplate/701c87f48c817e69aa45ff247f33eeac2841cff2/extensive_template/assets/python_datapack_64x64.png -------------------------------------------------------------------------------- /extensive_template/assets/records/Shazinho.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stoupy51/PythonDatapackTemplate/701c87f48c817e69aa45ff247f33eeac2841cff2/extensive_template/assets/records/Shazinho.ogg -------------------------------------------------------------------------------- /extensive_template/assets/records/_README.md: -------------------------------------------------------------------------------- 1 | 2 | ## Sounds folder 3 | Every .ogg file in the folder will be recognized by the build system ONLY IF you call the 'generate_custom_records()' function available in the database_helper file. 4 | Files in subfolders will be ignored! 5 | 6 | -------------------------------------------------------------------------------- /extensive_template/assets/sounds/_README.md: -------------------------------------------------------------------------------- 1 | 2 | ## Sounds folder 3 | Every .ogg file in the folder will be recognized by the build system. 4 | Files in subfolders will be ignored! 5 | 6 | -------------------------------------------------------------------------------- /extensive_template/assets/textures/_README.md: -------------------------------------------------------------------------------- 1 | 2 | ## Textures folder 3 | Every .png file and .png.mcmeta files in the folder will be recognized by the build system. 4 | You can pack up textures in folders if you want. 5 | 6 | -------------------------------------------------------------------------------- /extensive_template/assets/textures/deepslate_steel_ore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stoupy51/PythonDatapackTemplate/701c87f48c817e69aa45ff247f33eeac2841cff2/extensive_template/assets/textures/deepslate_steel_ore.png -------------------------------------------------------------------------------- /extensive_template/assets/textures/manual.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stoupy51/PythonDatapackTemplate/701c87f48c817e69aa45ff247f33eeac2841cff2/extensive_template/assets/textures/manual.png -------------------------------------------------------------------------------- /extensive_template/assets/textures/raw_steel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stoupy51/PythonDatapackTemplate/701c87f48c817e69aa45ff247f33eeac2841cff2/extensive_template/assets/textures/raw_steel.png -------------------------------------------------------------------------------- /extensive_template/assets/textures/raw_steel_block.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stoupy51/PythonDatapackTemplate/701c87f48c817e69aa45ff247f33eeac2841cff2/extensive_template/assets/textures/raw_steel_block.png -------------------------------------------------------------------------------- /extensive_template/assets/textures/shazinho.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stoupy51/PythonDatapackTemplate/701c87f48c817e69aa45ff247f33eeac2841cff2/extensive_template/assets/textures/shazinho.png -------------------------------------------------------------------------------- /extensive_template/assets/textures/steel_axe.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stoupy51/PythonDatapackTemplate/701c87f48c817e69aa45ff247f33eeac2841cff2/extensive_template/assets/textures/steel_axe.png -------------------------------------------------------------------------------- /extensive_template/assets/textures/steel_block.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stoupy51/PythonDatapackTemplate/701c87f48c817e69aa45ff247f33eeac2841cff2/extensive_template/assets/textures/steel_block.png -------------------------------------------------------------------------------- /extensive_template/assets/textures/steel_boots.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stoupy51/PythonDatapackTemplate/701c87f48c817e69aa45ff247f33eeac2841cff2/extensive_template/assets/textures/steel_boots.png -------------------------------------------------------------------------------- /extensive_template/assets/textures/steel_chestplate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stoupy51/PythonDatapackTemplate/701c87f48c817e69aa45ff247f33eeac2841cff2/extensive_template/assets/textures/steel_chestplate.png -------------------------------------------------------------------------------- /extensive_template/assets/textures/steel_dust.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stoupy51/PythonDatapackTemplate/701c87f48c817e69aa45ff247f33eeac2841cff2/extensive_template/assets/textures/steel_dust.png -------------------------------------------------------------------------------- /extensive_template/assets/textures/steel_helmet.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stoupy51/PythonDatapackTemplate/701c87f48c817e69aa45ff247f33eeac2841cff2/extensive_template/assets/textures/steel_helmet.png -------------------------------------------------------------------------------- /extensive_template/assets/textures/steel_hoe.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stoupy51/PythonDatapackTemplate/701c87f48c817e69aa45ff247f33eeac2841cff2/extensive_template/assets/textures/steel_hoe.png -------------------------------------------------------------------------------- /extensive_template/assets/textures/steel_ingot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stoupy51/PythonDatapackTemplate/701c87f48c817e69aa45ff247f33eeac2841cff2/extensive_template/assets/textures/steel_ingot.png -------------------------------------------------------------------------------- /extensive_template/assets/textures/steel_layer_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stoupy51/PythonDatapackTemplate/701c87f48c817e69aa45ff247f33eeac2841cff2/extensive_template/assets/textures/steel_layer_1.png -------------------------------------------------------------------------------- /extensive_template/assets/textures/steel_layer_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stoupy51/PythonDatapackTemplate/701c87f48c817e69aa45ff247f33eeac2841cff2/extensive_template/assets/textures/steel_layer_2.png -------------------------------------------------------------------------------- /extensive_template/assets/textures/steel_leggings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stoupy51/PythonDatapackTemplate/701c87f48c817e69aa45ff247f33eeac2841cff2/extensive_template/assets/textures/steel_leggings.png -------------------------------------------------------------------------------- /extensive_template/assets/textures/steel_nugget.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stoupy51/PythonDatapackTemplate/701c87f48c817e69aa45ff247f33eeac2841cff2/extensive_template/assets/textures/steel_nugget.png -------------------------------------------------------------------------------- /extensive_template/assets/textures/steel_ore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stoupy51/PythonDatapackTemplate/701c87f48c817e69aa45ff247f33eeac2841cff2/extensive_template/assets/textures/steel_ore.png -------------------------------------------------------------------------------- /extensive_template/assets/textures/steel_pickaxe.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stoupy51/PythonDatapackTemplate/701c87f48c817e69aa45ff247f33eeac2841cff2/extensive_template/assets/textures/steel_pickaxe.png -------------------------------------------------------------------------------- /extensive_template/assets/textures/steel_shovel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stoupy51/PythonDatapackTemplate/701c87f48c817e69aa45ff247f33eeac2841cff2/extensive_template/assets/textures/steel_shovel.png -------------------------------------------------------------------------------- /extensive_template/assets/textures/steel_stick.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stoupy51/PythonDatapackTemplate/701c87f48c817e69aa45ff247f33eeac2841cff2/extensive_template/assets/textures/steel_stick.png -------------------------------------------------------------------------------- /extensive_template/assets/textures/steel_sword.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stoupy51/PythonDatapackTemplate/701c87f48c817e69aa45ff247f33eeac2841cff2/extensive_template/assets/textures/steel_sword.png -------------------------------------------------------------------------------- /extensive_template/assets/textures/stone_rod.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stoupy51/PythonDatapackTemplate/701c87f48c817e69aa45ff247f33eeac2841cff2/extensive_template/assets/textures/stone_rod.png -------------------------------------------------------------------------------- /extensive_template/assets/textures/stone_stick.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stoupy51/PythonDatapackTemplate/701c87f48c817e69aa45ff247f33eeac2841cff2/extensive_template/assets/textures/stone_stick.png -------------------------------------------------------------------------------- /extensive_template/assets/textures/super_stone.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stoupy51/PythonDatapackTemplate/701c87f48c817e69aa45ff247f33eeac2841cff2/extensive_template/assets/textures/super_stone.png -------------------------------------------------------------------------------- /extensive_template/build/datapack/data/_your_namespace/function/bookshelf/test.mcfunction: -------------------------------------------------------------------------------- 1 | 2 | #> _your_namespace:bookshelf/test 3 | # 4 | # @within ??? 5 | # 6 | 7 | # Once 8 | scoreboard players set $math.divide.x bs.in 9 9 | scoreboard players set $math.divide.y bs.in 5 10 | function #bs.math:divide 11 | tellraw @a [{"text": "9 / 5 = ", "color": "dark_gray"},{"score":{"name":"$math.divide", "objective": "bs.out"}, "color": "gold"}] 12 | 13 | -------------------------------------------------------------------------------- /extensive_template/build/datapack/data/_your_namespace/function/calls/smart_ore_generation/generate_ores.mcfunction: -------------------------------------------------------------------------------- 1 | 2 | #> _your_namespace:calls/smart_ore_generation/generate_ores 3 | # 4 | # @within ??? 5 | # 6 | 7 | # Generate Steel Ore (x2) 8 | scoreboard players set #dimension smart_ore_generation.data -1 9 | execute if dimension minecraft:overworld run scoreboard players set #dimension smart_ore_generation.data 0 10 | execute if dimension stardust:cavern run scoreboard players set #dimension smart_ore_generation.data 1 11 | execute if dimension some_other:dimension run scoreboard players set #dimension smart_ore_generation.data 2 12 | scoreboard players set #min_height smart_ore_generation.data 0 13 | scoreboard players set #max_height smart_ore_generation.data 50 14 | execute if score #dimension smart_ore_generation.data matches 0.. run function _your_namespace:calls/smart_ore_generation/veins/steel_ore 15 | execute if score #dimension smart_ore_generation.data matches 0.. run function _your_namespace:calls/smart_ore_generation/veins/steel_ore 16 | 17 | # Generate Deepslate Steel Ore (x2) 18 | scoreboard players set #dimension smart_ore_generation.data -1 19 | execute if dimension minecraft:overworld run scoreboard players set #dimension smart_ore_generation.data 0 20 | scoreboard players operation #min_height smart_ore_generation.data = _OVERWORLD_BOTTOM smart_ore_generation.data 21 | scoreboard players set #max_height smart_ore_generation.data 0 22 | execute if score #dimension smart_ore_generation.data matches 0.. run function _your_namespace:calls/smart_ore_generation/veins/deepslate_steel_ore_0 23 | execute if score #dimension smart_ore_generation.data matches 0.. run function _your_namespace:calls/smart_ore_generation/veins/deepslate_steel_ore_0 24 | 25 | # Generate Deepslate Steel Ore (x8) 26 | scoreboard players set #dimension smart_ore_generation.data -1 27 | execute if dimension stardust:cavern run scoreboard players set #dimension smart_ore_generation.data 0 28 | scoreboard players operation #min_height smart_ore_generation.data = _OVERWORLD_BOTTOM smart_ore_generation.data 29 | scoreboard players set #max_height smart_ore_generation.data 0 30 | execute if score #dimension smart_ore_generation.data matches 0.. run function _your_namespace:calls/smart_ore_generation/veins/deepslate_steel_ore_1 31 | execute if score #dimension smart_ore_generation.data matches 0.. run function _your_namespace:calls/smart_ore_generation/veins/deepslate_steel_ore_1 32 | execute if score #dimension smart_ore_generation.data matches 0.. run function _your_namespace:calls/smart_ore_generation/veins/deepslate_steel_ore_1 33 | execute if score #dimension smart_ore_generation.data matches 0.. run function _your_namespace:calls/smart_ore_generation/veins/deepslate_steel_ore_1 34 | execute if score #dimension smart_ore_generation.data matches 0.. run function _your_namespace:calls/smart_ore_generation/veins/deepslate_steel_ore_1 35 | execute if score #dimension smart_ore_generation.data matches 0.. run function _your_namespace:calls/smart_ore_generation/veins/deepslate_steel_ore_1 36 | execute if score #dimension smart_ore_generation.data matches 0.. run function _your_namespace:calls/smart_ore_generation/veins/deepslate_steel_ore_1 37 | execute if score #dimension smart_ore_generation.data matches 0.. run function _your_namespace:calls/smart_ore_generation/veins/deepslate_steel_ore_1 38 | 39 | -------------------------------------------------------------------------------- /extensive_template/build/datapack/data/_your_namespace/function/path/to/a/random/function/i/guess.mcfunction: -------------------------------------------------------------------------------- 1 | 2 | #> _your_namespace:path/to/a/random/function/i/guess 3 | # 4 | # @within ??? 5 | # 6 | 7 | say Hello world! 8 | 9 | -------------------------------------------------------------------------------- /extensive_template/build/datapack/data/_your_namespace/function/v2.0.0/load/confirm_load.mcfunction: -------------------------------------------------------------------------------- 1 | 2 | #> _your_namespace:v2.0.0/load/confirm_load 3 | # 4 | # @within ??? 5 | # 6 | 7 | # Add a message when loading 8 | say Here is a message when loading the datapack, located in `src/link.py` 9 | 10 | -------------------------------------------------------------------------------- /extensive_template/build/datapack/data/_your_namespace/function/v2.0.0/minute.mcfunction: -------------------------------------------------------------------------------- 1 | 2 | #> _your_namespace:v2.0.0/minute 3 | # 4 | # @within ??? 5 | # 6 | 7 | execute if score #spam _your_namespace.data matches 1 run say This is a message every minute 8 | 9 | -------------------------------------------------------------------------------- /extensive_template/build/datapack/data/_your_namespace/function/v2.0.0/second_5.mcfunction: -------------------------------------------------------------------------------- 1 | 2 | #> _your_namespace:v2.0.0/second_5 3 | # 4 | # @within ??? 5 | # 6 | 7 | execute if score #spam _your_namespace.data matches 1 run say This is a SPAM message every 5 seconds 8 | 9 | -------------------------------------------------------------------------------- /extensive_template/build/datapack/data/_your_namespace/function/v2.0.0/tick_2.mcfunction: -------------------------------------------------------------------------------- 1 | 2 | #> _your_namespace:v2.0.0/tick_2 3 | # 4 | # @within ??? 5 | # 6 | 7 | execute if score #spam _your_namespace.data matches 1 run say This is a SPAM message every 2 ticks 8 | 9 | -------------------------------------------------------------------------------- /extensive_template/build/datapack/data/_your_namespace/jukebox_song/shazinho.json: -------------------------------------------------------------------------------- 1 | { 2 | "comparator_output": 15, 3 | "length_in_seconds": 128, 4 | "sound_event": { 5 | "sound_id": "_your_namespace:shazinho" 6 | }, 7 | "description": { 8 | "text": "Shazinho" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /extensive_template/build/datapack/pack.mcmeta: -------------------------------------------------------------------------------- 1 | { 2 | "pack": { 3 | "pack_format": 71, 4 | "description": "Author: Stoupy51\nVersion: 2.0.0" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /extensive_template/build/resource_pack/assets/_your_namespace/equipment/steel.json: -------------------------------------------------------------------------------- 1 | { 2 | "layers": { 3 | "humanoid": [{"texture": "_your_namespace:steel_layer_1"}], 4 | "humanoid_leggings": [{"texture": "_your_namespace:steel_layer_2"}] 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /extensive_template/build/resource_pack/assets/_your_namespace/sounds/shazinho.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stoupy51/PythonDatapackTemplate/701c87f48c817e69aa45ff247f33eeac2841cff2/extensive_template/build/resource_pack/assets/_your_namespace/sounds/shazinho.ogg -------------------------------------------------------------------------------- /extensive_template/build/resource_pack/assets/_your_namespace/textures/entity/equipment/humanoid/steel_layer_1.png.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stoupy51/PythonDatapackTemplate/701c87f48c817e69aa45ff247f33eeac2841cff2/extensive_template/build/resource_pack/assets/_your_namespace/textures/entity/equipment/humanoid/steel_layer_1.png.png -------------------------------------------------------------------------------- /extensive_template/build/resource_pack/assets/_your_namespace/textures/entity/equipment/humanoid_leggings/steel_layer_2.png.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stoupy51/PythonDatapackTemplate/701c87f48c817e69aa45ff247f33eeac2841cff2/extensive_template/build/resource_pack/assets/_your_namespace/textures/entity/equipment/humanoid_leggings/steel_layer_2.png.png -------------------------------------------------------------------------------- /extensive_template/build/resource_pack/pack.mcmeta: -------------------------------------------------------------------------------- 1 | { 2 | "pack": { 3 | "pack_format": 55, 4 | "description": "Author: Stoupy51\nVersion: 2.0.0" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /extensive_template/src/link.py: -------------------------------------------------------------------------------- 1 | 2 | # Imports 3 | from beet import Context 4 | from beet.core.utils import JsonDict 5 | from stewbeet import core 6 | from stewbeet.core.utils.io import * 7 | 8 | 9 | # Main entry point (ran just before making finalyzing the build process (zip, headers, lang, ...)) 10 | def beet_default(ctx: Context): 11 | ns: str = ctx.project_id 12 | database: JsonDict = core.Mem.database # noqa: F841 13 | 14 | # Generate ores in the world 15 | core.CustomOreGeneration.all_with_config(ore_configs = { 16 | "steel_ore": [ 17 | core.CustomOreGeneration( 18 | dimensions = ["minecraft:overworld","stardust:cavern","some_other:dimension"], 19 | maximum_height = 50, 20 | minimum_height = 0, 21 | veins_per_region = 2, 22 | vein_size_logic = 0.4, 23 | ) 24 | ], 25 | "deepslate_steel_ore": [ 26 | core.CustomOreGeneration( 27 | dimensions = ["minecraft:overworld"], 28 | maximum_height = 0, 29 | veins_per_region = 2, 30 | vein_size_logic = 0.4, 31 | ), 32 | core.CustomOreGeneration( 33 | dimensions = ["stardust:cavern"], 34 | maximum_height = 0, 35 | veins_per_region = 8, 36 | vein_size_logic = 0.8, 37 | ) 38 | ], 39 | }) 40 | 41 | 42 | # Add some commands when loading datapack 43 | write_load_file(""" 44 | # Add a message when loading 45 | say Here is a message when loading the datapack, located in `src/link.py` 46 | """) 47 | # write_function("v{version}/load/confirm_load", ...) <- This is the same as the previous line 48 | 49 | 50 | ## Clock functions 51 | # When you write to the following files: "tick_2", "second", "second_5", "minute"... the tick function will automatically call them, ex: 52 | write_versioned_function("minute", f"execute if score #spam {ns}.data matches 1 run say This is a message every minute\n") 53 | write_versioned_function("second_5", f"execute if score #spam {ns}.data matches 1 run say This is a SPAM message every 5 seconds\n") 54 | write_versioned_function("tick_2", f"execute if score #spam {ns}.data matches 1 run say This is a SPAM message every 2 ticks\n") 55 | # The two following functions calls are equivalent: 56 | # write_tick_file(config, ...) 57 | # write_versioned_file(config, "tick", ...) 58 | 59 | # Create a random function 60 | write_function(f"{ns}:path/to/a/random/function/i/guess", "say Hello world!") 61 | 62 | # Call a bookshelf module (Every single module from https://docs.mcbookshelf.dev/en/latest/ is supported) 63 | write_function(f"{ns}:bookshelf/test", """ 64 | # Once 65 | scoreboard players set $math.divide.x bs.in 9 66 | scoreboard players set $math.divide.y bs.in 5 67 | function #bs.math:divide 68 | tellraw @a [{"text": "9 / 5 = ", "color": "dark_gray"},{"score":{"name":"$math.divide", "objective": "bs.out"}, "color": "gold"}] 69 | """) 70 | 71 | pass 72 | 73 | -------------------------------------------------------------------------------- /extensive_template/src/setup_database.py: -------------------------------------------------------------------------------- 1 | 2 | # Imports 3 | from beet import Context 4 | from beet.core.utils import JsonDict 5 | from stewbeet import core 6 | 7 | # Configuration to generate everything about the material based on "steel_ingot" 8 | ORES_CONFIGS: dict[str, core.EquipmentsConfig|None] = { 9 | "steel_ingot": core.EquipmentsConfig( 10 | # This steel is equivalent to iron, 11 | equivalent_to = core.DefaultOre.IRON, 12 | 13 | # But, has more durability (3 times more) 14 | pickaxe_durability = 3 * core.VanillaEquipments.PICKAXE.value[core.DefaultOre.IRON]["durability"], 15 | 16 | # And, does 1 more damage per hit (mainhand), and has 0.5 more armor, and mines 20% faster (pickaxe) 17 | attributes = {"attack_damage": 1, "armor": 0.5, "mining_efficiency": 0.2} 18 | ), 19 | 20 | # Simple material stone, this will automatically detect stone stick and rod textures. 21 | "minecraft:stone": None, 22 | } 23 | 24 | # Main entry point 25 | def beet_default(ctx: Context): 26 | ns: str = ctx.project_id 27 | database: JsonDict = core.Mem.database 28 | 29 | # Generate ores in database (add every stuff (found in the textures folder) related to the given materials, to the database) 30 | core.generate_everything_about_these_materials(ORES_CONFIGS) 31 | 32 | database["steel_ingot"][core.WIKI_COMPONENT] = [ 33 | {"text":"Here is an example of a wiki component, this text component will be displayed as a button in the manual.\n"}, 34 | {"text":"You can write anything you want here.","color":"yellow"}, 35 | ] 36 | 37 | # Generate custom disc records 38 | core.generate_custom_records("auto") 39 | 40 | # Add a super stone block that can be crafted with 9 deepslate or stone, and has cobblestone as base block 41 | database["super_stone"] = { 42 | "id": core.CUSTOM_BLOCK_VANILLA, # Placeholder for the base block 43 | core.VANILLA_BLOCK: {"id": "minecraft:cobblestone", "apply_facing": False}, # Base block 44 | core.RESULT_OF_CRAFTING: [ # Crafting recipes (shaped and shapeless examples) 45 | {"type":"crafting_shaped","result_count":1,"group":"super_stone","category":"blocks","shape":["XXX","XXX","XXX"],"ingredients": {"X": core.ingr_repr("minecraft:stone")}}, 46 | {"type":"crafting_shapeless","result_count":1,"group":"super_stone","category":"blocks","ingredients": 9 * [core.ingr_repr("minecraft:deepslate")] }, 47 | ], 48 | # Exemple of recipe with vanilla result (not custom item) 49 | core.USED_FOR_CRAFTING: [ 50 | {"type":"smelting","result_count":1,"cookingtime":200,"experience":0.1,"group":"super_stone", 51 | "category":"blocks","ingredient":core.ingr_repr("super_stone", ns),"result":core.ingr_repr("minecraft:diamond")}, 52 | ] 53 | } 54 | 55 | # Don't forget to add the vanilla blocks for the custom blocks (not automatic even though they was recognized by generate_everything_about_these_materials()) 56 | database["steel_block"][core.VANILLA_BLOCK] = {"id": "minecraft:iron_block", "apply_facing": False} # Placeholder for the base block 57 | database["raw_steel_block"][core.VANILLA_BLOCK] = {"id": "minecraft:raw_iron_block", "apply_facing": False} # Placeholder for the base block 58 | 59 | # Add a recipe for the future generated manual (the manual recipe will show up in itself) 60 | database["manual"] = { 61 | "id": "minecraft:written_book", "category": "misc", "item_name": ctx.meta.stewbeet.manual.name, 62 | core.RESULT_OF_CRAFTING: [ 63 | # Put a book and a steel ingot in the crafting grid to get the manual 64 | {"type":"crafting_shapeless","result_count":1,"group":"manual","category":"misc","ingredients": [core.ingr_repr("minecraft:book"), core.ingr_repr("steel_ingot", ns)]}, 65 | 66 | # Put the manual in the crafting grid to get the manual back (update the manual) 67 | {"type":"crafting_shapeless","result_count":1,"group":"manual","category":"misc","ingredients": [core.ingr_repr("manual", ns)]}, 68 | ], 69 | } 70 | 71 | # Add item categories to the remaining items (should select 'shazinho' and 'super_stone') 72 | for item in database.values(): 73 | if not item.get("category"): 74 | item["category"] = "misc" 75 | 76 | # Final adjustments, you definitively should keep them! 77 | core.add_item_model_component(black_list = ["item_ids","you_don't_want","in_that","list"]) 78 | core.add_item_name_and_lore_if_missing() 79 | core.add_private_custom_data_for_namespace() # Add a custom namespace for easy item detection 80 | core.add_smithed_ignore_vanilla_behaviours_convention() # Smithed items convention 81 | print() 82 | 83 | -------------------------------------------------------------------------------- /python_package/.gitignore: -------------------------------------------------------------------------------- 1 | 2 | *__pycache__/ 3 | 4 | -------------------------------------------------------------------------------- /python_package/1_upgrades.py: -------------------------------------------------------------------------------- 1 | 2 | import os 3 | import sys 4 | 5 | os.system(f"{sys.executable} -m pip install --upgrade pip setuptools build twine packaging") 6 | 7 | -------------------------------------------------------------------------------- /python_package/2_build.py: -------------------------------------------------------------------------------- 1 | 2 | import os 3 | import sys 4 | 5 | os.system(f"{sys.executable} -m build") 6 | 7 | -------------------------------------------------------------------------------- /python_package/3_upload.py: -------------------------------------------------------------------------------- 1 | 2 | import os 3 | import sys 4 | 5 | LAST_FILES: int = 1 6 | for root, _, files in os.walk("dist"): 7 | files = sorted(files, key=lambda x: os.path.getmtime(f"dist/{x}"), reverse=True) 8 | lasts = files[:LAST_FILES * 2] # x2 because we have the .tar.gz and the .whl 9 | for file in lasts: 10 | if file.endswith(".tar.gz"): 11 | os.system(f"{sys.executable} -m twine upload --verbose -r stewbeet dist/{file}") 12 | 13 | -------------------------------------------------------------------------------- /python_package/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Alexandre Collignon 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /python_package/README.md: -------------------------------------------------------------------------------- 1 | 2 | # 📦 Package Link 3 | [![PyPI - Downloads](https://img.shields.io/pypi/dm/stewbeet?logo=python&label=PyPI%20downloads)](https://pypi.org/project/stewbeet/) 4 | 5 | # 🐍 Python Datapack 6 | Here is a template to a GitHub repository using this Python package: 📝 7 | [https://github.com/Stoupy51/StewBeetTemplate](https://github.com/Stoupy51/StewBeetTemplate) 8 | 9 | Here is an advanced example using this Python package: ⚡ 10 | [https://github.com/Stoupy51/SimplEnergy](https://github.com/Stoupy51/SimplEnergy) 11 | 12 | ## ⭐ Star History 13 | 14 | 15 | 16 | 17 | 18 | Star History Chart 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /python_package/TODO.md: -------------------------------------------------------------------------------- 1 | 2 | - Convertir StewBeet à un ctx 3 | - Maybe beeter Python Datapack 4 | 5 | -------------------------------------------------------------------------------- /python_package/all_doctests.py: -------------------------------------------------------------------------------- 1 | 2 | # Imports 3 | import sys 4 | 5 | # Import stouputils 6 | from stouputils import get_root_path, info, launch_tests, measure_time 7 | 8 | 9 | # Main 10 | @measure_time(info, message="All doctests finished") 11 | def main() -> None: 12 | FOLDER_TO_TEST: str = get_root_path(__file__) 13 | if launch_tests(f"{FOLDER_TO_TEST}/src/stewbeet") > 0: 14 | sys.exit(1) 15 | 16 | if __name__ == "__main__": 17 | main() 18 | 19 | -------------------------------------------------------------------------------- /python_package/all_in_one.py: -------------------------------------------------------------------------------- 1 | 2 | import os 3 | import sys 4 | 5 | commands = [ 6 | f"{sys.executable} upgrade.py", # Upgrade the version in pyproject.toml 7 | f"{sys.executable} build_all_in_one.py", # Build the package and upload it 8 | ] 9 | 10 | for command in commands: 11 | if os.system(command) != 0: 12 | print(f"Error while executing '{command}'") 13 | exit(1) 14 | 15 | -------------------------------------------------------------------------------- /python_package/build_all_in_one.py: -------------------------------------------------------------------------------- 1 | 2 | import os 3 | import sys 4 | 5 | commands = [ 6 | f"{sys.executable} 1_upgrades.py", 7 | f"{sys.executable} 2_build.py", 8 | f"{sys.executable} 3_upload.py", 9 | ] 10 | 11 | for command in commands: 12 | if os.system(command) != 0: 13 | print(f"Error while executing '{command}'") 14 | exit(1) 15 | 16 | -------------------------------------------------------------------------------- /python_package/github_release.py: -------------------------------------------------------------------------------- 1 | 2 | # Imports 3 | from src.stewbeet.continuous_delivery import load_credentials, upload_to_github 4 | 5 | # Get credentials 6 | credentials: dict = load_credentials("~/stewbeet/credentials.yml") 7 | 8 | # Upload to GitHub 9 | from upgrade import current_version 10 | 11 | github_config: dict = { 12 | "project_name": "stewbeet", 13 | "version": current_version, 14 | "build_folder": "", 15 | } 16 | changelog: str = upload_to_github(credentials, github_config) 17 | 18 | -------------------------------------------------------------------------------- /python_package/pyproject.toml: -------------------------------------------------------------------------------- 1 | 2 | [build-system] 3 | requires = ["hatchling"] 4 | build-backend = "hatchling.build" 5 | 6 | [project] 7 | name = "stewbeet" 8 | version = "0.0.1" 9 | authors = [ 10 | { name="Stoupy51", email="stoupy51@gmail.com" }, 11 | ] 12 | description = "Beet Framework made to help generating advanced Minecraft datapack contents" 13 | readme = "README.md" 14 | license = "MIT" 15 | requires-python = ">=3.12" 16 | classifiers = [ 17 | "Programming Language :: Python :: 3", 18 | "License :: OSI Approved :: MIT License", 19 | "Operating System :: OS Independent", 20 | ] 21 | dependencies = [ 22 | "stouputils>=1.3.10", 23 | "model_resolver>=1.8.0", 24 | "bolt>=0.49.1", 25 | "smithed>=0.19.0", 26 | "mutagen>=1.47.0", 27 | "pyperclip>=1.8.0", 28 | ] 29 | 30 | [project.urls] 31 | Homepage = "https://github.com/Stoupy51/stewbeet" 32 | Issues = "https://github.com/Stoupy51/stewbeet/issues" 33 | 34 | [project.scripts] 35 | stewbeet = "stewbeet.cli:main" 36 | 37 | [tool.pyright] 38 | typeCheckingMode = "basic" 39 | 40 | [tool.setuptools] 41 | license-files = ["LICENSE"] 42 | 43 | -------------------------------------------------------------------------------- /python_package/scripts/check_bookshelf_version.py: -------------------------------------------------------------------------------- 1 | """ Script to check if a new Bookshelf version is available. 2 | 3 | This script checks the current installed Bookshelf version against the latest 4 | available version and outputs 'true' if an update is available. 5 | 6 | Returns: 7 | str: 'true' if an update is available, 'false' otherwise 8 | """ 9 | 10 | # Imports 11 | import json 12 | import os 13 | from typing import Any 14 | 15 | import requests 16 | import stouputils as stp 17 | 18 | # Constants 19 | from on_bookshelf_release import API_URL, CONFIG_PATH 20 | 21 | 22 | def load_current_version() -> str | None: 23 | """Load the current Bookshelf version from config. 24 | 25 | Returns: 26 | str | None: The current version or None if not found 27 | """ 28 | if not os.path.exists(CONFIG_PATH): 29 | return None 30 | 31 | try: 32 | with open(CONFIG_PATH, "r") as f: 33 | config: dict[str, Any] = json.load(f) 34 | return config.get("version") 35 | except (json.JSONDecodeError, FileNotFoundError) as e: 36 | stp.warning(f"Failed to load current version: {e}") 37 | return None 38 | 39 | 40 | def get_latest_version() -> str | None: 41 | """Fetch the latest Bookshelf version from the API. 42 | 43 | Returns: 44 | str | None: The latest version or None if request failed 45 | """ 46 | try: 47 | response: requests.Response = requests.get(API_URL) 48 | response.raise_for_status() 49 | data: dict[str, Any] = response.json() 50 | # Assuming the version is in the tag_name field 51 | return data.get("tag_name", "") 52 | except (requests.RequestException, json.JSONDecodeError) as e: 53 | stp.warning(f"Failed to fetch latest version: {e}") 54 | return None 55 | 56 | 57 | @stp.handle_error() 58 | def main() -> None: 59 | """ Main function that checks for updates and prints result.""" 60 | current_version: str | None = load_current_version() 61 | latest_version: str | None = get_latest_version() 62 | 63 | # If versions are different, an update is available, return true 64 | if current_version != latest_version: 65 | print("true") 66 | else: 67 | print("false") 68 | 69 | 70 | if __name__ == "__main__": 71 | main() 72 | 73 | -------------------------------------------------------------------------------- /python_package/scripts/on_bookshelf_release.py: -------------------------------------------------------------------------------- 1 | """ 2 | This script is called when a Bookshelf release is triggered on GitHub. 3 | 4 | It will download all "bs.*.zip" files from the release and extract them to the "datapack" folder. 5 | It will then update the "bookshelf.py" file with the new version information. 6 | """ 7 | # Imports 8 | import os 9 | import re 10 | from copy import deepcopy 11 | 12 | import requests 13 | import stouputils as stp 14 | 15 | # Constants 16 | ROOT: str = stp.get_root_path(__file__, go_up=1) 17 | GITHUB_REPO: str = "https://github.com/mcbookshelf/bookshelf/releases" 18 | API_URL: str = "https://api.github.com/repos/mcbookshelf/bookshelf/releases/latest" 19 | DESTINATION_FOLDER: str = f"{ROOT}/src/stewbeet/dependencies" 20 | CONFIG_PATH: str = f"{DESTINATION_FOLDER}/bookshelf_config.json" 21 | DEPS_TO_UPDATE: str = "src/stewbeet/dependencies/bookshelf.py" 22 | MODULE_TEMPLATE: dict = {"version": [0, 0, 0], "name": "Title Name", "url": GITHUB_REPO, "is_used": False} 23 | 24 | 25 | # Helper function to parse version string (e.g., "v1.2.3" or "1.2.3") 26 | def parse_version(tag: str) -> list[int]: 27 | tag = tag.lstrip("v") 28 | return [int(part) for part in tag.split(".") if part.isdigit()] 29 | 30 | # Convert PascalCase or dash-style module names to title format 31 | def format_module_name(module: str) -> str: 32 | # Remove version suffix 33 | module = re.sub(r"[-_]?\d+(\.\d+)*(-v\d+(\.\d+)*)?$", "", module) 34 | # Replace dash/underscore with space, capitalize each word 35 | return "Bookshelf " + " ".join(word.capitalize() for word in re.split(r"[-_.]", module) if word != "bs") 36 | 37 | # Convert PascalCase to snake_case 38 | def pascal_to_snake(name: str) -> str: 39 | return re.sub(r'(? None: 44 | """ 45 | Download the wanted files from the latest release on GitHub. 46 | 47 | Then, generate a new "bookshelf.py" file with the new modules looking like the following: 48 | 49 | BOOKSHELF_MODULES: dict[str, dict] = { 50 | "bs.block_barrel": { ... }, 51 | "bs.item_tool": { ... }, 52 | ... 53 | } 54 | """ 55 | stp.info(f"Fetching latest release from {API_URL}") 56 | response: requests.Response = requests.get(API_URL) 57 | if response.status_code != 200: 58 | stp.info(f"Error fetching release info: {response.status_code}") 59 | return 60 | release_info: dict = response.json() 61 | 62 | tag_name: str = release_info.get("tag_name", "v0.0.0") 63 | stp.info(f"Latest release tag: {tag_name}") 64 | 65 | modules: dict = {} 66 | assets: list[dict] = release_info.get("assets", []) 67 | os.makedirs(f"{DESTINATION_FOLDER}/datapack", exist_ok=True) 68 | 69 | # Filter assets 70 | assets = [asset for asset in assets if re.match(r"^bs\..*\.zip$", asset.get("name", ""))] 71 | 72 | def download_module(asset: dict) -> tuple[str, dict] | None: 73 | file_name: str = asset.get("name", "") 74 | no_extension: str = os.path.splitext(file_name)[0] 75 | module_name: str = format_module_name(no_extension) 76 | download_url: str = asset.get("browser_download_url", "") 77 | if not download_url: 78 | stp.warning(f"No download URL for {file_name}, skipping.") 79 | return 80 | 81 | asset_response: requests.Response = requests.get(download_url) 82 | if asset_response.status_code != 200: 83 | stp.warning(f"Failed to download {file_name}: HTTP {asset_response.status_code}") 84 | return 85 | 86 | local_zip_path: str = f"{DESTINATION_FOLDER}/datapack/{module_name}.zip" 87 | with open(local_zip_path, "wb") as f: 88 | f.write(asset_response.content) 89 | stp.debug(f"Downloaded '{file_name}' to '...{local_zip_path[-40:]}'") 90 | 91 | # Extract module version from name (e.g. "bs.health-1.21.5-v3.0.0.zip" -> [3, 0, 0]) 92 | version: list[int] = parse_version(no_extension.split('-')[-1]) 93 | 94 | # Prepare module info 95 | module_key_snake: str = file_name.split('-')[0] 96 | module_info: dict = deepcopy(MODULE_TEMPLATE) 97 | module_info["version"] = version 98 | module_info["name"] = module_name 99 | return module_key_snake, module_info 100 | 101 | # Multithreading 102 | results: list[tuple[str, dict]] = stp.multithreading(download_module, assets, max_workers=len(assets)) 103 | modules: dict = dict(sorted(results, key=lambda x: x[0])) 104 | 105 | dumped: str = stp.super_json_dump(modules, max_level=1).replace("false", "False").replace("true", "True") 106 | file_content: str = f""" 107 | # This file is auto-generated by the release script ({DEPS_TO_UPDATE}) 108 | BOOKSHELF_MODULES = {dumped} 109 | """ 110 | 111 | with open(f"{ROOT}/{DEPS_TO_UPDATE}", "w") as f: 112 | f.write(file_content) 113 | stp.info(f"Updated '{DEPS_TO_UPDATE}' with new module information.") 114 | 115 | # Update the config file 116 | with open(CONFIG_PATH, "w") as f: 117 | stp.super_json_dump({"version": tag_name}, f) 118 | stp.info(f"Updated '{CONFIG_PATH}' with new version information.") 119 | 120 | # Main 121 | if __name__ == "__main__": 122 | download_latest_release() 123 | 124 | -------------------------------------------------------------------------------- /python_package/src/python_datapack/__main__.py: -------------------------------------------------------------------------------- 1 | 2 | # Imports 3 | from .cli import main # noqa: F401 4 | 5 | -------------------------------------------------------------------------------- /python_package/src/python_datapack/__memory__.py: -------------------------------------------------------------------------------- 1 | 2 | # pyright: reportAssignmentType=false 3 | # ruff: noqa: RUF012 4 | from collections.abc import Iterable 5 | from typing import Any 6 | 7 | from beet import ProjectConfig 8 | 9 | 10 | # Python Datapack Config class 11 | class StewBeetCfg(ProjectConfig): 12 | """ Main configuration class for Python Datapack projects. """ 13 | 14 | # Folders 15 | ROOT: str = None 16 | """ [Required] Absolute path to the project's root directory. """ 17 | SOURCE_FOLDER: str = None 18 | """ [Required] Directory containing source files that will be processed by the beet build system. (Will be used to infer paths SOURCE_FOLDER/datapack and SOURCE_FOLDER/resource_pack) """ 19 | BUILD_FOLDER: str = None 20 | """ [Required] Output directory where the final datapack and resource pack will be generated. """ 21 | ASSETS_FOLDER: str = None 22 | """ [Required] Directory containing all project assets including textures, sounds, and other media files. """ 23 | LIBS_FOLDER: str | None = None 24 | """ Directory containing libraries that will be copied to the build destination, and merged using Smithed Weld if enabled. """ 25 | BUILD_COPY_DESTINATIONS: tuple[Iterable[str], Iterable[str]] = ([], []) 26 | """ Optional list of destination paths where generated files will be copied. Can be empty if no copying is needed. """ 27 | 28 | # Dev constants 29 | DATABASE_DEBUG: str = "" # TODO 30 | """ Path where database debug information will be dumped. If empty, defaults to """ 31 | ENABLE_TRANSLATIONS: bool = True 32 | """ Will convert all the text components to translate and generate a lang file in the resource pack. Meaning you can easily translate the datapack in multiple languages! """ 33 | MERGE_LIBS: bool = True 34 | """ Enables merging of libraries with the datapack and resource pack using Smithed Weld. """ 35 | 36 | # Project information 37 | AUTHOR: str = None 38 | """ Author(s) name(s) displayed in pack.mcmeta, also used to add 'convention.debug' tag to the players of the same name(s) <-- showing additionnal displays like datapack loading """ 39 | PROJECT_NAME: str = None 40 | """ Name of the project that will be used in messages, item lore, etc. """ 41 | VERSION: str = None 42 | """ Project version in semantic versioning format (e.g., 1.0.0 or 2.84.615). """ 43 | NAMESPACE: str = None 44 | """ Simplified project name used for function and tag namespacing. Must match merge folder namespace. """ 45 | DESCRIPTION: str = "" # TODO 46 | """ Pack description for pack.mcmeta. Defaults to "{PROJECT_NAME} [{VERSION}] by {AUTHOR}" if empty. """ 47 | DEPENDENCIES: dict[str, dict[str, list[int] | str]] = {} 48 | """ Automagically, the datapack will check for the presence of dependencies and their minimum required versions at runtime. 49 | The url is used when the dependency is not found to suggest where to get it. 50 | The version dict key contains the minimum required version of the dependency in [major, minor, patch] format. 51 | The main key is the dependency namespace to check for. 52 | The name can be whatever you want, it's just used in messages 53 | 54 | Example: 55 | { 56 | # Example for DatapackEnergy >= 1.8.0 57 | "energy": {"version":[1, 8, 0], "name":"DatapackEnergy", "url":"https://github.com/ICY105/DatapackEnergy"}, 58 | } 59 | """ 60 | 61 | # Technical constants 62 | SOURCE_LORE: list[Any] | str = "auto" # TODO 63 | """ Custom item lore configuration. If set to "auto", uses project icon followed by PROJECT_NAME. """ 64 | 65 | # Manual configuration 66 | class Manual: 67 | IS_ENABLED: bool = True 68 | """ Controls manual/guide generation. Note: Malformed database items may cause server log spam. """ 69 | 70 | DEBUG_MODE: bool = False 71 | """ Enables grid display in the manual for debugging. """ 72 | 73 | MANUAL_OVERRIDES: str | None = None 74 | """ Path to directory containing custom manual assets that override defaults. """ 75 | 76 | MANUAL_HIGH_RESOLUTION: bool = True 77 | """ Enables high-resolution crafting displays in the manual. """ 78 | 79 | CACHE_PATH: str = None 80 | """ Directory for storing cached manual assets. """ 81 | 82 | CACHE_ASSETS: bool = True 83 | """ Enables caching of Minecraft assets and item renders (manual/items/*.png). """ 84 | 85 | CACHE_PAGES: bool = False 86 | """ Enables caching of manual content and images (manual/pages/*.png). """ 87 | 88 | JSON_DUMP_PATH: str = "" # TODO 89 | """ Path for manual debug dump. Defaults to "{Manual.CACHE_PATH}/debug_manual.json" if empty. """ 90 | 91 | MANUAL_NAME: str = "" # TODO 92 | """ Manual title used in book and first page. Defaults to "{PROJECT_NAME} Manual". """ 93 | 94 | MAX_ITEMS_PER_ROW: int = 5 95 | """ Maximum number of items displayed per row in manual (max: 6). """ 96 | 97 | MAX_ROWS_PER_PAGE: int = 5 98 | """ Maximum number of rows displayed per page in manual (max: 6). """ 99 | 100 | OPENGL_RESOLUTION: int = 256 101 | """ Resolution for OpenGL renders in manual (recommended: 256x256). """ 102 | 103 | MANUAL_FIRST_PAGE_TEXT: list[Any] | str = [{"text":"Modify in config.py the text that will be shown in this first manual page", "color":"#505050"}] 104 | """ Text component used for the manual's first page. """ 105 | 106 | Beet: ProjectConfig = ProjectConfig( 107 | require=[ 108 | "bolt", 109 | "mecha.contrib.relative_location", 110 | "mecha.contrib.nested_location" 111 | ], 112 | pipeline=[ 113 | "" 114 | ] 115 | ) 116 | """ Beet project configuration. 117 | Allows overriding individual or multiple configuration elements 118 | such as pipeline configuration, dependency management, etc. 119 | 120 | Note: Python Datapack automatically completes missing beet configuration elements (namespace, etc.) - only override if necessary. 121 | """ 122 | 123 | -------------------------------------------------------------------------------- /python_package/src/python_datapack/cli.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | def main(): 4 | pass 5 | 6 | if __name__ == "__main__": 7 | main() 8 | 9 | -------------------------------------------------------------------------------- /python_package/src/python_datapack/compatibilities/main.py: -------------------------------------------------------------------------------- 1 | 2 | # Imports 3 | from .neo_enchant import main as neo_enchant 4 | from .simpledrawer import main as simpledrawer 5 | 6 | 7 | def main(config: dict): 8 | 9 | # Compatibility with SimpleDrawer's compacting drawer 10 | simpledrawer(config) 11 | 12 | # Compatibility with NeoEnchant's veinminer 13 | neo_enchant(config) 14 | 15 | -------------------------------------------------------------------------------- /python_package/src/python_datapack/compatibilities/neo_enchant.py: -------------------------------------------------------------------------------- 1 | 2 | # Imports 3 | import stouputils as stp 4 | 5 | from ...stewbeet.core.constants import VANILLA_BLOCK, VANILLA_BLOCK_FOR_ORES 6 | from ..utils.io import write_file 7 | 8 | 9 | # Main function 10 | def main(config: dict): 11 | database: dict[str, dict] = config["database"] 12 | 13 | # If any block use the vanilla block for ores, add the compatibility 14 | if any(VANILLA_BLOCK_FOR_ORES == data.get(VANILLA_BLOCK) for data in database.values()): 15 | 16 | # Add the block to veinminer tag 17 | write_file(f"{config['build_datapack']}/data/enchantplus/tags/block/veinminer.json", stp.super_json_dump({"values": [VANILLA_BLOCK_FOR_ORES["id"]]})) 18 | 19 | # Final print 20 | stp.debug("Special datapack compatibility done for NeoEnchant's veinminer!") 21 | 22 | -------------------------------------------------------------------------------- /python_package/src/python_datapack/datapack/basic_structure.py: -------------------------------------------------------------------------------- 1 | 2 | # Imports 3 | from ..utils.io import is_in_write_queue, write_tick_file, write_versioned_function 4 | 5 | 6 | def main(config: dict): 7 | version: str = config['version'] 8 | namespace: str = config['namespace'] 9 | functions: str = f"{config['build_datapack']}/data/{namespace}/function/v{version}" 10 | tick_2: str = f"{functions}/tick_2.mcfunction" 11 | second: str = f"{functions}/second.mcfunction" 12 | second_5: str = f"{functions}/second_5.mcfunction" 13 | minute: str = f"{functions}/minute.mcfunction" 14 | 15 | # Prepend to tick_2, second, second_5, and minute if they exists 16 | if is_in_write_queue(tick_2): 17 | write_versioned_function(config, "tick_2", 18 | f""" 19 | # Reset timer 20 | scoreboard players set #tick_2 {namespace}.data 1 21 | """, prepend = True) 22 | if is_in_write_queue(second): 23 | write_versioned_function(config, "second", 24 | f""" 25 | # Reset timer 26 | scoreboard players set #second {namespace}.data 0 27 | """, prepend = True) 28 | if is_in_write_queue(second_5): 29 | write_versioned_function(config, "second_5", 30 | f""" 31 | # Reset timer 32 | scoreboard players set #second_5 {namespace}.data -10 33 | """, prepend = True) 34 | if is_in_write_queue(minute): 35 | write_versioned_function(config, "minute", 36 | f""" 37 | # Reset timer 38 | scoreboard players set #minute {namespace}.data 1 39 | """, prepend = True) 40 | 41 | # Tick structure, tick_2 and second_5 are "offsync" for a better load distribution 42 | if is_in_write_queue(tick_2) or is_in_write_queue(second) or is_in_write_queue(second_5) or is_in_write_queue(minute): 43 | content: str = "# Timers\n" 44 | if is_in_write_queue(tick_2): 45 | content += f"scoreboard players add #tick_2 {namespace}.data 1\n" 46 | if is_in_write_queue(second): 47 | content += f"scoreboard players add #second {namespace}.data 1\n" 48 | if is_in_write_queue(second_5): 49 | content += f"scoreboard players add #second_5 {namespace}.data 1\n" 50 | if is_in_write_queue(minute): 51 | content += f"scoreboard players add #minute {namespace}.data 1\n" 52 | 53 | if is_in_write_queue(tick_2): 54 | content += f"execute if score #tick_2 {namespace}.data matches 3.. run function {namespace}:v{version}/tick_2\n" 55 | if is_in_write_queue(second): 56 | content += f"execute if score #second {namespace}.data matches 20.. run function {namespace}:v{version}/second\n" 57 | if is_in_write_queue(second_5): 58 | content += f"execute if score #second_5 {namespace}.data matches 90.. run function {namespace}:v{version}/second_5\n" 59 | if is_in_write_queue(minute): 60 | content += f"execute if score #minute {namespace}.data matches 1200.. run function {namespace}:v{version}/minute\n" 61 | if content: 62 | write_tick_file(config, content, prepend = True) 63 | 64 | 65 | -------------------------------------------------------------------------------- /python_package/src/python_datapack/datapack/custom_block_ticks.py: -------------------------------------------------------------------------------- 1 | 2 | # Imports 3 | from ..utils.io import FILES_TO_WRITE, write_file, write_function 4 | 5 | 6 | def custom_blocks_ticks_and_second_functions(config: dict) -> None: 7 | """ Setup custom blocks ticks and second functions calls 8 | 9 | It will seek for "second.mcfunction" and "tick.mcfunction" files in the custom_blocks folder 10 | 11 | Then it will generate all functions to lead to the execution of these files by adding tags 12 | 13 | Args: 14 | config (dict): The config dictionary 15 | """ 16 | namespace: str = config['namespace'] 17 | build_datapack: str = config['build_datapack'] 18 | functions: str = f"{build_datapack}/data/{namespace}/function" 19 | custom_blocks = f"{functions}/custom_blocks" 20 | 21 | # Get second and ticks functions 22 | custom_blocks_second = [] 23 | custom_blocks_tick = [] 24 | for path in FILES_TO_WRITE: 25 | if custom_blocks in path: 26 | custom_block = path.split(custom_blocks + "/")[1] 27 | if custom_block.count("/") == 1: 28 | splitted = custom_block.split("/") 29 | function_name = splitted[1].replace(".mcfunction", "") 30 | custom_block = splitted[0] 31 | if function_name == "second": 32 | custom_blocks_second.append(custom_block) 33 | elif function_name == "tick": 34 | custom_blocks_tick.append(custom_block) 35 | 36 | # For each custom block, add tags when placed 37 | for custom_block in custom_blocks_second: 38 | write_file(f"{custom_blocks}/{custom_block}/place_secondary.mcfunction", f"\n# Add tag for loop every second\ntag @s add {namespace}.second\nscoreboard players add #second_entities {namespace}.data 1\n") 39 | write_file(f"{custom_blocks}/{custom_block}/destroy.mcfunction", f"\n# Decrease the number of entities with second tag\nscoreboard players remove #second_entities {namespace}.data 1\n") 40 | for custom_block in custom_blocks_tick: 41 | write_file(f"{custom_blocks}/{custom_block}/place_secondary.mcfunction", f"\n# Add tag for loop every tick\ntag @s add {namespace}.tick\nscoreboard players add #tick_entities {namespace}.data 1\n") 42 | write_file(f"{custom_blocks}/{custom_block}/destroy.mcfunction", f"\n# Decrease the number of entities with tick tag\nscoreboard players remove #tick_entities {namespace}.data 1\n") 43 | 44 | # Write second functions 45 | version: str = config['version'] 46 | if custom_blocks_second: 47 | score_check: str = f"score #second_entities {namespace}.data matches 1.." 48 | write_file(f"{functions}/v{version}/second.mcfunction", f"\n# Custom blocks second functions\nexecute if {score_check} as @e[tag={namespace}.second] at @s run function {namespace}:custom_blocks/second") 49 | content = "\n".join(f"execute if entity @s[tag={namespace}.{custom_block}] run function {namespace}:custom_blocks/{custom_block}/second" for custom_block in custom_blocks_second) 50 | write_file(f"{custom_blocks}/second.mcfunction", content) 51 | 52 | # Write in stats_custom_blocks 53 | write_function(config, f"{namespace}:_stats_custom_blocks",f'scoreboard players add #second_entities {namespace}.data 0\n', prepend = True) 54 | write_function(config, f"{namespace}:_stats_custom_blocks", 55 | f'tellraw @s [{{"text":"- \'second\' tag function: ","color":"green"}},{{"score":{{"name":"#second_entities","objective":"{namespace}.data"}},"color":"dark_green"}}]\n' 56 | ) 57 | 58 | 59 | # Write tick functions 60 | if custom_blocks_tick: 61 | score_check: str = f"score #tick_entities {namespace}.data matches 1.." 62 | write_file(f"{functions}/v{version}/tick.mcfunction", f"\n# Custom blocks tick functions\nexecute if {score_check} as @e[tag={namespace}.tick] at @s run function {namespace}:custom_blocks/tick") 63 | content = "\n".join(f"execute if entity @s[tag={namespace}.{custom_block}] run function {namespace}:custom_blocks/{custom_block}/tick" for custom_block in custom_blocks_tick) 64 | write_file(f"{custom_blocks}/tick.mcfunction", content) 65 | 66 | # Write in stats_custom_blocks 67 | write_function(config, f"{namespace}:_stats_custom_blocks",f'scoreboard players add #tick_entities {namespace}.data 0\n', prepend = True) 68 | write_function(config, f"{namespace}:_stats_custom_blocks", 69 | f'tellraw @s [{{"text":"- \'tick\' tag function: ","color":"green"}},{{"score":{{"name":"#tick_entities","objective":"{namespace}.data"}},"color":"dark_green"}}]\n' 70 | ) 71 | 72 | -------------------------------------------------------------------------------- /python_package/src/python_datapack/datapack/headers.py: -------------------------------------------------------------------------------- 1 | 2 | # Imports 3 | import json 4 | from typing import Any 5 | 6 | from ..utils.io import FILES_TO_WRITE 7 | 8 | 9 | def main(config: dict): 10 | 11 | # Get all mcfunctions paths 12 | mcfunctions: dict[str, dict[str, Any]] = {} 13 | functions_folder = "/function/" 14 | for file_path in FILES_TO_WRITE: 15 | if functions_folder in file_path and file_path.endswith(".mcfunction"): 16 | 17 | # Get namespace of the file 18 | splitted = file_path.split(functions_folder, 1) 19 | namespace = splitted[0].split("/")[-1] 20 | 21 | # Get string that is used for calling the function (ex: "namespace:my_function") 22 | to_be_called = f"{config['namespace']}:" + splitted[1].replace(".mcfunction","") 23 | 24 | # Add to mcfunctions dictionary 25 | mcfunctions[to_be_called] = {"path": file_path, "within": []} 26 | 27 | # For each json file, get the functions that it calls 28 | functions_tags_folder = "/tags/function/" 29 | advancements_folder = "/advancement/" 30 | for file_path in FILES_TO_WRITE: 31 | if file_path.endswith(".json"): 32 | if functions_tags_folder in file_path: 33 | 34 | # Get namespace of the file 35 | splitted = file_path.split(functions_tags_folder, 1) 36 | namespace = splitted[0].split("/")[-1] 37 | 38 | # Get string that is used for calling the function (ex: "#namespace:my_function") 39 | to_be_called = f"#{namespace}:" + splitted[1].replace(".json","") 40 | 41 | # Read the json file and loop its values 42 | data = json.loads(FILES_TO_WRITE[file_path]) 43 | if data.get("values"): 44 | for value in data["values"]: 45 | 46 | # Get the function that is called, either "function" or {"id": "function", ...} 47 | calling = value if isinstance(value, str) else value["id"] 48 | 49 | # If the called function is registered, append the name of this file 50 | if calling in mcfunctions.keys() and to_be_called not in mcfunctions[calling]["within"]: 51 | mcfunctions[calling]["within"].append(to_be_called) 52 | 53 | elif advancements_folder in file_path: 54 | 55 | # Get namespace of the file 56 | splitted = file_path.split(advancements_folder, 1) 57 | namespace = splitted[0].split("/")[-1] 58 | 59 | # Get string that is used for calling the function (ex: "advancement namespace:my_function") 60 | to_be_called = f"advancement {namespace}:" + splitted[1].replace(".json","") 61 | 62 | # Read the json file and loop its values 63 | data: dict = json.loads(FILES_TO_WRITE[file_path]) 64 | if data.get("rewards", {}) and data["rewards"].get("function"): 65 | calling = data["rewards"]["function"] 66 | if calling in mcfunctions.keys() and to_be_called not in mcfunctions[calling]["within"]: 67 | mcfunctions[calling]["within"].append(to_be_called) 68 | 69 | 70 | # For each mcfunction file, look at each lines 71 | for file, data in mcfunctions.items(): 72 | for line in FILES_TO_WRITE[data["path"]].split("\n"): 73 | 74 | # If the line call a function, 75 | if "function " in line: 76 | 77 | # Get the called function 78 | splitted = line.split("function ", 1)[1].replace("\n","").split(" ") 79 | calling = splitted[0].replace('"', '').replace("'", "") 80 | 81 | # Get additional text like macros, ex: function iyc:function {id:"51"} 82 | more = "" 83 | if len(splitted) > 0: 84 | more = " " + " ".join(splitted[1:]) # Add Macros or schedule time 85 | 86 | # If the called function is registered, append the name of this file as well as the additional text 87 | if calling in mcfunctions.keys() and (file + more) not in mcfunctions[calling]["within"]: 88 | mcfunctions[calling]["within"].append(file + more) 89 | 90 | 91 | # For each mcfunction file, add the header 92 | for file, data in mcfunctions.items(): 93 | 94 | # Get file content 95 | content = FILES_TO_WRITE[data["path"]] 96 | if not content.startswith("\n"): 97 | content = "\n" + content 98 | 99 | # Prepare header 100 | header: str = f""" 101 | #> {file} 102 | # 103 | # @within\t""" 104 | 105 | # Get all the calling function and join them with new lines 106 | withins = "\n#\t\t\t".join(w.strip() for w in data["within"]) 107 | if data["within"]: 108 | header += withins + "\n#\n" 109 | else: 110 | header += "???\n#\n" 111 | 112 | # Re-write the file 113 | FILES_TO_WRITE[data["path"]] = header + content 114 | 115 | -------------------------------------------------------------------------------- /python_package/src/python_datapack/datapack/lang.py: -------------------------------------------------------------------------------- 1 | 2 | # Imports 3 | import re 4 | 5 | import stouputils as stp 6 | 7 | from ..utils.io import FILES_TO_WRITE 8 | 9 | # Regex pattern for text extraction 10 | TEXT_RE: re.Pattern = re.compile( 11 | r''' 12 | (?P["']?text["']?\s*:\s*) # Match the "text": part 13 | (?P["']) # Opening quote for value 14 | (?P(?:\\.|[^\\])*?) # The value, handling escapes 15 | (?P=quote) # Closing quote 16 | ''', re.VERBOSE 17 | ) 18 | 19 | 20 | def extract_texts(content: str) -> list[tuple[str, int, int, str]]: 21 | """ Extract all text values from content using regex patterns. 22 | 23 | Args: 24 | content (str): The content to extract text from. 25 | 26 | Returns: 27 | list[tuple[str, int, int, str]]: List of tuples containing (text, start_pos, end_pos, quote_char) 28 | """ 29 | matches: list[tuple[str, int, int, str]] = [] 30 | for match in TEXT_RE.finditer(content): 31 | start, end = match.span() 32 | value: str = match.group("value") 33 | quote: str = match.group("quote") 34 | matches.append((value, start, end, quote)) 35 | return matches 36 | 37 | 38 | # Main function 39 | def main(config: dict): 40 | # Prepare lang dictionary and lang_format function 41 | lang: dict[str, str] = {} 42 | 43 | @stp.simple_cache() 44 | def lang_format(text: str) -> tuple[str, str]: 45 | """ Format text into a valid lang key. 46 | 47 | Args: 48 | text (str): The text to format. 49 | 50 | Returns: 51 | tuple[str, str]: The formatted key and a simplified version of it. 52 | """ 53 | text = re.sub(r"[./:]", "_", text) # Clean up all unwanted chars 54 | text = re.sub(r"[^a-zA-Z0-9 _-]", "", text).lower() 55 | alpha_num: str = re.sub(r"[ _-]+", "_", text).strip("_")[:64] 56 | namespace: str = config['namespace'] 57 | key: str = f"{namespace}.{alpha_num}" if not alpha_num.startswith(namespace) else alpha_num 58 | return key, re.sub(r"[._]", "", alpha_num) 59 | 60 | def handle_file(file: str, content: str): 61 | # Extract all text matches 62 | matches: list[tuple[str, int, int, str]] = extract_texts(content) 63 | 64 | # Process matches in reverse to avoid position shifting 65 | for text, start, end, quote in reversed(matches): 66 | # Clean text and skip if not useful 67 | clean_text: str = text.replace("\\n", "\n").replace("\\", "") 68 | if not any(c.isalnum() for c in clean_text): 69 | continue 70 | 71 | key_for_lang, verif = lang_format(clean_text) 72 | if len(verif) < 3 or not verif.isalnum() or "\\u" in text or "$(" in text: 73 | continue 74 | 75 | if key_for_lang not in lang: 76 | lang[key_for_lang] = clean_text 77 | elif lang[key_for_lang] != clean_text: 78 | continue 79 | 80 | # Replace whole "text": "value" with "translate": "key" 81 | new_fragment: str = f'{quote}translate{quote}: {quote}{key_for_lang}{quote}' 82 | content = content[:start] + new_fragment + content[end:] 83 | 84 | # Write the new content to the file 85 | FILES_TO_WRITE[file] = content 86 | 87 | # Show progress of the handle_file function 88 | stp.multithreading(handle_file, FILES_TO_WRITE.items(), use_starmap=True, desc="Generating lang file", max_workers=min(32, len(FILES_TO_WRITE))) 89 | 90 | # Sort the lang dictionary (by value) 91 | lang = dict(sorted(lang.items(), key=lambda x: x[1])) 92 | 93 | # Write the lang file 94 | path: str = f"{config['build_resource_pack']}/assets/minecraft/lang/en_us.json" 95 | FILES_TO_WRITE[path] = stp.super_json_dump(lang) 96 | 97 | -------------------------------------------------------------------------------- /python_package/src/python_datapack/datapack/loading.py: -------------------------------------------------------------------------------- 1 | 2 | # Imports 3 | import stouputils as stp 4 | 5 | from ...stewbeet.core.constants import NOT_COMPONENTS 6 | from ..utils.io import write_file, write_load_file, write_versioned_function 7 | 8 | 9 | def main(config: dict): 10 | version: str = config['version'] 11 | namespace: str = config['namespace'] 12 | major, minor, patch = version.split(".") 13 | 14 | # Setup enumerate and resolve functions 15 | write_versioned_function(config, "load/enumerate", 16 | f""" 17 | # If current major is too low, set it to the current major 18 | execute unless score #{namespace}.major load.status matches {major}.. run scoreboard players set #{namespace}.major load.status {major} 19 | 20 | # If current minor is too low, set it to the current minor (only if major is correct) 21 | execute if score #{namespace}.major load.status matches {major} unless score #{namespace}.minor load.status matches {minor}.. run scoreboard players set #{namespace}.minor load.status {minor} 22 | 23 | # If current patch is too low, set it to the current patch (only if major and minor are correct) 24 | execute if score #{namespace}.major load.status matches {major} if score #{namespace}.minor load.status matches {minor} unless score #{namespace}.patch load.status matches {patch}.. run scoreboard players set #{namespace}.patch load.status {patch} 25 | """) 26 | write_versioned_function(config, "load/resolve", 27 | f""" 28 | # If correct version, load the datapack 29 | execute if score #{namespace}.major load.status matches {major} if score #{namespace}.minor load.status matches {minor} if score #{namespace}.patch load.status matches {patch} run function {namespace}:v{version}/load/main 30 | """) 31 | 32 | # Setup enumerate and resolve function tags 33 | write_file(f"{config['build_datapack']}/data/{namespace}/tags/function/enumerate.json", stp.super_json_dump({"values": [f"{namespace}:v{version}/load/enumerate"]})) 34 | write_file(f"{config['build_datapack']}/data/{namespace}/tags/function/resolve.json", stp.super_json_dump({"values": [f"{namespace}:v{version}/load/resolve"]})) 35 | 36 | # Setup load main function 37 | write_versioned_function(config, "load/main", 38 | f""" 39 | # Avoiding multiple executions of the same load function 40 | execute unless score #{namespace}.loaded load.status matches 1 run function {namespace}:v{version}/load/secondary 41 | 42 | """) 43 | 44 | # Confirm load 45 | items_storage = "" # Storage representation of every item in the database 46 | if config['database']: 47 | items_storage += f"\n# Items storage\ndata modify storage {namespace}:items all set value {{}}\n" 48 | for item, data in config['database'].items(): 49 | 50 | # Prepare storage data with item_model component in first 51 | mc_data = {"id":"","count":1, "components":{"minecraft:item_model":""}} 52 | for k, v in data.items(): 53 | if k not in NOT_COMPONENTS: 54 | 55 | # Add 'minecraft:' if missing 56 | if ":" not in k: 57 | k = f"minecraft:{k}" 58 | 59 | # Copy component 60 | mc_data["components"][k] = v 61 | 62 | # Copy the id 63 | elif k == "id": 64 | mc_data[k] = v 65 | 66 | # If no item_model, remove it 67 | if mc_data["components"]["minecraft:item_model"] == "": 68 | del mc_data["components"]["minecraft:item_model"] 69 | 70 | # Append to the storage database, json_dump adds 71 | 72 | items_storage += f"data modify storage {namespace}:items all.{item} set value " + stp.super_json_dump(mc_data, max_level = 0) 73 | 74 | # Write the loading tellraw and score, along with the final dataset 75 | write_load_file(config, 76 | f""" 77 | # Confirm load 78 | tellraw @a[tag=convention.debug] {{"text":"[Loaded {config['project_name']} v{version}]","color":"green"}} 79 | scoreboard players set #{namespace}.loaded load.status 1 80 | """ + items_storage) 81 | 82 | -------------------------------------------------------------------------------- /python_package/src/python_datapack/datapack/loot_tables.py: -------------------------------------------------------------------------------- 1 | 2 | # Imports 3 | import stouputils as stp 4 | 5 | from ...stewbeet.core.constants import NOT_COMPONENTS, RESULT_OF_CRAFTING 6 | from ..utils.io import write_file, write_function 7 | 8 | 9 | def main(config: dict): 10 | database: dict[str,dict] = config["database"] 11 | external_database: dict[str,dict] = config["external_database"] 12 | namespace: str = config["namespace"] 13 | 14 | # For each item in the database, create a loot table 15 | for item, data in database.items(): 16 | loot_table = {"pools":[{"rolls":1,"entries":[{"type":"minecraft:item", "name": data.get("id")}]}]} 17 | 18 | # Set components 19 | set_components = {"function":"minecraft:set_components","components":{}} 20 | for k, v in data.items(): 21 | if k not in NOT_COMPONENTS: 22 | set_components["components"][f"minecraft:{k}"] = v 23 | 24 | # Add functions 25 | loot_table["pools"][0]["entries"][0]["functions"] = [set_components] 26 | 27 | json_content: str = stp.super_json_dump(loot_table, max_level = 10) 28 | write_file(f"{config['build_datapack']}/data/{namespace}/loot_table/i/{item}.json", json_content) 29 | 30 | # Same for external items 31 | for item, data in external_database.items(): 32 | ns, item = item.split(":") 33 | loot_table = {"pools":[{"rolls":1,"entries":[{"type":"minecraft:item", "name": data.get("id")}]}]} 34 | set_components = {"function":"minecraft:set_components","components":{}} 35 | for k, v in data.items(): 36 | if k not in NOT_COMPONENTS: 37 | set_components["components"][f"minecraft:{k}"] = v 38 | loot_table["pools"][0]["entries"][0]["functions"] = [set_components] 39 | write_file(f"{config['build_datapack']}/data/{namespace}/loot_table/external/{ns}/{item}.json", stp.super_json_dump(loot_table, max_level = 9)) 40 | 41 | 42 | # Loot tables for items with crafting recipes 43 | for item, data in database.items(): 44 | if data.get(RESULT_OF_CRAFTING): 45 | results = [] 46 | for d in data[RESULT_OF_CRAFTING]: 47 | d: dict 48 | count = d.get("result_count", 1) 49 | if count != 1: 50 | results.append(count) 51 | 52 | # For each result count, create a loot table for it 53 | for result_count in results: 54 | loot_table = {"pools":[{"rolls":1,"entries":[{"type":"minecraft:loot_table","value":f"{namespace}:i/{item}","functions":[{"function":"minecraft:set_count","count":result_count}]}]}]} 55 | write_file(f"{config['build_datapack']}/data/{namespace}/loot_table/i/{item}_x{result_count}.json", stp.super_json_dump(loot_table, max_level = -1), overwrite = True) 56 | 57 | # Second loot table for the manual (if present) 58 | if "manual" in database: 59 | loot_table = {"pools":[{"rolls":1,"entries":[{"type":"minecraft:loot_table","value":f"{namespace}:i/manual"}]}]} 60 | write_file(f"{config['build_datapack']}/data/{namespace}/loot_table/i/{namespace}_manual.json", stp.super_json_dump(loot_table, max_level = -1), overwrite = True) 61 | 62 | # Make a give all command that gives chests with all the items 63 | CHEST_SIZE = 27 64 | total_chests = (len(database) + CHEST_SIZE - 1) // CHEST_SIZE 65 | lore = stp.super_json_dump(config["source_lore"], max_level = 0).replace("\n", "") 66 | chests = [] 67 | database_copy = list(database.items()) 68 | for i in range(total_chests): 69 | chest_contents = [] 70 | 71 | # For each slot of the chest, append an item and remove it from the copy 72 | for j in range(CHEST_SIZE): 73 | if not database_copy: 74 | break 75 | item, data = database_copy.pop(0) 76 | data = data.copy() 77 | id = data.get("id") 78 | for k in NOT_COMPONENTS: # Remove non-component data 79 | if data.get(k, None) is not None: 80 | del data[k] 81 | json_content = stp.super_json_dump(data, max_level = 0).replace("\n","") 82 | chest_contents.append(f'{{slot:{j},item:{{count:1,id:"{id}",components:{json_content}}}}}') 83 | joined_content = ",".join(chest_contents) 84 | chests.append(f'give @s chest[container=[{joined_content}],custom_name={{"text":"Chest [{i+1}/{total_chests}]","color":"yellow"}},lore=[{lore}]]') 85 | write_function(config, f"{namespace}:_give_all", "\n" + "\n\n".join(chests) + "\n\n") 86 | 87 | -------------------------------------------------------------------------------- /python_package/src/python_datapack/datapack/main.py: -------------------------------------------------------------------------------- 1 | 2 | # Imports 3 | import stouputils as stp 4 | 5 | from .custom_blocks import main as custom_blocks_main 6 | from .loading import main as loading_main 7 | from .loot_tables import main as loot_tables_main 8 | 9 | 10 | @stp.measure_time(stp.info, "Datapack successfully generated") 11 | def main(config: dict): 12 | print() 13 | 14 | # Generate datapack loading 15 | loading_main(config) 16 | 17 | if config.get("database"): 18 | 19 | # Custom Blocks (place + destroy) 20 | custom_blocks_main(config) 21 | 22 | # Generate items loot tables 23 | loot_tables_main(config) 24 | 25 | -------------------------------------------------------------------------------- /python_package/src/python_datapack/debug_info.py: -------------------------------------------------------------------------------- 1 | 2 | # Imports 3 | import threading 4 | 5 | import requests 6 | import stouputils as stp 7 | 8 | from ..stewbeet.core.constants import MINECRAFT_VERSION 9 | 10 | # Constants 11 | END_SERVER: str = "https://paralya.fr/stewbeet_debug.php" 12 | 13 | """ 14 | 51 | """ 52 | 53 | def main(config: dict): 54 | """ Send debug info to the end server in a background thread. 55 | 56 | Args: 57 | config (dict): The configuration dictionary to send 58 | """ 59 | def send_debug_info(): 60 | try: 61 | # Create a copy of the config without override_model key 62 | config_copy: dict = config.copy() 63 | if "database" in config_copy: 64 | database_copy: dict[str, dict] = {} 65 | for item, data in config_copy["database"].items(): 66 | database_copy[item] = data.copy() 67 | if "override_model" in database_copy[item]: 68 | del database_copy[item]["override_model"] 69 | config_copy["database"] = database_copy 70 | config_copy["mc_version"] = MINECRAFT_VERSION 71 | 72 | json_data: str = stp.super_json_dump(config_copy) 73 | requests.post(END_SERVER, json={"data":json_data}) 74 | 75 | # Ignore any error 76 | except BaseException: 77 | pass 78 | 79 | # Start thread in background 80 | thread: threading.Thread = threading.Thread(target=send_debug_info, daemon=True) 81 | thread.start() 82 | 83 | -------------------------------------------------------------------------------- /python_package/src/python_datapack/enhance_config.py: -------------------------------------------------------------------------------- 1 | 2 | # Imports 3 | import os 4 | 5 | import stouputils as stp 6 | 7 | from .resource_pack.source_lore_font import main as source_lore_font 8 | 9 | 10 | # Main function 11 | def main(config: dict) -> dict: 12 | 13 | # Assets files 14 | if config.get('assets_folder'): 15 | config['assets_files'] = [stp.clean_path(f"{root}/{f}") for root, _, files in os.walk(config['assets_folder']) for f in files] 16 | textures: str = f"{config['assets_folder']}/textures" 17 | if os.path.exists(textures): 18 | config['textures_files'] = [path.split(f"{textures}/")[1] for path in config['assets_files'] if path.startswith(textures) and path.endswith(".png")] 19 | 20 | # Datapack related constants 21 | config['project_name_simple'] = "".join([c for c in config['project_name'] if c.isalnum()]) # Simplified version of the datapack name, used for paths 22 | 23 | # Technical constants 24 | config['build_datapack'] = f"{config['build_folder']}/datapack" # Folder where the final datapack will be built 25 | config['build_resource_pack'] = f"{config['build_folder']}/resource_pack" # Folder where the final resource pack will be built 26 | 27 | # If the source_lore has an ICON text component, make a font 28 | config = source_lore_font(config) 29 | 30 | return config 31 | 32 | -------------------------------------------------------------------------------- /python_package/src/python_datapack/initialize.py: -------------------------------------------------------------------------------- 1 | 2 | # Imports 3 | import os 4 | import shutil 5 | 6 | import stouputils as stp 7 | 8 | from ..stewbeet.core.constants import DATAPACK_FORMAT, RESOURCE_PACK_FORMAT 9 | from .utils.io import read_initial_files, write_file 10 | 11 | 12 | def main(config: dict): 13 | 14 | # Delete database_debug 15 | if config.get("database_debug"): 16 | shutil.rmtree(config["database_debug"], ignore_errors=True) 17 | 18 | # Read initial files in build folder 19 | read_initial_files([config["build_datapack"], config["build_resource_pack"]]) 20 | 21 | # Setup pack.mcmeta for the datapack 22 | pack_mcmeta = {"pack":{"pack_format": DATAPACK_FORMAT, "description": config["description"]}, "id": config["namespace"]} 23 | write_file(f"{config['build_datapack']}/pack.mcmeta", stp.super_json_dump(pack_mcmeta)) 24 | 25 | # Setup pack.mcmeta for the resource pack 26 | pack_mcmeta = {"pack":{"pack_format": RESOURCE_PACK_FORMAT, "description": config["description"]}, "id": config["namespace"]} 27 | write_file(f"{config['build_resource_pack']}/pack.mcmeta", stp.super_json_dump(pack_mcmeta)) 28 | 29 | # Convert textures names if needed 30 | if config.get('textures_files'): 31 | REPLACEMENTS = { 32 | "_off": "", 33 | "_down": "_bottom", 34 | "_up": "_top", 35 | "_north": "_front", 36 | "_south": "_back", 37 | "_west": "_left", 38 | "_east": "_right", 39 | } 40 | for file in config['textures_files']: 41 | new_name = file.lower() 42 | for k, v in REPLACEMENTS.items(): 43 | if k in file: 44 | new_name = new_name.replace(k, v) 45 | if new_name != file: 46 | os.rename(f"{config['assets_folder']}/textures/{file}", f"{config['assets_folder']}/textures/{new_name}") 47 | stp.info(f"Renamed {file} to {new_name}") 48 | pass 49 | 50 | -------------------------------------------------------------------------------- /python_package/src/python_datapack/manual/assets/furnace.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stoupy51/PythonDatapackTemplate/701c87f48c817e69aa45ff247f33eeac2841cff2/python_package/src/python_datapack/manual/assets/furnace.png -------------------------------------------------------------------------------- /python_package/src/python_datapack/manual/assets/heavy_workbench.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stoupy51/PythonDatapackTemplate/701c87f48c817e69aa45ff247f33eeac2841cff2/python_package/src/python_datapack/manual/assets/heavy_workbench.png -------------------------------------------------------------------------------- /python_package/src/python_datapack/manual/assets/invisible_item.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stoupy51/PythonDatapackTemplate/701c87f48c817e69aa45ff247f33eeac2841cff2/python_package/src/python_datapack/manual/assets/invisible_item.png -------------------------------------------------------------------------------- /python_package/src/python_datapack/manual/assets/invisible_item_release.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stoupy51/PythonDatapackTemplate/701c87f48c817e69aa45ff247f33eeac2841cff2/python_package/src/python_datapack/manual/assets/invisible_item_release.png -------------------------------------------------------------------------------- /python_package/src/python_datapack/manual/assets/minecraft_font.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stoupy51/PythonDatapackTemplate/701c87f48c817e69aa45ff247f33eeac2841cff2/python_package/src/python_datapack/manual/assets/minecraft_font.ttf -------------------------------------------------------------------------------- /python_package/src/python_datapack/manual/assets/none.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stoupy51/PythonDatapackTemplate/701c87f48c817e69aa45ff247f33eeac2841cff2/python_package/src/python_datapack/manual/assets/none.png -------------------------------------------------------------------------------- /python_package/src/python_datapack/manual/assets/none_release.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stoupy51/PythonDatapackTemplate/701c87f48c817e69aa45ff247f33eeac2841cff2/python_package/src/python_datapack/manual/assets/none_release.png -------------------------------------------------------------------------------- /python_package/src/python_datapack/manual/assets/pulverizing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stoupy51/PythonDatapackTemplate/701c87f48c817e69aa45ff247f33eeac2841cff2/python_package/src/python_datapack/manual/assets/pulverizing.png -------------------------------------------------------------------------------- /python_package/src/python_datapack/manual/assets/shaped_2x2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stoupy51/PythonDatapackTemplate/701c87f48c817e69aa45ff247f33eeac2841cff2/python_package/src/python_datapack/manual/assets/shaped_2x2.png -------------------------------------------------------------------------------- /python_package/src/python_datapack/manual/assets/shaped_3x3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stoupy51/PythonDatapackTemplate/701c87f48c817e69aa45ff247f33eeac2841cff2/python_package/src/python_datapack/manual/assets/shaped_3x3.png -------------------------------------------------------------------------------- /python_package/src/python_datapack/manual/assets/simple_case_no_border.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stoupy51/PythonDatapackTemplate/701c87f48c817e69aa45ff247f33eeac2841cff2/python_package/src/python_datapack/manual/assets/simple_case_no_border.png -------------------------------------------------------------------------------- /python_package/src/python_datapack/manual/assets/wiki_information.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stoupy51/PythonDatapackTemplate/701c87f48c817e69aa45ff247f33eeac2841cff2/python_package/src/python_datapack/manual/assets/wiki_information.png -------------------------------------------------------------------------------- /python_package/src/python_datapack/manual/assets/wiki_ingredient_of_craft.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stoupy51/PythonDatapackTemplate/701c87f48c817e69aa45ff247f33eeac2841cff2/python_package/src/python_datapack/manual/assets/wiki_ingredient_of_craft.png -------------------------------------------------------------------------------- /python_package/src/python_datapack/manual/assets/wiki_ingredient_of_craft_template.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stoupy51/PythonDatapackTemplate/701c87f48c817e69aa45ff247f33eeac2841cff2/python_package/src/python_datapack/manual/assets/wiki_ingredient_of_craft_template.png -------------------------------------------------------------------------------- /python_package/src/python_datapack/manual/assets/wiki_result_of_craft.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stoupy51/PythonDatapackTemplate/701c87f48c817e69aa45ff247f33eeac2841cff2/python_package/src/python_datapack/manual/assets/wiki_result_of_craft.png -------------------------------------------------------------------------------- /python_package/src/python_datapack/manual/book_components.py: -------------------------------------------------------------------------------- 1 | """ 2 | Handles generation of book components and content 3 | """ 4 | import os 5 | 6 | import stouputils as stp 7 | from PIL import Image 8 | 9 | from ..utils.ingredients import ingr_to_id 10 | from .image_utils import generate_high_res_font 11 | from .shared_import import COMPONENTS_TO_INCLUDE, NONE_FONT, get_page_number 12 | 13 | 14 | # Call the previous function 15 | def high_res_font_from_ingredient(config: dict, ingredient: str|dict, count: int = 1) -> str: 16 | """ Generate the high res font to display in the manual for the ingredient 17 | Args: 18 | ingredient (str|dict): The ingredient, ex: "adamantium_fragment" or {"item": "minecraft:stick"} or {"components": {"custom_data": {"iyc": {"adamantium_fragment": true}}}} 19 | count (int): The count of the item 20 | Returns: 21 | str: The font to the generated texture 22 | """ 23 | # Decode the ingredient 24 | if isinstance(ingredient, dict): 25 | ingr_str: str = ingr_to_id(ingredient, add_namespace = True) 26 | else: 27 | ingr_str = ingredient 28 | 29 | # Get the item image 30 | if ':' in ingr_str: 31 | image_path = f"{config['manual_path']}/items/{ingr_str.replace(':', '/')}.png" 32 | if not os.path.exists(image_path): 33 | stp.error(f"Missing item texture at '{image_path}'") 34 | item_image = Image.open(image_path) 35 | ingr_str = ingr_str.split(":")[1] 36 | else: 37 | item_image = Image.open(f"{config['manual_path']}/items/{config['namespace']}/{ingr_str}.png") 38 | 39 | # Generate the high res font 40 | return generate_high_res_font(config, ingr_str, item_image, count) 41 | 42 | 43 | # Convert ingredient to formatted JSON for book 44 | def get_item_component(config: dict, ingredient: dict|str, only_those_components: list[str] = [], count: int = 1) -> dict: 45 | """ Generate item hover text for a craft ingredient 46 | Args: 47 | ingredient (dict|str): The ingredient 48 | ex: {'components': {'custom_data': {'iyc': {'adamantium_fragment': True}}}} 49 | ex: {'item': 'minecraft:stick'} 50 | ex: "adamantium_fragment" # Only available for the datapack items 51 | Returns: 52 | dict: The text component 53 | ex: {"text":NONE_FONT,"color":"white","hover_event":{"action":"show_item","id":"minecraft:command_block", "components": {...}},"click_event":{"action":"change_page","value":"8"}} 54 | ex: {"text":NONE_FONT,"color":"white","hover_event":{"action":"show_item","id":"minecraft:stick"}} 55 | """ 56 | # Get the item id 57 | formatted: dict = { 58 | "text": NONE_FONT, 59 | "hover_event": { 60 | "action": "show_item", 61 | "id": "", # Inline contents field 62 | "components": {} # Will be added if needed 63 | } 64 | } 65 | 66 | if isinstance(ingredient, dict) and ingredient.get("item"): 67 | formatted["hover_event"]["id"] = ingredient["item"] 68 | else: 69 | # Get the item in the database 70 | if isinstance(ingredient, str): 71 | id = ingredient 72 | item = config['database'][ingredient] 73 | else: 74 | custom_data: dict = ingredient["components"]["minecraft:custom_data"] 75 | id = ingr_to_id(ingredient, add_namespace = False) 76 | if custom_data.get(config['namespace']): 77 | item = config['database'].get(id) 78 | else: 79 | ns = list(custom_data.keys())[0] + ":" 80 | for data in custom_data.values(): 81 | item = config['external_database'].get(ns + list(data.keys())[0]) 82 | if item: 83 | break 84 | if not item: 85 | stp.error("Item not found in database or external database: " + str(ingredient)) 86 | 87 | # Copy id and components 88 | formatted["hover_event"]["id"] = item["id"].replace("minecraft:", "") 89 | components = {} 90 | if only_those_components: 91 | for key in only_those_components: 92 | if key in item: 93 | components[key] = item[key] 94 | else: 95 | for key, value in item.items(): 96 | if key in COMPONENTS_TO_INCLUDE: 97 | components[key] = value 98 | formatted["hover_event"]["components"] = components 99 | 100 | # If item is from my datapack, get its page number 101 | page_number = get_page_number(id) 102 | if page_number != -1: 103 | formatted["click_event"] = { 104 | "action": "change_page", 105 | "page": page_number 106 | } 107 | 108 | # High resolution 109 | if config["manual_high_resolution"]: 110 | formatted["text"] = high_res_font_from_ingredient(config, ingredient, count) 111 | 112 | # Return 113 | return formatted 114 | 115 | -------------------------------------------------------------------------------- /python_package/src/python_datapack/manual/book_optimizer.py: -------------------------------------------------------------------------------- 1 | 2 | # Page optimizer 3 | def optimize_element(content: list|dict|str) -> list|dict|str: 4 | """ Optimize the page content by merging compounds when possible 5 | Args: 6 | content (list|dict|str): The page content 7 | Returns: 8 | list|dict|str: The optimized page content 9 | """ 10 | # If dict, optimize the values 11 | if isinstance(content, dict): 12 | if not any(x in content for x in ["text", "translate", "contents"]): # If not a text, translate or contents, just return 13 | return content 14 | content = content.copy() 15 | new_content = {} 16 | for key, value in content.items(): 17 | new_content[key] = optimize_element(value) 18 | return new_content 19 | 20 | # If not a list, just return 21 | if not isinstance(content, list): 22 | return content 23 | 24 | # If list with only one element, return the element 25 | if len(content) == 1: 26 | return content[0] 27 | 28 | # For each compound 29 | new_content = [] 30 | for i, compound in enumerate(content): 31 | if isinstance(compound, list) or i == 0: 32 | new_content.append(optimize_element(compound)) 33 | else: 34 | # If the current is a dict with only "text" key, transform it to a string 35 | if isinstance(compound, dict) and len(compound) == 1 and "text" in compound: 36 | compound = compound["text"] 37 | 38 | # For checks 39 | compound_without_text = compound.copy() if isinstance(compound, dict) else compound 40 | previous_without_text = new_content[-1].copy() if isinstance(new_content[-1], dict) else new_content[-1] 41 | if isinstance(compound, dict) and isinstance(new_content[-1], dict): 42 | compound_without_text.pop("text", None) 43 | previous_without_text.pop("text", None) # type: ignore 44 | 45 | # If the previous compound is the same as the current one, merge the text 46 | if str(compound_without_text) == str(previous_without_text): 47 | 48 | # If the previous compound is a text, merge the text 49 | if isinstance(new_content[-1], str): 50 | new_content[-1] += str(compound) 51 | 52 | # If the previous compound is a dict, merge the dict 53 | elif isinstance(new_content[-1], dict): 54 | new_content[-1]["text"] += compound["text"] 55 | 56 | # Always add break lines to the previous part (if the text is a string containing only break lines) 57 | elif isinstance(compound, str) and all([c == "\n" for c in compound]): 58 | if isinstance(new_content[-1], str): 59 | new_content[-1] += compound 60 | elif isinstance(new_content[-1], dict): 61 | new_content[-1]["text"] += compound 62 | 63 | # Always merge two strings 64 | elif isinstance(compound, str) and isinstance(new_content[-1], str): 65 | new_content[-1] += compound 66 | 67 | # Otherwise, just add the optimized compound 68 | else: 69 | new_content.append(optimize_element(compound)) 70 | 71 | # Return 72 | return new_content 73 | 74 | # Remove events recursively 75 | EVENTS: list[str] = ["hover_event", "click_event"] 76 | def remove_events(compound: dict|list): 77 | """ Remove events from a compound recursively 78 | Args: 79 | compound (dict): The compound 80 | """ 81 | if not isinstance(compound, dict): 82 | if isinstance(compound, list): 83 | for element in compound: 84 | remove_events(element) 85 | return 86 | for key in EVENTS: 87 | if key in compound: 88 | del compound[key] 89 | for value in compound.values(): 90 | remove_events(value) 91 | 92 | # Function 93 | def optimize_book(book_content: list|dict|str) -> list|dict|str: 94 | """ Optimize the book content by associating compounds when possible 95 | Args: 96 | book_content (list|dict|str): The book content 97 | Returns: 98 | list|dict|str: The optimized book content 99 | """ 100 | if not isinstance(book_content, list): 101 | book_content = [book_content] 102 | 103 | # For each page, remove events if useless (in an item Lore for example) 104 | for page in book_content: 105 | for compound in page: 106 | if isinstance(compound, list): 107 | l = compound 108 | else: 109 | l = [compound] 110 | for e in l: 111 | if isinstance(e, dict): 112 | for event_type in EVENTS: 113 | if e.get(event_type): 114 | remove_events(e[event_type]) # Remove all events below the first event 115 | 116 | # For each page, optimize the array 117 | return optimize_element(book_content) 118 | 119 | -------------------------------------------------------------------------------- /python_package/src/python_datapack/manual/image_utils.py: -------------------------------------------------------------------------------- 1 | 2 | # Imports 3 | import os 4 | from typing import Any 5 | 6 | from PIL import Image, ImageDraw, ImageFont 7 | 8 | from .shared_import import ( 9 | MICRO_NONE_FONT, 10 | TEMPLATES_PATH, 11 | font_providers, 12 | get_next_font, 13 | ) 14 | 15 | 16 | # Generate high res simple case no border 17 | def load_simple_case_no_border(high_res: bool) -> Image.Image: 18 | path = f"{TEMPLATES_PATH}/simple_case_no_border.png" 19 | img = Image.open(path) 20 | if not high_res: 21 | return img 22 | 23 | # Make the image bigger on the right 24 | middle_x = img.size[0] // 2 25 | result = Image.new("RGBA", (img.size[0] + 1, img.size[1])) 26 | result.paste(img, (0, 0)) 27 | img = img.crop((middle_x, 0, img.size[0], img.size[1])) 28 | result.paste(img, (middle_x + 1, 0)) 29 | return result 30 | 31 | 32 | def careful_resize(image: Image.Image, max_result_size: int) -> Image.Image: 33 | """Resize an image while keeping the aspect ratio""" 34 | if image.size[0] >= image.size[1]: 35 | factor = max_result_size / image.size[0] 36 | return image.resize((max_result_size, int(image.size[1] * factor)), Image.Resampling.NEAREST) 37 | else: 38 | factor = max_result_size / image.size[1] 39 | return image.resize((int(image.size[0] * factor), max_result_size), Image.Resampling.NEAREST) 40 | 41 | def add_border(image: Image.Image, border_color: tuple[int, int, int, int], border_size: int, is_rectangle_shape: bool) -> Image.Image: 42 | """Add a border to every part of the image""" 43 | image = image.convert("RGBA") 44 | pixels: Any = image.load() 45 | 46 | if not is_rectangle_shape: 47 | pixels_to_change = [(x, y) for x in range(image.width) for y in range(image.height) if pixels[x, y][3] == 0] 48 | r = range(-border_size, border_size + 1) 49 | for x, y in pixels_to_change: 50 | try: 51 | if any(pixels[x + dx, y + dy][3] != 0 and pixels[x + dx, y + dy] != border_color for dx in r for dy in r): 52 | pixels[x, y] = border_color 53 | except Exception: 54 | pass 55 | else: 56 | height, width = 8, 8 57 | while height < image.height and pixels[8, height][3]!= 0: 58 | height += 1 59 | while width < image.width and pixels[width, 8][3]!= 0: 60 | width += 1 61 | 62 | border = Image.new("RGBA", (width + 2, height + 2), border_color) 63 | border.paste(image, (0, 0), image) 64 | image.paste(border, (0, 0), border) 65 | 66 | return image 67 | 68 | # Generate an image showing the result count 69 | def image_count(count: int) -> Image.Image: 70 | """ Generate an image showing the result count 71 | Args: 72 | count (int): The count to show 73 | Returns: 74 | Image: The image with the count 75 | """ 76 | # Create the image 77 | img = Image.new("RGBA", (32, 32), (0, 0, 0, 0)) 78 | draw = ImageDraw.Draw(img) 79 | font_size = 16 80 | font = ImageFont.truetype(f"{TEMPLATES_PATH}/minecraft_font.ttf", size = font_size) 81 | 82 | # Calculate text size and positions of the two texts 83 | text_width = draw.textlength(str(count), font = font) 84 | text_height = font_size + 4 85 | pos_1 = (34-text_width), (32-text_height) 86 | pos_2 = (32-text_width), (30-text_height) 87 | 88 | # Draw the count 89 | draw.text(pos_1, str(count), (50, 50, 50), font = font) 90 | draw.text(pos_2, str(count), (255, 255, 255), font = font) 91 | return img 92 | 93 | # Generate high res image for item 94 | def generate_high_res_font(config: dict, item: str, item_image: Image.Image, count: int = 1) -> str: 95 | """ Generate the high res font to display in the manual for the item 96 | Args: 97 | item (str): The name of the item, ex: "adamantium_fragment" 98 | item_image (Image): The image of the item 99 | count (int): The count of the item 100 | Returns: 101 | str: The font to the generated texture 102 | """ 103 | font = get_next_font() 104 | item = f"{item}_{count}" if count > 1 else item 105 | 106 | # Get output path 107 | path = f"{config['manual_path']}/font/high_res/{item}.png" 108 | provider_path = f"{config['namespace']}:font/high_res/{item}.png" 109 | for p in font_providers: # Check if it already exists 110 | if p["file"] == provider_path: 111 | return MICRO_NONE_FONT + p["chars"][0] 112 | font_providers.append({"type":"bitmap","file": provider_path, "ascent": 7, "height": 16, "chars": [font]}) 113 | 114 | 115 | # Generate high res font 116 | os.makedirs(os.path.dirname(path), exist_ok = True) 117 | high_res: int = 256 118 | resized = careful_resize(item_image, high_res) 119 | resized = resized.convert("RGBA") 120 | 121 | # Add the item count 122 | if count > 1: 123 | img_count = image_count(count) 124 | img_count = careful_resize(img_count, high_res) 125 | resized.paste(img_count, (0, 0), img_count) 126 | 127 | # Add invisible pixels for minecraft font at each corner 128 | total_width = resized.size[0] - 1 129 | total_height = resized.size[1] - 1 130 | angles = [(0, 0), (total_width, 0), (0, total_height), (total_width, total_height)] 131 | for angle in angles: 132 | resized.putpixel(angle, (0, 0, 0, 100)) 133 | 134 | # Save the result and return the font 135 | resized.save(path) 136 | return MICRO_NONE_FONT + font 137 | 138 | -------------------------------------------------------------------------------- /python_package/src/python_datapack/manual/iso_renders.py: -------------------------------------------------------------------------------- 1 | 2 | # Imports 3 | import os 4 | from pathlib import Path 5 | 6 | import requests 7 | import stouputils as stp 8 | from beet import ProjectConfig, run_beet 9 | from model_resolver import Render 10 | 11 | from ...stewbeet.core.constants import ( 12 | CUSTOM_BLOCK_VANILLA, 13 | DOWNLOAD_VANILLA_ASSETS_RAW, 14 | DOWNLOAD_VANILLA_ASSETS_SOURCE, 15 | OVERRIDE_MODEL, 16 | RESULT_OF_CRAFTING, 17 | USED_FOR_CRAFTING, 18 | ) 19 | from ..utils.io import super_copy 20 | 21 | 22 | # Generate iso renders for every item in the database 23 | def generate_all_iso_renders(config: dict): 24 | database: dict[str, dict] = config['database'] 25 | namespace: str = config['namespace'] 26 | 27 | # Create the items folder 28 | path = config['manual_path'] + "/items" 29 | os.makedirs(f"{path}/{namespace}", exist_ok = True) 30 | 31 | # For every item, get the model path and the destination path 32 | for_model_resolver: dict[str, str] = {} 33 | for item, data in database.items(): 34 | 35 | # Skip items that don't have models 36 | if not data.get("item_model"): 37 | continue 38 | 39 | # If it's not a block, simply copy the texture 40 | try: 41 | if data["id"] == CUSTOM_BLOCK_VANILLA: 42 | raise ValueError() 43 | if not os.path.exists(f"{path}/{namespace}/{item}.png") or not config['cache_manual_assets']: 44 | if data.get(OVERRIDE_MODEL, None) != {}: 45 | source: str = f"{config['assets_folder']}/textures/{item}.png" 46 | if os.path.exists(source): 47 | super_copy(source, f"{path}/{namespace}/{item}.png") 48 | else: 49 | stp.warning(f"Missing texture for item '{item}', please add it manually to '{path}/{namespace}/{item}.png'") 50 | except ValueError: 51 | # Else, add the block to the model resolver list 52 | # Skip if item is already generated (to prevent OpenGL launching for nothing) 53 | if os.path.exists(f"{path}/{namespace}/{item}.png") and config['cache_manual_assets']: 54 | continue 55 | 56 | # Add to the model resolver queue 57 | rp_path = f"{namespace}:item/{item}" 58 | dst_path = f"{path}/{namespace}/{item}.png" 59 | for_model_resolver[rp_path] = dst_path 60 | 61 | # Launch model resolvers for remaining blocks 62 | if len(for_model_resolver) > 0: 63 | load_dir = Path(config['build_resource_pack']) 64 | 65 | ## Model Resolver v0.12.0 66 | # model_resolver_main( 67 | # render_size = config['opengl_resolution'], 68 | # load_dir = load_dir, 69 | # output_dir = None, # type: ignore 70 | # use_cache = False, 71 | # minecraft_version = "latest", 72 | # __special_filter__ = for_model_resolver # type: ignore 73 | # ) 74 | 75 | ## Model Resolver v1.3.0 76 | beet_config = ProjectConfig( 77 | output=None, 78 | resource_pack={"load": load_dir, "name": load_dir.name}, # type: ignore 79 | meta={"model_resolver": {"dont_merge_datapack": True, "use_cache": True}} 80 | ) 81 | 82 | stp.debug(f"Generating iso renders for {len(for_model_resolver)} items, this may take a while...") 83 | with run_beet(config=beet_config, cache=True) as ctx: 84 | render = Render(ctx) 85 | for rp_path, dst_path in for_model_resolver.items(): 86 | render.add_model_task(rp_path, path_save=dst_path) 87 | render.run() 88 | stp.debug("Generated iso renders for all items") 89 | 90 | ## Copy every used vanilla items 91 | # Get every used vanilla items 92 | used_vanilla_items = set() 93 | for data in database.values(): 94 | all_crafts: list[dict] = list(data.get(RESULT_OF_CRAFTING,[])) 95 | all_crafts += list(data.get(USED_FOR_CRAFTING,[])) 96 | for recipe in all_crafts: 97 | ingredients = [] 98 | if "ingredients" in recipe: 99 | ingredients = recipe["ingredients"] 100 | if isinstance(ingredients, dict): 101 | ingredients = ingredients.values() 102 | elif "ingredient" in recipe: 103 | ingredients = [recipe["ingredient"]] 104 | for ingredient in ingredients: 105 | if "item" in ingredient: 106 | used_vanilla_items.add(ingredient["item"].split(":")[1]) 107 | if "result" in recipe and "item" in recipe["result"]: 108 | used_vanilla_items.add(recipe["result"]["item"].split(":")[1]) 109 | pass 110 | 111 | # Download all the vanilla textures from the wiki 112 | def download_item(item: str): 113 | destination = f"{path}/minecraft/{item}.png" 114 | if not (os.path.exists(destination) and config['cache_manual_assets']): # If not downloaded yet or not using cache 115 | response = requests.get(f"{DOWNLOAD_VANILLA_ASSETS_RAW}/item/{item}.png") 116 | if response.status_code == 200: 117 | with stp.super_open(destination, "wb") as file: 118 | file.write(response.content) 119 | else: 120 | stp.warning(f"Failed to download texture for item '{item}', please add it manually to '{destination}'") 121 | stp.warning(f"Suggestion link: '{DOWNLOAD_VANILLA_ASSETS_SOURCE}'") 122 | 123 | # Multithread the download 124 | stp.multithreading(download_item, used_vanilla_items, max_workers=min(32, len(used_vanilla_items))) 125 | 126 | -------------------------------------------------------------------------------- /python_package/src/python_datapack/manual/other_utils.py: -------------------------------------------------------------------------------- 1 | 2 | # Imports 3 | import stouputils as stp 4 | 5 | from ...stewbeet.core.constants import PULVERIZING, RESULT_OF_CRAFTING 6 | from ..utils.ingredients import ( 7 | CRAFTING_RECIPES_TYPES, 8 | FURNACES_RECIPES_TYPES, 9 | SPECIAL_RECIPES_TYPES, 10 | ingr_repr, 11 | ingr_to_id, 12 | ) 13 | from .shared_import import ( 14 | FURNACE_FONT, 15 | PULVERIZING_FONT, 16 | SHAPED_2X2_FONT, 17 | SHAPED_3X3_FONT, 18 | ) 19 | 20 | 21 | # Convert craft function 22 | @stp.simple_cache() 23 | def convert_shapeless_to_shaped(craft: dict) -> dict: 24 | """ Convert a shapeless craft to a shaped craft 25 | Args: 26 | craft (dict): The craft to convert 27 | Returns: 28 | dict: The craft converted 29 | """ 30 | new_craft = {"type": "crafting_shaped", "result_count": craft["result_count"], "ingredients": {}} 31 | if craft.get("result"): 32 | new_craft["result"] = craft["result"] 33 | 34 | # Get all ingredients to the dictionary 35 | next_key = "A" 36 | for ingr in craft["ingredients"]: 37 | key = next_key 38 | for new_key, new_ingr in new_craft["ingredients"].items(): 39 | if str(ingr) == str(new_ingr): 40 | key = new_key 41 | break 42 | 43 | if key == next_key: 44 | new_craft["ingredients"][next_key] = ingr 45 | next_key = chr(ord(next_key) + 1) 46 | 47 | # Make the shape of the craft, with an exception when 2 materials to put one alone in the center 48 | if len(new_craft["ingredients"]) == 2 and len(craft["ingredients"]) == 9: 49 | new_craft["shape"] = ["AAA","ABA","AAA"] 50 | else: 51 | 52 | # For each ingredient, add to the shape depending on the occurences 53 | new_craft["shape"] = [] 54 | for key, ingr in new_craft["ingredients"].items(): 55 | for ingr_craft in craft["ingredients"]: 56 | if str(ingr_craft) == str(ingr): 57 | new_craft["shape"].append(key) 58 | 59 | # Fix the shape (ex: ["A","A","A","B","B","B","C","C","C"] -> ["AAA","BBB","CCC"]) 60 | # ex 2: ["A","B","C","D"] -> ["AB","CD"] 61 | col_size = 3 62 | if len(new_craft["shape"]) <= 4: 63 | col_size = 2 64 | ranged = range(0, len(new_craft["shape"]), col_size) 65 | new_craft["shape"] = ["".join(new_craft["shape"][i:i + col_size]) for i in ranged] 66 | 67 | # Return the shaped craft 68 | return new_craft 69 | 70 | 71 | # Util function 72 | @stp.simple_cache() 73 | def high_res_font_from_craft(craft: dict) -> str: 74 | if craft["type"] in FURNACES_RECIPES_TYPES: 75 | return FURNACE_FONT 76 | elif craft["type"] == "crafting_shaped": 77 | if len(craft["shape"]) == 3 or len(craft["shape"][0]) == 3: 78 | return SHAPED_3X3_FONT 79 | else: 80 | return SHAPED_2X2_FONT 81 | elif craft["type"] == PULVERIZING: 82 | return PULVERIZING_FONT 83 | else: 84 | stp.error(f"Unknown craft type '{craft['type']}'") 85 | return "" 86 | 87 | def remove_unknown_crafts(crafts: list[dict]) -> list[dict]: 88 | """ Remove crafts that are not recognized by the program 89 | Args: 90 | crafts (list[dict]): The list of crafts 91 | Returns: 92 | list[dict]: The list of crafts without unknown crafts 93 | """ 94 | supported_crafts = [] 95 | for craft in crafts: 96 | if craft["type"] in CRAFTING_RECIPES_TYPES or craft["type"] in FURNACES_RECIPES_TYPES or craft["type"] in SPECIAL_RECIPES_TYPES: 97 | supported_crafts.append(craft) 98 | return supported_crafts 99 | 100 | # Generate USED_FOR_CRAFTING key like 101 | def generate_otherside_crafts(config: dict, item: str) -> list[dict]: 102 | """ Generate the USED_FOR_CRAFTING key like 103 | Args: 104 | item (str): The item to generate the key for 105 | Returns: 106 | list[dict]: ex: [{"type": "crafting_shaped","result_count": 1,"category": "equipment","shape": ["XXX","X X"],"ingredients": {"X": {"components": {"custom_data": {"iyc": {"chainmail": true}}}}},"result": {"item": "minecraft:chainmail_helmet","count": 1}}, ...] 107 | """ 108 | # Get all crafts that use the item 109 | crafts = [] 110 | for key, value in config['database'].items(): 111 | if key != item and value.get(RESULT_OF_CRAFTING): 112 | for craft in value[RESULT_OF_CRAFTING]: 113 | craft: dict = craft 114 | if ("ingredient" in craft and item == ingr_to_id(craft["ingredient"], False)) or \ 115 | ("ingredients" in craft and isinstance(craft["ingredients"], dict) and item in [ingr_to_id(x, False) for x in craft["ingredients"].values()]) or \ 116 | ("ingredients" in craft and isinstance(craft["ingredients"], list) and item in [ingr_to_id(x, False) for x in craft["ingredients"]]): 117 | # Convert craft, ex: 118 | # before: chainmail_helmet {"type": "crafting_shaped","result_count": 1,"category": "equipment","shape": ["XXX","X X"],"ingredients": {"X": {"components": {"custom_data": {"iyc": {"chainmail": true}}}}}}} 119 | # after: chainmail {"type": "crafting_shaped","result_count": 1,"category": "equipment","shape": ["XXX","X X"],"ingredients": {"X": {"components": {"custom_data": {"iyc": {"chainmail": true}}}}},"result": {"item": "minecraft:chainmail_helmet","count": 1}} 120 | craft_copy = craft.copy() 121 | craft_copy["result"] = ingr_repr(key, ns = config['namespace'], count = craft["result_count"]) 122 | crafts.append(craft_copy) 123 | return crafts 124 | 125 | -------------------------------------------------------------------------------- /python_package/src/python_datapack/manual/shared_import.py: -------------------------------------------------------------------------------- 1 | 2 | # Imports 3 | import os 4 | 5 | import stouputils as stp 6 | 7 | 8 | # Utils functions for fonts (item start at 0x0000, pages at 0xa000) 9 | # Return the character that will be used for font, ex: chr(0x0002) with i = 2 10 | def get_font(i: int) -> str: 11 | i += 0x0020 # Minecraft only allow starting this value 12 | if i > 0xffff: 13 | stp.error(f"Font index {i} is too big. Maximum is 0xffff.") 14 | return chr(i) 15 | def get_page_font(i: int) -> str: 16 | return get_font(i + 0x1000) 17 | def get_next_font() -> str: # Returns an incrementing value for each craft 18 | global next_craft_font 19 | next_craft_font += 1 20 | return get_font(next_craft_font) 21 | 22 | 23 | # Constants 24 | COMPONENTS_TO_INCLUDE: list[str] = ["item_name", "lore", "custom_name", "damage", "max_damage"] 25 | SQUARE_SIZE: int = 32 26 | MANUAL_ASSETS_PATH: str = stp.clean_path(os.path.dirname(os.path.realpath(__file__)) + "/") 27 | TEMPLATES_PATH: str = MANUAL_ASSETS_PATH + "templates" 28 | FONT_FILE: str = "manual" 29 | BORDER_COLOR_HEX: int = 0xB64E2F 30 | BORDER_COLOR: tuple[int, int, int, int] = (BORDER_COLOR_HEX >> 16) & 0xFF, (BORDER_COLOR_HEX >> 8) & 0xFF, BORDER_COLOR_HEX & 0xFF, 255 31 | BORDER_SIZE: int = 2 32 | HEAVY_WORKBENCH_CATEGORY: str = "__private_heavy_workbench" 33 | NONE_FONT: str = get_font(0x0000) 34 | MEDIUM_NONE_FONT: str = get_font(0x0001) 35 | SMALL_NONE_FONT: str = get_font(0x0002) 36 | VERY_SMALL_NONE_FONT: str = get_font(0x0003) 37 | MICRO_NONE_FONT: str = get_font(0x0004) 38 | WIKI_NONE_FONT: str = get_font(0x0005) 39 | WIKI_INFO_FONT: str = get_font(0x0006) 40 | WIKI_RESULT_OF_CRAFT_FONT: str = get_font(0x0007) 41 | WIKI_INGR_OF_CRAFT_FONT: str = get_font(0x0008) 42 | SHAPED_2X2_FONT: str = get_font(0x0009) 43 | SHAPED_3X3_FONT: str = get_font(0x000A) 44 | FURNACE_FONT: str = get_font(0x000B) 45 | PULVERIZING_FONT: str = get_font(0x000C) 46 | HOVER_SHAPED_2X2_FONT: str = get_font(0x000D) 47 | HOVER_SHAPED_3X3_FONT: str = get_font(0x000E) 48 | HOVER_FURNACE_FONT: str = get_font(0x000F) 49 | HOVER_PULVERIZING_FONT: str = get_font(0x0010) 50 | INVISIBLE_ITEM_FONT: str = get_font(0x0011) # Invisible item to place 51 | INVISIBLE_ITEM_WIDTH: str = INVISIBLE_ITEM_FONT + MICRO_NONE_FONT 52 | 53 | HOVER_EQUIVALENTS: dict[str, str] = { 54 | SHAPED_2X2_FONT: HOVER_SHAPED_2X2_FONT, 55 | SHAPED_3X3_FONT: HOVER_SHAPED_3X3_FONT, 56 | FURNACE_FONT: HOVER_FURNACE_FONT, 57 | PULVERIZING_FONT: HOVER_PULVERIZING_FONT, 58 | } 59 | 60 | # Global variables 61 | next_craft_font: int = 0x8000 62 | font_providers: list = [] 63 | manual_pages: list = [] 64 | 65 | # Get page number 66 | def get_page_number(item_id: str) -> int: 67 | for p in manual_pages: 68 | if p["name"] == item_id: 69 | return p["number"] 70 | return -1 71 | 72 | -------------------------------------------------------------------------------- /python_package/src/python_datapack/manual/text_components.py: -------------------------------------------------------------------------------- 1 | """ 2 | Handles text component formatting and updates for the manual 3 | """ 4 | from typing import Literal, Union 5 | 6 | 7 | def create_hover_event(action: Literal["show_text", "show_item"], value: Union[str, dict, list]) -> dict: 8 | """Creates a hover event with the new format""" 9 | if action == "show_text": 10 | return { 11 | "action": "show_text", 12 | "value": value 13 | } 14 | elif action == "show_item" and isinstance(value, dict): 15 | return { 16 | "action": "show_item", 17 | "id": value.get("id", ""), 18 | "components": value.get("components", {}) 19 | } 20 | return {} 21 | 22 | def create_click_event(action: Literal["change_page", "run_command", "suggest_command", "open_url"], value: Union[str, int]) -> dict: 23 | """Creates a click event with the new format""" 24 | if action == "change_page": 25 | return { 26 | "action": "change_page", 27 | "page": int(value) if isinstance(value, str) else value 28 | } 29 | elif action == "run_command": 30 | return { 31 | "action": "run_command", 32 | "command": value 33 | } 34 | elif action == "suggest_command": 35 | return { 36 | "action": "suggest_command", 37 | "command": value 38 | } 39 | elif action == "open_url": 40 | return { 41 | "action": "open_url", 42 | "url": value 43 | } 44 | return {} 45 | 46 | -------------------------------------------------------------------------------- /python_package/src/python_datapack/resource_pack/check_unused_textures.py: -------------------------------------------------------------------------------- 1 | 2 | # Imports 3 | import re 4 | from re import Pattern 5 | from typing import Any 6 | 7 | import stouputils as stp 8 | 9 | from ..utils.io import FILES_TO_WRITE 10 | 11 | 12 | def main(config: dict[str, Any]) -> None: 13 | """ Check for unused textures in the resource pack. 14 | 15 | Args: 16 | config (dict[str, Any]): Configuration containing: 17 | - "textures_files": list[str] of all texture filenames (e.g. ["dirt.png", "stone.png", ...]) 18 | - "assets_folder": base path to the assets folder (e.g. "my_pack/assets/") 19 | """ 20 | # 1) Build a set of all texture names (without ".png"): 21 | all_textures: set[str] = {fname.rsplit(".png", 1)[0] for fname in config["textures_files"]} 22 | 23 | # 2) Concatenate all JSON contents into one big string: 24 | all_json_content: str = " ".join( 25 | content 26 | for path, content in FILES_TO_WRITE.items() 27 | if path.endswith(".json") 28 | ) 29 | 30 | # 3) Run a single regex to extract ANY substring that looks like '/' or ':' 31 | # followed by either ".png" or a closing quote ". 32 | # This regex will catch both: 33 | # "/dirt.png" 34 | # ":stone" 35 | # "/subfolder/brick.png" (we'll strip off directories later) 36 | # 37 | # Breakdown of the pattern: 38 | # (?:(?:/|:)) → match either '/' or ':' but don't capture it 39 | # ( → start capturing group #1 40 | # [^/:"\\]+ → one or more chars that are NOT '/', ':', '"', or backslash 41 | # ) → end capturing group 42 | # (?=\.png|" ) → lookahead: next char must be either ".png" or a double-quote 43 | # 44 | # In practice, if your JSON references look like "/namespace:texture_name" or "modid:foo/bar/baz", 45 | # you may need to tweak the class [^/:"\\]+ to include forward slashes. Here, we assume your 46 | # texture references do NOT include subdirectories. If they DO, see the "Note on subfolders" below. 47 | texture_ref_regex: Pattern[str] = re.compile(r'(?:(?:/|:))([^/:"\\]+)(?=\.png|")') 48 | 49 | found_matches: set[str] = set(texture_ref_regex.findall(all_json_content)) 50 | 51 | # 4) Now finally compute unused: 52 | unused_textures: set[str] = all_textures.difference(found_matches) 53 | 54 | # 5) If anything is unused, warn about it: 55 | if unused_textures: 56 | sorted_unused: list[str] = sorted(unused_textures) 57 | unused_paths: list[str] = [ 58 | f"{config['assets_folder']}/textures/{name}.png" 59 | for name in sorted_unused 60 | ] 61 | warning_lines: list[str] = [ 62 | f"'{stp.relative_path(path)}' not used in the resource pack" 63 | for path in unused_paths 64 | ] 65 | warning_msg: str = ( 66 | "Some textures are not used in the resource pack:\n" 67 | + "\n".join(warning_lines) 68 | ) 69 | stp.warning(warning_msg) 70 | 71 | -------------------------------------------------------------------------------- /python_package/src/python_datapack/resource_pack/main.py: -------------------------------------------------------------------------------- 1 | 2 | # Imports 3 | from ..utils.io import write_all_files 4 | from .item_models import main as item_models_main 5 | from .power_of_2 import main as check_all_textures_power_of_2 6 | from .sounds import main as sounds_main 7 | 8 | 9 | def main(config: dict): 10 | 11 | # Add the sounds folder to the resource pack 12 | sounds_main(config) 13 | 14 | # For each item, copy textures and make models 15 | item_models_main(config) 16 | 17 | # Check all textures if they are in power of 2 resolution 18 | check_all_textures_power_of_2(config) 19 | 20 | # Write resource pack files to write 21 | build_rp: str = config["build_resource_pack"] 22 | write_all_files(contains=f"{build_rp}/assets") 23 | 24 | -------------------------------------------------------------------------------- /python_package/src/python_datapack/resource_pack/power_of_2.py: -------------------------------------------------------------------------------- 1 | 2 | # Imports 3 | import os 4 | 5 | import stouputils as stp 6 | from PIL import Image 7 | 8 | 9 | def main(config: dict): 10 | 11 | # Create a test texture (to comment this line after testing) 12 | # img = Image.new('RGB', (17,17), color = 'red') 13 | # img.save(f"{config['build_resource_pack']}/assets/{config['namespace']}/textures/item/test.png") 14 | 15 | # Get all textures in the resource pack folder 16 | wrongs: list[tuple[str, int, int]] = [] 17 | for root, _, files in os.walk(f"{config['build_resource_pack']}/assets/{config['namespace']}/textures"): 18 | for file in files: 19 | if file.endswith(".png") and ("block" in root or "item" in root): 20 | file_path: str = stp.clean_path(f"{root}/{file}") 21 | 22 | # Check if the texture is in power of 2 resolution 23 | image: Image.Image = Image.open(file_path) 24 | width, height = image.size 25 | if bin(width).count("1") != 1 or bin(height).count("1") != 1: # At least one of them is not a power of 2 26 | 27 | # If width can't divide height, add it to the wrongs list (else it's probably a GUI or animation texture) 28 | if height % width != 0: 29 | wrongs.append((file_path, width, height)) 30 | 31 | # Print all wrong textures 32 | if wrongs: 33 | text: str = "The following textures are not in power of 2 resolution (2x2, 4x4, 8x8, 16x16, ...):\n" 34 | for file_path, width, height in wrongs: 35 | text += f"- {file_path}\t({width}x{height})\n" 36 | stp.warning(text) 37 | 38 | -------------------------------------------------------------------------------- /python_package/src/python_datapack/resource_pack/sounds.py: -------------------------------------------------------------------------------- 1 | 2 | # Imports 3 | import os 4 | import re 5 | import time 6 | from collections import defaultdict 7 | 8 | import stouputils as stp 9 | 10 | from ..utils.io import super_copy, write_file 11 | 12 | 13 | def main(config: dict): 14 | sounds_folder: str = stp.clean_path(config.get("assets_folder", "") + "/sounds") 15 | if sounds_folder != "/sounds" and not os.path.exists(sounds_folder): 16 | return 17 | 18 | # Get all files 19 | all_files: list[str] = [stp.clean_path(f"{root}/{file}") for root, _, files in os.walk(sounds_folder) for file in files] 20 | sounds_names: list[str] = [sound for sound in all_files if sound.endswith(".ogg") or sound.endswith(".wav")] 21 | 22 | # Add the sounds folder to the resource pack 23 | if sounds_names: 24 | start_time: float = time.perf_counter() 25 | 26 | # Dictionary to group sound variants 27 | sound_groups: dict[str, list[str]] = defaultdict(list) 28 | 29 | def handle_sound(sound: str) -> None: 30 | rel_sound: str = sound.replace(sounds_folder + "/", "") 31 | 32 | # Get sound without spaces and special characters 33 | sound_file: str = "".join(char for char in rel_sound.replace(" ", "_") if char.isalnum() or char in "._/").lower() 34 | 35 | # Copy to resource pack 36 | super_copy(f"{sounds_folder}/{rel_sound}", f"{config['build_resource_pack']}/assets/{config['namespace']}/sounds/{sound_file}") 37 | 38 | # Get sound without file extension 39 | sound_file_no_ext: str = ".".join(sound_file.split(".")[:-1]) 40 | 41 | # Check if sound is a numbered variant (e.g. name_01, name_02 or name1, name2) 42 | base_name_match = re.match(r'(.+?)(?:_)?(\d+)$', sound_file_no_ext) 43 | if base_name_match: 44 | base_name: str = base_name_match.group(1) 45 | sound_groups[base_name].append(sound_file_no_ext) 46 | else: 47 | # Not a numbered variant, add as individual sound 48 | sound_groups[sound_file_no_ext] = [sound_file_no_ext] 49 | 50 | # Process sounds in parallel 51 | stp.multithreading(handle_sound, sounds_names, max_workers=min(32, len(sounds_names))) 52 | 53 | # Create sounds.json with grouped sounds 54 | sounds_json: dict = {} 55 | for base_name, variants in sorted(sound_groups.items()): 56 | # Convert directory separators to dots for the sound ID 57 | sound_id: str = base_name 58 | 59 | if len(variants) > 1: 60 | # Multiple variants - group them 61 | sounds_json[sound_id] = { 62 | "subtitle": sound_id.split("/")[-1], 63 | "sounds": [f"{config['namespace']}:{variant}" for variant in sorted(variants)] 64 | } 65 | else: 66 | # Single sound 67 | sounds_json[sound_id] = { 68 | "subtitle": sound_id.split("/")[-1], 69 | "sounds": [f"{config['namespace']}:{variants[0]}"] 70 | } 71 | 72 | # Write the sounds.json file 73 | write_file(f"{config['build_resource_pack']}/assets/{config['namespace']}/sounds.json", stp.super_json_dump(sounds_json)) 74 | 75 | total_time: float = time.perf_counter() - start_time 76 | stp.info(f"All sounds in '{stp.replace_tilde(sounds_folder)}/' have been copied to the resource pack in {total_time:.5f}s") 77 | 78 | 79 | -------------------------------------------------------------------------------- /python_package/src/python_datapack/resource_pack/source_lore_font.py: -------------------------------------------------------------------------------- 1 | 2 | # Imports 3 | import os 4 | 5 | import stouputils as stp 6 | from PIL import Image 7 | 8 | from ..utils.io import write_file 9 | 10 | 11 | # Utility functions 12 | def main(config: dict) -> dict: 13 | namespace: str = config["namespace"] 14 | source_lore: list[dict|str] = config.get("source_lore", []) 15 | 16 | # If the source_lore has an ICON text component and original_icon is present, 17 | if source_lore and any(isinstance(component, dict) and "ICON" == component.get("text") for component in source_lore) and config.get("assets_folder"): 18 | original_icon: str = config["assets_folder"] + "/original_icon.png" 19 | if not os.path.exists(original_icon): 20 | return config 21 | 22 | # Create the font file 23 | write_file( 24 | f"{config['build_resource_pack']}/assets/{namespace}/font/icons.json", 25 | stp.super_json_dump({"providers": [{"type": "bitmap","file": f"{namespace}:font/original_icon.png","ascent": 8,"height": 9,"chars": ["I"]}]}) 26 | ) 27 | 28 | # Copy the original icon to the resource pack 29 | destination: str = f"{config['build_resource_pack']}/assets/{namespace}/textures/font/original_icon.png" 30 | os.makedirs(os.path.dirname(destination), exist_ok=True) 31 | image: Image.Image = Image.open(original_icon).convert("RGBA") 32 | if image.width > 256: 33 | image = image.resize((256, 256)) 34 | image.save(destination) 35 | 36 | # Replace every ICON text component with the original icon 37 | for component in source_lore: 38 | if isinstance(component, dict) and component.get("text") == "ICON": 39 | component["text"] = "I" 40 | component["color"] = "white" 41 | component["italic"] = False 42 | component["font"] = f"{namespace}:icons" 43 | source_lore.insert(0, "") 44 | config["source_lore"] = source_lore 45 | 46 | return config 47 | 48 | -------------------------------------------------------------------------------- /python_package/src/python_datapack/watcher.py: -------------------------------------------------------------------------------- 1 | 2 | # Imports 3 | import os 4 | import sys 5 | import threading 6 | import time 7 | 8 | import stouputils as stp 9 | from watchdog.events import FileSystemEvent, FileSystemEventHandler 10 | from watchdog.observers import Observer 11 | 12 | 13 | # Build processor 14 | class BuildProcessor(threading.Thread): 15 | def __init__(self, build_script: str): 16 | super().__init__() 17 | self.queue: list[str] = [] 18 | self.running = True 19 | self.build_script = build_script 20 | 21 | def run(self): 22 | while self.running: 23 | if len(self.queue) > 0: 24 | # Clear the queue and get the latest change 25 | latest_change = None 26 | while len(self.queue) > 0: 27 | latest_change = self.queue.pop(0) 28 | self.queue = [] 29 | 30 | if latest_change: 31 | print(f"Processing changes... (Latest: {latest_change})") 32 | os.system(f"{sys.executable} {self.build_script}") 33 | 34 | # Wait a bit before processing more changes 35 | time.sleep(1) 36 | 37 | def stop(self): 38 | self.running = False 39 | 40 | def add_to_queue(self, file_path: str): 41 | self.queue.append(file_path) 42 | 43 | # File change handler 44 | class ChangeHandler(FileSystemEventHandler): 45 | def __init__(self, to_watch: list[str], to_ignore: list[str], processor: BuildProcessor): 46 | """ Class to handle file changes 47 | 48 | Args: 49 | to_watch (list[str]): List of paths to watch (starts with) 50 | to_ignore (list[str]): List of paths to ignore (contains) 51 | processor (BuildProcessor): Thread that processes the builds 52 | """ 53 | self.to_watch: list[str] = to_watch 54 | self.to_ignore: list[str] = to_ignore 55 | self.processor = processor 56 | super().__init__() 57 | 58 | def on_modified(self, event: FileSystemEvent): 59 | """ Function called when a file is modified 60 | 61 | Args: 62 | event (FileSystemEvent): Watchdog event 63 | """ 64 | source_path: str = os.path.abspath(str(event.src_path)).replace("\\", "/") 65 | if not event.is_directory and not any(x in source_path for x in self.to_ignore) and any(source_path.startswith(x) for x in self.to_watch): 66 | self.processor.add_to_queue(source_path) 67 | 68 | # Main watcher 69 | def watcher(to_watch: list[str], to_ignore: list[str], build_script: str): 70 | """ Start a watcher to monitor file changes and automatically build the datapack 71 | 72 | Args: 73 | to_watch (list[str]): List of paths to watch (starts with) 74 | to_ignore (list[str]): List of paths to ignore (contains) 75 | build_script (str): Path to the build script 76 | """ 77 | # Start the build processor thread 78 | processor = BuildProcessor(build_script) 79 | processor.start() 80 | 81 | event_handler: ChangeHandler = ChangeHandler(to_watch, to_ignore, processor) 82 | observer = Observer() 83 | observer.schedule(event_handler, ".", recursive=True) 84 | observer.start() 85 | stp.warning("Watching for file changes... (Press Ctrl+C to stop)") 86 | 87 | try: 88 | while True: 89 | observer.join(1) 90 | except KeyboardInterrupt: 91 | processor.stop() 92 | observer.stop() 93 | observer.join() 94 | processor.join() 95 | stp.warning("Watcher stopped") 96 | 97 | -------------------------------------------------------------------------------- /python_package/src/stewbeet/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /python_package/src/stewbeet/__main__.py: -------------------------------------------------------------------------------- 1 | 2 | # Imports 3 | from .cli import main # noqa: F401 4 | 5 | -------------------------------------------------------------------------------- /python_package/src/stewbeet/cli.py: -------------------------------------------------------------------------------- 1 | 2 | # Imports 3 | import importlib 4 | import os 5 | import shutil 6 | import subprocess 7 | import sys 8 | 9 | from beet import ProjectConfig, load_config 10 | from box import Box 11 | from stouputils.decorators import LogLevels, handle_error 12 | from stouputils.print import info 13 | 14 | 15 | @handle_error(message="Error while running 'stewbeet'") 16 | def main(): 17 | second_arg: str = sys.argv[1] if len(sys.argv) == 2 else "" 18 | 19 | # Try to find and load the beet configuration file 20 | for ext in (".json", ".yml", ".yaml", ".toml"): 21 | if os.path.exists(f"beet{ext}"): 22 | cfg: ProjectConfig = load_config(filename=f"beet{ext}") 23 | break 24 | 25 | # Check if the command is "clean" or "rebuild" 26 | if second_arg in ["clean", "rebuild"]: 27 | info("Cleaning project and caches...") 28 | 29 | # Remove the beet cache directory 30 | subprocess.run(["beet", "cache", "-c"], check=False, capture_output=True) 31 | if os.path.exists(".beet_cache"): 32 | shutil.rmtree(".beet_cache", ignore_errors=True) 33 | 34 | # Remove the output directory specified in the config 35 | shutil.rmtree(cfg.output, ignore_errors=True) 36 | 37 | # Remove all __pycache__ folders 38 | for root, dirs, _ in os.walk("."): 39 | if "__pycache__" in dirs: 40 | cache_dir: str = os.path.join(root, "__pycache__") 41 | shutil.rmtree(cache_dir, ignore_errors=True) 42 | 43 | # Load metadata from config into a Box 44 | meta: Box = Box(cfg.meta, default_box=True, default_box_attr={}) 45 | 46 | # Remove manual cache directory if specified in metadata 47 | cache_path: str = meta.stewbeet.manual.cache_path 48 | if cache_path and os.path.exists(cache_path): 49 | shutil.rmtree(cache_path, ignore_errors=True) 50 | 51 | # Remove debug database file if it exists 52 | database_debug: str = meta.stewbeet.database_debug 53 | if database_debug and os.path.exists(database_debug): 54 | os.remove(database_debug) 55 | info("Cleaning done!") 56 | 57 | if second_arg != "clean": 58 | 59 | # Add current directory to Python path 60 | current_dir: str = os.getcwd() 61 | if current_dir not in sys.path: 62 | sys.path.insert(0, current_dir) 63 | 64 | # Try to import all pipeline 65 | for plugin in cfg.pipeline: 66 | handle_error(importlib.import_module, error_log=LogLevels.ERROR_TRACEBACK)(plugin) 67 | 68 | # Run beet with all remaining arguments 69 | subprocess.run(["beet"] + [x for x in sys.argv[1:] if x != "rebuild"], check=False) 70 | 71 | 72 | if __name__ == "__main__": 73 | main() 74 | 75 | -------------------------------------------------------------------------------- /python_package/src/stewbeet/core/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | # Imports 3 | from .__memory__ import * 4 | from .constants import * 5 | from .database_helper import * 6 | from .ingredients import * 7 | 8 | -------------------------------------------------------------------------------- /python_package/src/stewbeet/core/__memory__.py: -------------------------------------------------------------------------------- 1 | 2 | # pyright: reportAssignmentType=false 3 | # Imports 4 | from typing import Any 5 | 6 | from beet import Context, ListOption 7 | from box import Box 8 | 9 | 10 | # Shared variables among plugins 11 | class Mem: 12 | ctx: Context = None 13 | """ Global context object that holds the beet project configuration. 14 | This is set during plugins.initialize and used throughout the codebase. """ 15 | 16 | database: Box[str, Any] = Box({}, default_box=True, default_box_attr={}) 17 | """ Main database that stores all item and block data for the project. 18 | Uses Box object for dynamic attribute access and automatic dictionary creation. """ 19 | 20 | external_database: Box[str, Any] = Box({}, default_box=True, default_box_attr={}) 21 | """ Secondary database for storing external dependencies and compatibility data. 22 | Uses Box for dynamic attribute access and automatic dictionary creation. """ 23 | 24 | 25 | # Utility function for assertions on ctx 26 | def assert_ctx(members: ListOption) -> None: 27 | """ Assert that required context metadata paths exist. 28 | 29 | Args: 30 | members (ListOption): List of paths to check in ctx. 31 | """ 32 | for path in members: 33 | if path == "meta.stewbeet.sounds_folder": 34 | assert Mem.ctx.meta.stewbeet.sounds_folder, "Sounds folder not found in context metadata, please fill meta.stewbeet.sounds_folder with a directory path." 35 | elif path == "meta.stewbeet.records_folder": 36 | assert Mem.ctx.meta.stewbeet.records_folder, "Records folder not found in context metadata, please fill meta.stewbeet.records_folder with a directory path." 37 | elif path == "meta.stewbeet.textures_folder": 38 | assert Mem.ctx.meta.stewbeet.textures_folder, "Textures folder not found in context metadata, please fill meta.stewbeet.textures_folder with a directory path." 39 | 40 | -------------------------------------------------------------------------------- /python_package/src/stewbeet/core/database_helper/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | # Imports 3 | from ..constants import * 4 | from ..ingredients import * 5 | from .completion import * 6 | from .equipments import * 7 | from .materials import * 8 | from .records import * 9 | from .smart_ore_generation import * 10 | from .text import * 11 | 12 | -------------------------------------------------------------------------------- /python_package/src/stewbeet/core/database_helper/completion.py: -------------------------------------------------------------------------------- 1 | 2 | # ruff: noqa: RUF012 3 | # Imports 4 | from __future__ import annotations 5 | 6 | from typing import Any 7 | 8 | from beet.core.utils import TextComponent 9 | from box import Box 10 | 11 | from ..__memory__ import Mem 12 | 13 | 14 | # Add item model component 15 | def add_item_model_component(black_list: list[str] | None = None) -> None: 16 | """ Add an item model component to all items in the database. 17 | 18 | Args: 19 | black_list (list[str]): The list of items to ignore. 20 | """ 21 | if black_list is None: 22 | black_list = [] 23 | for item, data in Mem.database.items(): 24 | if item in black_list or data.get("item_model", None) is not None: 25 | continue 26 | data["item_model"] = f"{Mem.ctx.project_id}:{item}" 27 | return 28 | 29 | # Add item name and lore 30 | def add_item_name_and_lore_if_missing(is_external: bool = False, black_list: list[str] | None = None) -> None: 31 | """ Add item name and lore to all items in the database if they are missing. 32 | 33 | Args: 34 | is_external (bool): Whether the database is the external one or not (meaning the namespace is in the item name). 35 | black_list (list[str]): The list of items to ignore. 36 | """ 37 | # Load the source lore 38 | if black_list is None: 39 | black_list = [] 40 | source_lore: TextComponent = Mem.ctx.meta.stewbeet.source_lore 41 | 42 | # For each item, add item name and lore if missing (if not in black_list) 43 | for item, data in Mem.database.items(): 44 | if item in black_list: 45 | continue 46 | 47 | # Add item name if none 48 | if not data.get("item_name"): 49 | if not is_external: 50 | item_str: str = item.replace("_"," ").title() 51 | else: 52 | item_str: str = item.split(":")[-1].replace("_"," ").title() 53 | data["item_name"] = {"text": item_str, "italic": False, "color":"white"} 54 | 55 | # Apply namespaced lore if none 56 | if not data.get("lore"): 57 | data["lore"] = [] 58 | 59 | # If item is not external, 60 | if not is_external: 61 | 62 | # Add the source lore ONLY if not already present 63 | if source_lore not in data["lore"]: 64 | data["lore"].append(source_lore) 65 | 66 | # If item is external, add the source lore to the item lore (without ICON) 67 | else: 68 | # Extract the namespace 69 | titled_namespace: str = item.split(":")[0].replace("_"," ").title() 70 | 71 | # Create the new namespace lore with the titled namespace 72 | new_source_lore: dict[str, Any] = {"text": titled_namespace, "italic": True, "color": "blue"} 73 | 74 | # Add the namespace lore ONLY if not already present 75 | if new_source_lore not in data["lore"]: 76 | data["lore"].append(new_source_lore) 77 | return 78 | 79 | # Add private custom data for namespace 80 | def add_private_custom_data_for_namespace(is_external: bool = False) -> None: 81 | """ Add private custom data for namespace to all items in the database if they are missing. 82 | 83 | Args: 84 | is_external (bool): Whether the database is the external one or not (meaning the namespace is in the item name). 85 | """ 86 | for item, data in Mem.database.items(): 87 | if not data.get("custom_data"): 88 | data["custom_data"] = {} 89 | if not is_external: 90 | ns, id = Mem.ctx.project_id, item 91 | elif ":" in item: 92 | ns, id = item.split(":") 93 | if not data["custom_data"].get(ns): 94 | data["custom_data"][ns] = {} 95 | data["custom_data"][ns][id] = True 96 | return 97 | 98 | # Smithed ignore convention 99 | def add_smithed_ignore_vanilla_behaviours_convention() -> None: 100 | """ Add smithed convention to all items in the database if they are missing. 101 | 102 | Refer to https://wiki.smithed.dev/conventions/tag-specification/#custom-items for more information. 103 | """ 104 | for data in Mem.database.values(): 105 | data["custom_data"] = Box(data.get("custom_data", {}), default_box=True, default_box_attr={}, default_box_create_on_get=True) 106 | data["custom_data"].smithed.ignore.functionality = True 107 | data["custom_data"].smithed.ignore.crafting = True 108 | 109 | -------------------------------------------------------------------------------- /python_package/src/stewbeet/core/database_helper/records.py: -------------------------------------------------------------------------------- 1 | 2 | # Imports 3 | import os 4 | from string import ascii_lowercase, digits 5 | 6 | from beet import JukeboxSong, Sound 7 | from mutagen.oggvorbis import OggVorbis 8 | from stouputils.decorators import handle_error 9 | from stouputils.io import clean_path, super_json_dump 10 | from stouputils.print import error, warning 11 | 12 | from ..__memory__ import Mem, assert_ctx 13 | from ..constants import CATEGORY, CUSTOM_ITEM_VANILLA 14 | 15 | 16 | # Cleaning function 17 | def clean_record_name(name: str) -> str: 18 | """ Clean a record name by removing special characters and converting to lowercase. 19 | 20 | Args: 21 | name (str): The name to clean 22 | 23 | Returns: 24 | str: The cleaned name containing only lowercase letters, numbers and underscores 25 | """ 26 | name = name.replace(".ogg", "").lower() 27 | to_replace = [" ", "-", "___"] 28 | for r in to_replace: 29 | name = name.replace(r, "_") 30 | return "".join([c for c in name if c in ascii_lowercase + digits + "_"]) 31 | 32 | 33 | # Custom records 34 | @handle_error 35 | def generate_custom_records(records: dict[str, str] | str | None = "auto", category: str | None = None) -> None: 36 | """ Generate custom records by searching in assets/records/ for the files and copying them to the database and resource pack folder. 37 | 38 | Args: 39 | database (dict[str, dict]): The database to add the custom records items to, ex: {"record_1": "song.ogg", "record_2": "another_song.ogg"} 40 | records (dict[str, str]): The custom records to apply, ex: {"record_1": "My first Record.ogg", "record_2": "A second one.ogg"} 41 | category (str): The database category to apply to the custom records (ex: "music"). 42 | """ 43 | # Assertions 44 | assert records is None or isinstance(records, dict) or records in ["auto", "all"], ( 45 | f"Error during custom record generation: records must be a dictionary, 'auto', or 'all' (got {type(records).__name__})" 46 | ) 47 | assert_ctx("meta.stewbeet.records_folder") 48 | records_folder: str = clean_path(Mem.ctx.meta.stewbeet.records_folder) 49 | 50 | # If no records specified, search in the records folder 51 | if not records or records in ["auto", "all"]: 52 | songs: list[str] = [x for x in os.listdir(records_folder) if x.endswith((".ogg",".wav"))] 53 | records_to_check: dict[str, str] = { clean_record_name(file): file for file in songs } 54 | else: 55 | records_to_check = records # type: ignore 56 | 57 | # For each record, add it to the database 58 | for record, sound in records_to_check.items(): 59 | # Validate sound file format 60 | if not isinstance(sound, str): 61 | error(f"Error during custom record generation: sound '{sound}' is not a string, got {type(sound).__name__}") 62 | if not sound.endswith(".ogg"): 63 | warning(f"Error during custom record generation: sound '{sound}' is not an ogg file") 64 | continue 65 | 66 | # Extract item name from sound file 67 | item_name: str = ".".join(sound.split(".")[:-1]) # Remove the file extension 68 | 69 | # Create database entry for the record 70 | Mem.database[record] = { 71 | "id": CUSTOM_ITEM_VANILLA, 72 | "custom_data": {Mem.ctx.project_id:{record: True}, "smithed":{"dict":{"record": {record: True}}}}, 73 | "item_name": {"text":"Music Disc", "italic": False}, 74 | "jukebox_playable": f"{Mem.ctx.project_id}:{record}", 75 | "max_stack_size": 1, 76 | "rarity": "rare", 77 | } 78 | if category: 79 | Mem.database[record][CATEGORY] = category 80 | 81 | # Process sound file 82 | file_path: str = f"{records_folder}/{sound}" 83 | if os.path.exists(file_path): 84 | try: 85 | # Get song duration from Ogg file 86 | duration: int = round(OggVorbis(file_path).info.length) # type: ignore 87 | 88 | # Create and write jukebox song configuration 89 | json_song: dict = { 90 | "comparator_output": duration % 16, 91 | "length_in_seconds": duration + 1, 92 | "sound_event": {"sound_id":f"{Mem.ctx.project_id}:{record}"}, 93 | "description": {"text": item_name} 94 | } 95 | Mem.ctx.data[f"{Mem.ctx.project_id}:{record}"] = JukeboxSong(super_json_dump(json_song)) 96 | 97 | # Create and write sound 98 | Mem.ctx.assets[f"{Mem.ctx.project_id}:{record}"] = Sound(source_path=file_path, stream=True) 99 | 100 | except Exception as e: 101 | error(f"Error during custom record generation of '{file_path}', make sure it is using proper Ogg format: {e}") 102 | else: 103 | warning(f"Error during custom record generation: path '{file_path}' does not exist") 104 | 105 | -------------------------------------------------------------------------------- /python_package/src/stewbeet/core/database_helper/text.py: -------------------------------------------------------------------------------- 1 | 2 | # Imports 3 | from typing import Any 4 | 5 | from stouputils.print import warning 6 | 7 | 8 | # Functions 9 | def create_gradient_text(text: str, start_hex: str = "c24a17", end_hex: str = "c77e36", text_length: int | None = None) -> list[dict[str, Any]]: 10 | """ Create a gradient text effect by interpolating colors between start and end hex. 11 | 12 | Args: 13 | text (str): The text to apply the gradient to. 14 | start_hex (str): Starting color in hex format (e.g. 'c24a17'). 15 | end_hex (str): Ending color in hex format (e.g. 'c77e36'). 16 | text_length (int | None): Optional length override for the text. If provided, uses this instead of len(text). 17 | 18 | Returns: 19 | list[dict[str, Any]]: List of text components, each with a letter and its color. 20 | """ 21 | # Convert hex to RGB 22 | start_r: int = int(start_hex[0:2], 16) 23 | start_g: int = int(start_hex[2:4], 16) 24 | start_b: int = int(start_hex[4:6], 16) 25 | 26 | end_r: int = int(end_hex[0:2], 16) 27 | end_g: int = int(end_hex[2:4], 16) 28 | end_b: int = int(end_hex[4:6], 16) 29 | 30 | result: list[dict[str, Any]] = [] 31 | len_text: int = text_length if text_length is not None else len(text) 32 | 33 | # For each letter, calculate its color 34 | for i, char in enumerate(text): 35 | # Calculate progress (0 to 1) 36 | progress: float = i / (len_text - 1) if len_text > 1 else 0 37 | 38 | # Interpolate each color component 39 | r: int = int(start_r + (end_r - start_r) * progress) 40 | g: int = int(start_g + (end_g - start_g) * progress) 41 | b: int = int(start_b + (end_b - start_b) * progress) 42 | 43 | # Convert to hex 44 | color: str = f"{r:02x}{g:02x}{b:02x}" 45 | 46 | # Add text component 47 | result.append({"text": char, "color": f"#{color}"}) 48 | if i == 0: 49 | result[-1]["italic"] = False 50 | 51 | return result 52 | 53 | 54 | def gradient_text_to_string(gradient_text: list[dict[str, Any]], color_pos: int = 0) -> dict[str, str]: 55 | """ Convert a gradient text back to a string, optionally getting the color at a specific position. 56 | 57 | Args: 58 | gradient_text (list[dict[str, Any]]): The gradient text to convert back to a string. 59 | color_pos (int): The position to get the color from. 60 | 61 | Returns: 62 | dict[str, str]: A dictionary containing the concatenated text and its color at the specified position. 63 | """ 64 | # Concatenate all text components into a single string 65 | text: str = "".join(item["text"] for item in gradient_text) 66 | 67 | # Check if the requested color position is valid 68 | if -len(gradient_text) <= color_pos < len(gradient_text): 69 | return {"text": text, "color": gradient_text[color_pos]["color"]} 70 | 71 | # If position is invalid, warn and use first color 72 | warning(f"Color position {color_pos} is out of range for gradient text of length {len(gradient_text)}. Using first color instead.") 73 | return {"text": text, "color": gradient_text[0]["color"]} 74 | -------------------------------------------------------------------------------- /python_package/src/stewbeet/core/utils/io.py: -------------------------------------------------------------------------------- 1 | 2 | # Imports 3 | from beet import Function 4 | 5 | from ..__memory__ import Mem 6 | 7 | 8 | # Functions 9 | def read_function(path: str) -> str: 10 | """ Read the content of a function at the given path. 11 | 12 | Args: 13 | path (str): The path to the function (ex: "namespace:folder/function_name") 14 | 15 | Returns: 16 | str: The content of the function 17 | """ 18 | f: Function = Mem.ctx.data.functions[path] 19 | return f.to_str(f.lines) 20 | 21 | 22 | def write_function(path: str, content: str, overwrite: bool = False, prepend: bool = False) -> None: 23 | """ Write the content to the function at the given path. 24 | 25 | Args: 26 | path (str): The path to the function (ex: "namespace:folder/function_name") 27 | content (str): The content to write 28 | overwrite (bool): If the file should be overwritten (default: Append the content) 29 | prepend (bool): If the content should be prepended instead of appended (not used if overwrite is True) 30 | """ 31 | if overwrite: 32 | Mem.ctx.data.functions[path] = Function(content) 33 | else: 34 | if prepend: 35 | Mem.ctx.data.functions.setdefault(path).prepend(content) 36 | else: 37 | Mem.ctx.data.functions.setdefault(path).append(content) 38 | 39 | 40 | def write_load_file(content: str, overwrite: bool = False, prepend: bool = False) -> None: 41 | """ Write the content to the load file 42 | 43 | Args: 44 | content (str): The content to write 45 | overwrite (bool): If the file should be overwritten (default: Append the content) 46 | prepend (bool): If the content should be prepended instead of appended (not used if overwrite is True) 47 | """ 48 | write_function(f"{Mem.ctx.project_id}:v{Mem.ctx.project_version}/load/confirm_load", content, overwrite, prepend) 49 | 50 | 51 | def write_tick_file(content: str, overwrite: bool = False, prepend: bool = False) -> None: 52 | """ Write the content to the tick file 53 | 54 | Args: 55 | content (str): The content to write 56 | overwrite (bool): If the file should be overwritten (default: Append the content) 57 | prepend (bool): If the content should be prepended instead of appended (not used if overwrite is True) 58 | """ 59 | write_function(f"{Mem.ctx.project_id}:v{Mem.ctx.project_version}/tick", content, overwrite, prepend) 60 | 61 | 62 | def write_versioned_function(path: str, content: str, overwrite: bool = False, prepend: bool = False) -> None: 63 | """ Write the content to a versioned function at the given path. 64 | 65 | Args: 66 | path (str): The path to the function (ex: "folder/function_name") 67 | content (str): The content to write 68 | overwrite (bool): If the file should be overwritten (default: Append the content) 69 | prepend (bool): If the content should be prepended instead of appended (not used if overwrite is True) 70 | """ 71 | write_function(f"{Mem.ctx.project_id}:v{Mem.ctx.project_version}/{path}", content, overwrite, prepend) 72 | 73 | -------------------------------------------------------------------------------- /python_package/src/stewbeet/dependencies/bookshelf.py: -------------------------------------------------------------------------------- 1 | 2 | # This file is auto-generated by the release script (src/stewbeet/dependencies/bookshelf.py) 3 | BOOKSHELF_MODULES = { 4 | "bs.bitwise": {"version": [3,0,2],"name": "Bookshelf Bitwise","url": "https://github.com/mcbookshelf/bookshelf/releases","is_used": False}, 5 | "bs.block": {"version": [3,0,2],"name": "Bookshelf Block","url": "https://github.com/mcbookshelf/bookshelf/releases","is_used": False}, 6 | "bs.color": {"version": [3,0,2],"name": "Bookshelf Color","url": "https://github.com/mcbookshelf/bookshelf/releases","is_used": False}, 7 | "bs.dump": {"version": [3,0,2],"name": "Bookshelf Dump","url": "https://github.com/mcbookshelf/bookshelf/releases","is_used": False}, 8 | "bs.environment": {"version": [3,0,2],"name": "Bookshelf Environment","url": "https://github.com/mcbookshelf/bookshelf/releases","is_used": False}, 9 | "bs.generation": {"version": [3,0,2],"name": "Bookshelf Generation","url": "https://github.com/mcbookshelf/bookshelf/releases","is_used": False}, 10 | "bs.health": {"version": [3,0,2],"name": "Bookshelf Health","url": "https://github.com/mcbookshelf/bookshelf/releases","is_used": False}, 11 | "bs.hitbox": {"version": [3,0,2],"name": "Bookshelf Hitbox","url": "https://github.com/mcbookshelf/bookshelf/releases","is_used": False}, 12 | "bs.id": {"version": [3,0,2],"name": "Bookshelf Id","url": "https://github.com/mcbookshelf/bookshelf/releases","is_used": False}, 13 | "bs.interaction": {"version": [3,0,2],"name": "Bookshelf Interaction","url": "https://github.com/mcbookshelf/bookshelf/releases","is_used": False}, 14 | "bs.link": {"version": [3,0,2],"name": "Bookshelf Link","url": "https://github.com/mcbookshelf/bookshelf/releases","is_used": False}, 15 | "bs.log": {"version": [3,0,2],"name": "Bookshelf Log","url": "https://github.com/mcbookshelf/bookshelf/releases","is_used": False}, 16 | "bs.math": {"version": [3,0,2],"name": "Bookshelf Math","url": "https://github.com/mcbookshelf/bookshelf/releases","is_used": False}, 17 | "bs.move": {"version": [3,0,2],"name": "Bookshelf Move","url": "https://github.com/mcbookshelf/bookshelf/releases","is_used": False}, 18 | "bs.position": {"version": [3,0,2],"name": "Bookshelf Position","url": "https://github.com/mcbookshelf/bookshelf/releases","is_used": False}, 19 | "bs.random": {"version": [3,0,2],"name": "Bookshelf Random","url": "https://github.com/mcbookshelf/bookshelf/releases","is_used": False}, 20 | "bs.raycast": {"version": [3,0,2],"name": "Bookshelf Raycast","url": "https://github.com/mcbookshelf/bookshelf/releases","is_used": False}, 21 | "bs.schedule": {"version": [3,0,2],"name": "Bookshelf Schedule","url": "https://github.com/mcbookshelf/bookshelf/releases","is_used": False}, 22 | "bs.sidebar": {"version": [3,0,2],"name": "Bookshelf Sidebar","url": "https://github.com/mcbookshelf/bookshelf/releases","is_used": False}, 23 | "bs.spline": {"version": [3,0,2],"name": "Bookshelf Spline","url": "https://github.com/mcbookshelf/bookshelf/releases","is_used": False}, 24 | "bs.string": {"version": [3,0,2],"name": "Bookshelf String","url": "https://github.com/mcbookshelf/bookshelf/releases","is_used": False}, 25 | "bs.time": {"version": [3,0,2],"name": "Bookshelf Time","url": "https://github.com/mcbookshelf/bookshelf/releases","is_used": False}, 26 | "bs.tree": {"version": [3,0,2],"name": "Bookshelf Tree","url": "https://github.com/mcbookshelf/bookshelf/releases","is_used": False}, 27 | "bs.vector": {"version": [3,0,2],"name": "Bookshelf Vector","url": "https://github.com/mcbookshelf/bookshelf/releases","is_used": False}, 28 | "bs.view": {"version": [3,0,2],"name": "Bookshelf View","url": "https://github.com/mcbookshelf/bookshelf/releases","is_used": False}, 29 | "bs.xp": {"version": [3,0,2],"name": "Bookshelf Xp","url": "https://github.com/mcbookshelf/bookshelf/releases","is_used": False} 30 | } 31 | 32 | -------------------------------------------------------------------------------- /python_package/src/stewbeet/dependencies/bookshelf_config.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "v3.0.2" 3 | } 4 | -------------------------------------------------------------------------------- /python_package/src/stewbeet/dependencies/datapack/Bookshelf Bitwise.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stoupy51/PythonDatapackTemplate/701c87f48c817e69aa45ff247f33eeac2841cff2/python_package/src/stewbeet/dependencies/datapack/Bookshelf Bitwise.zip -------------------------------------------------------------------------------- /python_package/src/stewbeet/dependencies/datapack/Bookshelf Block.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stoupy51/PythonDatapackTemplate/701c87f48c817e69aa45ff247f33eeac2841cff2/python_package/src/stewbeet/dependencies/datapack/Bookshelf Block.zip -------------------------------------------------------------------------------- /python_package/src/stewbeet/dependencies/datapack/Bookshelf Color.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stoupy51/PythonDatapackTemplate/701c87f48c817e69aa45ff247f33eeac2841cff2/python_package/src/stewbeet/dependencies/datapack/Bookshelf Color.zip -------------------------------------------------------------------------------- /python_package/src/stewbeet/dependencies/datapack/Bookshelf Dump.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stoupy51/PythonDatapackTemplate/701c87f48c817e69aa45ff247f33eeac2841cff2/python_package/src/stewbeet/dependencies/datapack/Bookshelf Dump.zip -------------------------------------------------------------------------------- /python_package/src/stewbeet/dependencies/datapack/Bookshelf Environment.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stoupy51/PythonDatapackTemplate/701c87f48c817e69aa45ff247f33eeac2841cff2/python_package/src/stewbeet/dependencies/datapack/Bookshelf Environment.zip -------------------------------------------------------------------------------- /python_package/src/stewbeet/dependencies/datapack/Bookshelf Generation.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stoupy51/PythonDatapackTemplate/701c87f48c817e69aa45ff247f33eeac2841cff2/python_package/src/stewbeet/dependencies/datapack/Bookshelf Generation.zip -------------------------------------------------------------------------------- /python_package/src/stewbeet/dependencies/datapack/Bookshelf Health.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stoupy51/PythonDatapackTemplate/701c87f48c817e69aa45ff247f33eeac2841cff2/python_package/src/stewbeet/dependencies/datapack/Bookshelf Health.zip -------------------------------------------------------------------------------- /python_package/src/stewbeet/dependencies/datapack/Bookshelf Hitbox.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stoupy51/PythonDatapackTemplate/701c87f48c817e69aa45ff247f33eeac2841cff2/python_package/src/stewbeet/dependencies/datapack/Bookshelf Hitbox.zip -------------------------------------------------------------------------------- /python_package/src/stewbeet/dependencies/datapack/Bookshelf Id.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stoupy51/PythonDatapackTemplate/701c87f48c817e69aa45ff247f33eeac2841cff2/python_package/src/stewbeet/dependencies/datapack/Bookshelf Id.zip -------------------------------------------------------------------------------- /python_package/src/stewbeet/dependencies/datapack/Bookshelf Interaction.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stoupy51/PythonDatapackTemplate/701c87f48c817e69aa45ff247f33eeac2841cff2/python_package/src/stewbeet/dependencies/datapack/Bookshelf Interaction.zip -------------------------------------------------------------------------------- /python_package/src/stewbeet/dependencies/datapack/Bookshelf Link.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stoupy51/PythonDatapackTemplate/701c87f48c817e69aa45ff247f33eeac2841cff2/python_package/src/stewbeet/dependencies/datapack/Bookshelf Link.zip -------------------------------------------------------------------------------- /python_package/src/stewbeet/dependencies/datapack/Bookshelf Log.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stoupy51/PythonDatapackTemplate/701c87f48c817e69aa45ff247f33eeac2841cff2/python_package/src/stewbeet/dependencies/datapack/Bookshelf Log.zip -------------------------------------------------------------------------------- /python_package/src/stewbeet/dependencies/datapack/Bookshelf Math.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stoupy51/PythonDatapackTemplate/701c87f48c817e69aa45ff247f33eeac2841cff2/python_package/src/stewbeet/dependencies/datapack/Bookshelf Math.zip -------------------------------------------------------------------------------- /python_package/src/stewbeet/dependencies/datapack/Bookshelf Move.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stoupy51/PythonDatapackTemplate/701c87f48c817e69aa45ff247f33eeac2841cff2/python_package/src/stewbeet/dependencies/datapack/Bookshelf Move.zip -------------------------------------------------------------------------------- /python_package/src/stewbeet/dependencies/datapack/Bookshelf Position.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stoupy51/PythonDatapackTemplate/701c87f48c817e69aa45ff247f33eeac2841cff2/python_package/src/stewbeet/dependencies/datapack/Bookshelf Position.zip -------------------------------------------------------------------------------- /python_package/src/stewbeet/dependencies/datapack/Bookshelf Random.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stoupy51/PythonDatapackTemplate/701c87f48c817e69aa45ff247f33eeac2841cff2/python_package/src/stewbeet/dependencies/datapack/Bookshelf Random.zip -------------------------------------------------------------------------------- /python_package/src/stewbeet/dependencies/datapack/Bookshelf Raycast.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stoupy51/PythonDatapackTemplate/701c87f48c817e69aa45ff247f33eeac2841cff2/python_package/src/stewbeet/dependencies/datapack/Bookshelf Raycast.zip -------------------------------------------------------------------------------- /python_package/src/stewbeet/dependencies/datapack/Bookshelf Schedule.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stoupy51/PythonDatapackTemplate/701c87f48c817e69aa45ff247f33eeac2841cff2/python_package/src/stewbeet/dependencies/datapack/Bookshelf Schedule.zip -------------------------------------------------------------------------------- /python_package/src/stewbeet/dependencies/datapack/Bookshelf Sidebar.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stoupy51/PythonDatapackTemplate/701c87f48c817e69aa45ff247f33eeac2841cff2/python_package/src/stewbeet/dependencies/datapack/Bookshelf Sidebar.zip -------------------------------------------------------------------------------- /python_package/src/stewbeet/dependencies/datapack/Bookshelf Spline.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stoupy51/PythonDatapackTemplate/701c87f48c817e69aa45ff247f33eeac2841cff2/python_package/src/stewbeet/dependencies/datapack/Bookshelf Spline.zip -------------------------------------------------------------------------------- /python_package/src/stewbeet/dependencies/datapack/Bookshelf String.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stoupy51/PythonDatapackTemplate/701c87f48c817e69aa45ff247f33eeac2841cff2/python_package/src/stewbeet/dependencies/datapack/Bookshelf String.zip -------------------------------------------------------------------------------- /python_package/src/stewbeet/dependencies/datapack/Bookshelf Time.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stoupy51/PythonDatapackTemplate/701c87f48c817e69aa45ff247f33eeac2841cff2/python_package/src/stewbeet/dependencies/datapack/Bookshelf Time.zip -------------------------------------------------------------------------------- /python_package/src/stewbeet/dependencies/datapack/Bookshelf Tree.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stoupy51/PythonDatapackTemplate/701c87f48c817e69aa45ff247f33eeac2841cff2/python_package/src/stewbeet/dependencies/datapack/Bookshelf Tree.zip -------------------------------------------------------------------------------- /python_package/src/stewbeet/dependencies/datapack/Bookshelf Vector.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stoupy51/PythonDatapackTemplate/701c87f48c817e69aa45ff247f33eeac2841cff2/python_package/src/stewbeet/dependencies/datapack/Bookshelf Vector.zip -------------------------------------------------------------------------------- /python_package/src/stewbeet/dependencies/datapack/Bookshelf View.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stoupy51/PythonDatapackTemplate/701c87f48c817e69aa45ff247f33eeac2841cff2/python_package/src/stewbeet/dependencies/datapack/Bookshelf View.zip -------------------------------------------------------------------------------- /python_package/src/stewbeet/dependencies/datapack/Bookshelf Xp.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stoupy51/PythonDatapackTemplate/701c87f48c817e69aa45ff247f33eeac2841cff2/python_package/src/stewbeet/dependencies/datapack/Bookshelf Xp.zip -------------------------------------------------------------------------------- /python_package/src/stewbeet/dependencies/datapack/Common Signals.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stoupy51/PythonDatapackTemplate/701c87f48c817e69aa45ff247f33eeac2841cff2/python_package/src/stewbeet/dependencies/datapack/Common Signals.zip -------------------------------------------------------------------------------- /python_package/src/stewbeet/dependencies/datapack/Furnace NBT Recipes.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stoupy51/PythonDatapackTemplate/701c87f48c817e69aa45ff247f33eeac2841cff2/python_package/src/stewbeet/dependencies/datapack/Furnace NBT Recipes.zip -------------------------------------------------------------------------------- /python_package/src/stewbeet/dependencies/datapack/ItemIO.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stoupy51/PythonDatapackTemplate/701c87f48c817e69aa45ff247f33eeac2841cff2/python_package/src/stewbeet/dependencies/datapack/ItemIO.zip -------------------------------------------------------------------------------- /python_package/src/stewbeet/dependencies/datapack/SmartOreGeneration.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stoupy51/PythonDatapackTemplate/701c87f48c817e69aa45ff247f33eeac2841cff2/python_package/src/stewbeet/dependencies/datapack/SmartOreGeneration.zip -------------------------------------------------------------------------------- /python_package/src/stewbeet/dependencies/datapack/Smithed Crafter.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stoupy51/PythonDatapackTemplate/701c87f48c817e69aa45ff247f33eeac2841cff2/python_package/src/stewbeet/dependencies/datapack/Smithed Crafter.zip -------------------------------------------------------------------------------- /python_package/src/stewbeet/dependencies/datapack/Smithed Custom Block.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stoupy51/PythonDatapackTemplate/701c87f48c817e69aa45ff247f33eeac2841cff2/python_package/src/stewbeet/dependencies/datapack/Smithed Custom Block.zip -------------------------------------------------------------------------------- /python_package/src/stewbeet/dependencies/resource_pack/Smithed Crafter.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stoupy51/PythonDatapackTemplate/701c87f48c817e69aa45ff247f33eeac2841cff2/python_package/src/stewbeet/dependencies/resource_pack/Smithed Crafter.zip -------------------------------------------------------------------------------- /python_package/src/stewbeet/plugins/archive/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | # Imports 3 | from beet import Context 4 | 5 | 6 | # Main entry point 7 | def beet_default(ctx: Context): 8 | pass 9 | 10 | -------------------------------------------------------------------------------- /python_package/src/stewbeet/plugins/archive/make_archive.py: -------------------------------------------------------------------------------- 1 | 2 | # Imports 3 | import os 4 | import shutil 5 | import time 6 | from zipfile import ZIP_DEFLATED, ZipFile, ZipInfo 7 | 8 | import stouputils as stp 9 | 10 | from .io import FILES_TO_WRITE 11 | 12 | 13 | # Function that makes an archive with consistency (same zip file each time) 14 | def make_archive(source: str, destination: str, copy_destinations: list[str] | None = None) -> float: 15 | """ Make an archive with consistency. 16 | Creates a zip archive from a source directory, ensuring consistent file timestamps and contents. 17 | Uses FILES_TO_WRITE to track known files and maintain consistency between builds. 18 | 19 | Args: 20 | source (str): Source directory to archive 21 | destination (str): Path where the zip file will be created 22 | copy_destinations (list[str] | None): Optional list of additional paths to copy the archive to 23 | Returns: 24 | float: Time taken to create the archive in seconds 25 | """ 26 | if copy_destinations is None: 27 | copy_destinations = [] 28 | start_time: float = time.perf_counter() 29 | 30 | # Fix copy_destinations type if needed 31 | if copy_destinations and isinstance(copy_destinations, str): 32 | copy_destinations = [copy_destinations] 33 | 34 | # Get all files that are not in FILES_TO_WRITE 35 | not_known_files: list[str] = [] 36 | for root, _, files in os.walk(source): 37 | for file in files: 38 | file_path: str = stp.clean_path(os.path.join(root, file)) 39 | if file_path not in FILES_TO_WRITE: 40 | not_known_files.append(file_path) 41 | 42 | # Get the constant time for the archive 43 | constant_time: tuple[int, ...] = (2024, 1, 1, 0, 0, 0) # default time: 2024-01-01 00:00:00 44 | for file in FILES_TO_WRITE: 45 | if file.endswith("pack.mcmeta"): 46 | 47 | # Get the pack folder (and the data and assets folders paths) 48 | pack_folder: str = os.path.dirname(file) 49 | data_folder: str = f"{pack_folder}/data" 50 | assets_folder: str = f"{pack_folder}/assets" 51 | 52 | # Get the time of the data or assets folder if it exists, else get the time of the pack.mcmeta file 53 | if os.path.exists(data_folder): 54 | time_float: float = os.path.getmtime(data_folder) 55 | elif os.path.exists(assets_folder): 56 | time_float: float = os.path.getmtime(assets_folder) 57 | else: 58 | time_float: float = os.path.getmtime(file) 59 | constant_time = time.localtime(time_float)[:6] 60 | break 61 | 62 | # Create the archive 63 | destination = destination if ".zip" in destination else destination + ".zip" 64 | 65 | def process_file(file: str, is_known: bool) -> tuple[ZipInfo, bytes] | None: 66 | """ Process a single file for the archive. 67 | 68 | Args: 69 | file (str): Path to the file to process. 70 | is_known (bool): Whether the file is in FILES_TO_WRITE. 71 | 72 | Returns: 73 | tuple[ZipInfo, bytes] | None: Tuple containing the ZipInfo and file contents, or None if file should be skipped. 74 | """ 75 | if source not in file: 76 | return None 77 | 78 | base_path: str = file.replace(source, "").strip("/") 79 | info: ZipInfo = ZipInfo(base_path) 80 | info.compress_type = ZIP_DEFLATED 81 | info.date_time = constant_time 82 | 83 | if is_known: 84 | file_content = FILES_TO_WRITE[file] 85 | content: bytes = file_content.encode() if isinstance(file_content, str) else file_content 86 | else: 87 | with open(file, "rb") as f: 88 | content: bytes = f.read() 89 | 90 | return info, content 91 | 92 | # Prepare file list for processing 93 | file_list: list[tuple[str, bool]] = [(f, False) for f in not_known_files] + [(f, True) for f in FILES_TO_WRITE.keys()] 94 | 95 | # Process files in parallel 96 | results: list[tuple[ZipInfo, bytes] | None] = stp.multithreading(process_file, sorted(file_list), use_starmap=True, max_workers=min(32, len(file_list))) 97 | 98 | for retry in range(10): 99 | try: 100 | # Write results directly to zip file 101 | with ZipFile(destination, "w", compression=ZIP_DEFLATED, compresslevel=6) as zip: 102 | for result in results: 103 | if result is not None: 104 | info, content = result 105 | zip.writestr(info, content) 106 | 107 | # Copy the archive to the destination(s) 108 | for dest_folder in copy_destinations: 109 | try: 110 | dest_folder = stp.clean_path(dest_folder) 111 | if dest_folder.endswith("/"): 112 | file_name = destination.split("/")[-1] 113 | shutil.copy(stp.clean_path(destination), f"{dest_folder}/{file_name}") 114 | 115 | # Else, it's not a folder but a file path 116 | else: 117 | shutil.copy(stp.clean_path(destination), dest_folder) 118 | except Exception as e: 119 | stp.warning(f"Unable to copy '{stp.clean_path(destination)}' to '{dest_folder}', reason: {e}") 120 | 121 | # Return the time taken to archive the source folder 122 | return time.perf_counter() - start_time 123 | 124 | # If OSError, means another program tried to read the zip file. 125 | # Therefore, try 10 times before stopping and send warning 126 | except OSError: 127 | stp.warning(f"Unable to archive '{source}' due to file being locked by another process. Retry {retry+1}/10...") 128 | time.sleep(1.0) # Wait a bit before retrying 129 | 130 | # Final error message 131 | stp.error(f"Failed to archive '{source}' after 10 attempts due to file being locked by another process") 132 | return 0.0 133 | 134 | -------------------------------------------------------------------------------- /python_package/src/stewbeet/plugins/auto/headers/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | # Imports 3 | from beet import Context 4 | from stouputils.decorators import measure_time 5 | 6 | from ....core.utils.io import read_function, write_function 7 | from .object import Header 8 | 9 | 10 | # Main entry point 11 | @measure_time(message="Executing") 12 | def beet_default(ctx: Context): 13 | """ Main entry point for the lang file plugin. 14 | 15 | Args: 16 | ctx (Context): The beet context. 17 | """ 18 | # Get all mcfunctions paths 19 | mcfunctions: dict[str, Header] = {} 20 | for path in ctx.data.functions: 21 | # Create a Header object from the function content 22 | content: str = read_function(path) 23 | mcfunctions[path] = Header.from_content(path, content) 24 | 25 | 26 | # For each function tag, get the functions that it calls 27 | for tag_path, tag in ctx.data.function_tags.items(): 28 | # Get string that is used for calling the function (ex: "#namespace:my_function") 29 | to_be_called: str = f"#{tag_path}" 30 | 31 | # Loop through the functions in the tag 32 | for function_path in tag.data["values"]: 33 | if isinstance(function_path, str): 34 | if function_path in mcfunctions: 35 | mcfunctions[function_path].within.append(to_be_called) 36 | elif isinstance(function_path, dict): 37 | function_path: str = function_path.get("id", "") 38 | if function_path in mcfunctions: 39 | mcfunctions[function_path].within.append(to_be_called) 40 | 41 | 42 | # For each advancement, get the functions that it calls 43 | for adv_path, adv in ctx.data.advancements.items(): 44 | # Get string that is used for calling the function (ex: "advancement namespace:my_function") 45 | to_be_called: str = f"advancement {adv_path}" 46 | 47 | # Check if the advancement has a function reward 48 | if adv.data.get("rewards", {}).get("function"): 49 | function_path: str = adv.data["rewards"]["function"] 50 | if function_path in mcfunctions: 51 | mcfunctions[function_path].within.append(to_be_called) 52 | 53 | 54 | # For each mcfunction file, look at each line 55 | for path, header in mcfunctions.items(): 56 | for line in header.content.split("\n"): 57 | 58 | # If the line calls a function 59 | if "function " in line: 60 | # Get the called function 61 | splitted: list[str] = line.split("function ", 1)[1].replace("\n", "").split(" ") 62 | calling: str = splitted[0].replace('"', '').replace("'", "") 63 | 64 | # Get additional text like macros, ex: function iyc:function {id:"51"} 65 | more: str = "" 66 | if len(splitted) > 1: 67 | more = " " + " ".join(splitted[1:]) # Add Macros or schedule time 68 | 69 | # If the called function is registered, append the name of this file as well as the additional text 70 | if calling in mcfunctions and (path + more) not in mcfunctions[calling].within: 71 | mcfunctions[calling].within.append(path + more) 72 | 73 | 74 | # For each mcfunction file, write the header 75 | for path, header in mcfunctions.items(): 76 | write_function(path, header.to_str(), overwrite=True) 77 | 78 | -------------------------------------------------------------------------------- /python_package/src/stewbeet/plugins/auto/headers/object.py: -------------------------------------------------------------------------------- 1 | # Imports 2 | from __future__ import annotations 3 | 4 | 5 | # Header class 6 | class Header: 7 | """ A class representing a function header. 8 | 9 | Attributes: 10 | path (str): The path to the function (ex: "namespace:folder/function_name") 11 | within (list[str]): List of functions that call this function 12 | other (list[str]): List of other information about the function 13 | content (str): The content of the function 14 | 15 | Examples: 16 | >>> header = Header("test:function", ["other:function"], ["Some info"], "say Hello") 17 | >>> header.path 18 | 'test:function' 19 | >>> header.within 20 | ['other:function'] 21 | >>> header.other 22 | ['Some info'] 23 | >>> header.content 24 | 'say Hello' 25 | """ 26 | def __init__(self, path: str, within: list[str] | None = None, other: list[str] | None = None, content: str = ""): 27 | self.path = path 28 | self.within = within or [] 29 | self.other = other or [] 30 | self.content = content 31 | 32 | @classmethod 33 | def from_content(cls, path: str, content: str) -> Header: 34 | """ Create a Header object from a function's content. 35 | 36 | Args: 37 | path (str): The path to the function 38 | content (str): The content of the function 39 | 40 | Returns: 41 | Header: A new Header object 42 | 43 | Examples: 44 | >>> content = ''' 45 | ... #> test:function 46 | ... # 47 | ... # @within other:function 48 | ... # Some info 49 | ... # 50 | ... say Hello''' 51 | >>> header = Header.from_content("test:function", content) 52 | >>> header.path 53 | 'test:function' 54 | >>> header.within 55 | ['other:function'] 56 | >>> header.other 57 | ['Some info'] 58 | >>> header.content 59 | 'say Hello' 60 | """ 61 | # Initialize empty lists 62 | within: list[str] = [] 63 | other: list[str] = [] 64 | actual_content: str = content.strip() 65 | 66 | # If the content has a header, parse it 67 | if content.strip().startswith("#> "): 68 | # Split the content into lines 69 | lines: list[str] = content.strip().split("\n") 70 | 71 | # Skip the first line (#> path) and the second line (#) 72 | i: int = 2 73 | 74 | # Parse within section 75 | while i < len(lines) and lines[i].strip().startswith("# @within"): 76 | within_line: str = lines[i].strip() 77 | if within_line != "# @within": 78 | # Extract the function name after @within 79 | func_name: str = within_line.split("@within")[1].strip() 80 | within.append(func_name) 81 | i += 1 82 | 83 | # Skip empty lines 84 | while i < len(lines) and lines[i].strip() == "#": 85 | i += 1 86 | 87 | # Parse other information 88 | while i < len(lines) and lines[i].strip().startswith("# "): 89 | other_line: str = lines[i].strip() 90 | if other_line != "#": 91 | # Remove the # prefix and add to other 92 | other.append(other_line[2:].strip()) 93 | i += 1 94 | 95 | # Skip empty lines 96 | while i < len(lines) and lines[i].strip() == "#": 97 | i += 1 98 | 99 | # The remaining lines are the actual content 100 | actual_content = "\n".join(lines[i:]).strip() 101 | 102 | return cls(path, within, other, actual_content) 103 | 104 | def to_str(self) -> str: 105 | """ Convert the Header object to a string. 106 | 107 | Returns: 108 | str: The function content with the header 109 | 110 | Examples: 111 | >>> content = ''' 112 | ... #> test:function 113 | ... # 114 | ... # @within\\tother:function 115 | ... # 116 | ... # Some info 117 | ... # 118 | ... 119 | ... say Hello\\n\\n''' 120 | >>> header = Header("test:function", ["other:function"], ["Some info"], "say Hello") 121 | >>> content.strip() == header.to_str().strip() 122 | True 123 | >>> content_lines = content.splitlines() 124 | >>> header_lines = header.to_str().splitlines() 125 | >>> for i, (c, h) in enumerate(zip(content_lines, header_lines)): 126 | ... if c != h: 127 | ... print(f"Difference at line {i}:") 128 | ... print(f"Content: {c}") 129 | ... print(f"Header: {h}") 130 | ... break 131 | """ 132 | # Start with the path 133 | header = f"\n#> {self.path}\n#\n" 134 | 135 | # Add the within list 136 | if self.within: 137 | header += "# @within\t" + "\n#\t\t\t".join(self.within) + "\n#\n" 138 | else: 139 | header += "# @within\t???\n#\n" 140 | 141 | # Add other information 142 | for line in self.other: 143 | header += f"# {line}\n" 144 | 145 | # Add final empty line and content 146 | if not header.endswith("#\n"): 147 | header += "#\n" 148 | return (header + "\n" + self.content.strip() + "\n\n").replace("\n\n\n", "\n\n") 149 | 150 | -------------------------------------------------------------------------------- /python_package/src/stewbeet/plugins/auto/lang_file/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | # Imports 3 | from beet import Context 4 | 5 | 6 | # Main entry point 7 | def beet_default(ctx: Context): 8 | pass 9 | 10 | -------------------------------------------------------------------------------- /python_package/src/stewbeet/plugins/compatibilities/neo_enchant/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | # Imports 3 | from beet import Context 4 | 5 | 6 | # Main entry point 7 | def beet_default(ctx: Context): 8 | pass 9 | 10 | -------------------------------------------------------------------------------- /python_package/src/stewbeet/plugins/compatibilities/simpledrawer/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | # Imports 3 | from beet import Context 4 | 5 | 6 | # Main entry point 7 | def beet_default(ctx: Context): 8 | pass 9 | 10 | -------------------------------------------------------------------------------- /python_package/src/stewbeet/plugins/compute_sha1/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | # Imports 3 | from beet import Context 4 | 5 | 6 | # Main entry point 7 | def beet_default(ctx: Context): 8 | pass 9 | 10 | -------------------------------------------------------------------------------- /python_package/src/stewbeet/plugins/copy_to_destination/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | # Imports 3 | from beet import Context 4 | 5 | 6 | # Main entry point 7 | def beet_default(ctx: Context): 8 | pass 9 | 10 | -------------------------------------------------------------------------------- /python_package/src/stewbeet/plugins/custom_recipes/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | # Imports 3 | from beet import Context 4 | 5 | 6 | # Main entry point 7 | def beet_default(ctx: Context): 8 | pass 9 | 10 | -------------------------------------------------------------------------------- /python_package/src/stewbeet/plugins/datapack/custom_blocks/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | # Imports 3 | from beet import Context 4 | 5 | 6 | # Main entry point 7 | def beet_default(ctx: Context): 8 | pass 9 | 10 | -------------------------------------------------------------------------------- /python_package/src/stewbeet/plugins/datapack/loading/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | # Imports 3 | from beet import Context 4 | 5 | 6 | # Main entry point 7 | def beet_default(ctx: Context): 8 | pass 9 | 10 | -------------------------------------------------------------------------------- /python_package/src/stewbeet/plugins/datapack/loot_tables/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | # Imports 3 | from beet import Context 4 | 5 | 6 | # Main entry point 7 | def beet_default(ctx: Context): 8 | pass 9 | 10 | -------------------------------------------------------------------------------- /python_package/src/stewbeet/plugins/finalyze/basic_datapack_structure/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | # Imports 3 | from beet import Context 4 | 5 | 6 | # Main entry point 7 | def beet_default(ctx: Context): 8 | pass 9 | 10 | -------------------------------------------------------------------------------- /python_package/src/stewbeet/plugins/finalyze/check_unused_textures/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | # Imports 3 | from beet import Context 4 | 5 | 6 | # Main entry point 7 | def beet_default(ctx: Context): 8 | pass 9 | 10 | -------------------------------------------------------------------------------- /python_package/src/stewbeet/plugins/finalyze/custom_blocks_ticking/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | # Imports 3 | from beet import Context 4 | 5 | 6 | # Main entry point 7 | def beet_default(ctx: Context): 8 | pass 9 | 10 | -------------------------------------------------------------------------------- /python_package/src/stewbeet/plugins/finalyze/dependencies/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | # Imports 3 | from beet import Context 4 | 5 | 6 | # Main entry point 7 | def beet_default(ctx: Context): 8 | pass 9 | 10 | -------------------------------------------------------------------------------- /python_package/src/stewbeet/plugins/ingame_manual/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | # Imports 3 | from beet import Context 4 | 5 | 6 | # Main entry point 7 | def beet_default(ctx: Context): 8 | pass 9 | 10 | -------------------------------------------------------------------------------- /python_package/src/stewbeet/plugins/initialize/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | # Imports 3 | from beet import Context 4 | from beet.core.utils import TextComponent 5 | from box import Box 6 | from stouputils import relative_path 7 | 8 | from ...core import Mem 9 | 10 | 11 | # Main entry point 12 | def beet_default(ctx: Context): 13 | 14 | # Store the Box object in ctx for access throughout the codebase 15 | meta_box: Box = Box(ctx.meta, default_box=True, default_box_attr={}) 16 | object.__setattr__(ctx, "meta", meta_box) # Bypass FrozenInstanceError 17 | Mem.ctx = ctx 18 | 19 | # Preprocess source lore 20 | source_lore: TextComponent = Mem.ctx.meta.stewbeet.source_lore 21 | if not source_lore or source_lore == "auto": 22 | Mem.ctx.meta.stewbeet.source_lore = [{"text":"ICON"},{"text":f" {ctx.project_name}","italic":True,"color":"blue"}] 23 | 24 | # Preprocess manual name 25 | manual_name: TextComponent = Mem.ctx.meta.stewbeet.manual.name 26 | if not manual_name: 27 | Mem.ctx.meta.stewbeet.manual.name = f"{ctx.project_name} Manual" 28 | 29 | # Convert paths to relative ones 30 | object.__setattr__(ctx, "output_directory", relative_path(Mem.ctx.output_directory)) 31 | 32 | pass 33 | 34 | -------------------------------------------------------------------------------- /python_package/src/stewbeet/plugins/merge_smithed_weld/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | # Imports 3 | from beet import Context 4 | 5 | 6 | # Main entry point 7 | def beet_default(ctx: Context): 8 | pass 9 | 10 | -------------------------------------------------------------------------------- /python_package/src/stewbeet/plugins/resource_pack/check_power_of_2/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | # Imports 3 | from beet import Context 4 | 5 | 6 | # Main entry point 7 | def beet_default(ctx: Context): 8 | pass 9 | 10 | -------------------------------------------------------------------------------- /python_package/src/stewbeet/plugins/resource_pack/item_models/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | # Imports 3 | from beet import Context 4 | 5 | 6 | # Main entry point 7 | def beet_default(ctx: Context): 8 | pass 9 | 10 | -------------------------------------------------------------------------------- /python_package/src/stewbeet/plugins/resource_pack/sounds/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | # Imports 3 | from beet import Context 4 | 5 | 6 | # Main entry point 7 | def beet_default(ctx: Context): 8 | pass 9 | 10 | -------------------------------------------------------------------------------- /python_package/upgrade.py: -------------------------------------------------------------------------------- 1 | 2 | ## Python script that modifies the pyproject.toml to go to the next version 3 | # Imports 4 | import os 5 | 6 | # Constants 7 | ROOT: str = os.path.dirname(os.path.realpath(__file__)).replace("\\", "/") 8 | PYPROJECT_PATH = f"{ROOT}/pyproject.toml" 9 | VERSION_KEY = "version = " 10 | 11 | def read_file(path: str) -> list[str]: 12 | """Read file and return lines""" 13 | with open(path, "r", encoding="utf-8") as file: 14 | return file.readlines() 15 | 16 | def find_version_line(lines: list[str]) -> int | None: 17 | """Find line containing version and return index""" 18 | for i, line in enumerate(lines): 19 | if line.startswith(VERSION_KEY): 20 | return i 21 | return None 22 | 23 | def increment_version(version: str) -> str: 24 | """Increment the last number in version string""" 25 | parts = version.split(".") 26 | parts[-1] = str(int(parts[-1]) + 1) 27 | return ".".join(parts) 28 | 29 | def extract_version(line: str) -> str: 30 | """Extract version number from line""" 31 | return line.replace(VERSION_KEY, "").strip().replace('"', "") 32 | 33 | def write_file(path: str, lines: list[str]) -> None: 34 | """Write lines to file""" 35 | with open(path, "w", encoding="utf-8") as file: 36 | file.writelines(lines) 37 | 38 | # Read the file 39 | lines = read_file(PYPROJECT_PATH) 40 | 41 | # Find and update version 42 | version_line = find_version_line(lines) 43 | current_version = None 44 | 45 | if version_line is not None: 46 | # Get and increment version 47 | current_version = extract_version(lines[version_line]) 48 | new_version = increment_version(current_version) 49 | 50 | # Main 51 | if __name__ == "__main__": 52 | 53 | # Update version line 54 | lines[version_line] = f'{VERSION_KEY}"{new_version}"\n' 55 | 56 | # Write updated file 57 | write_file(PYPROJECT_PATH, lines) 58 | 59 | --------------------------------------------------------------------------------