├── .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 | [](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 |
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 |
--------------------------------------------------------------------------------