diff --git a/.gitattributes b/.gitattributes index 63ea2676..1d8939fd 100644 --- a/.gitattributes +++ b/.gitattributes @@ -2,3 +2,4 @@ *.mtl filter=lfs diff=lfs merge=lfs -text *.png filter=lfs diff=lfs merge=lfs -text *.blend filter=lfs diff=lfs merge=lfs -text +*.zip filter=lfs diff=lfs merge=lfs -text diff --git a/.github/workflows/run_tests.yaml b/.github/workflows/run_tests.yaml new file mode 100644 index 00000000..69870d20 --- /dev/null +++ b/.github/workflows/run_tests.yaml @@ -0,0 +1,88 @@ +name: Run tests + +on: + workflow_dispatch: + inputs: + blender_versions: + description: 'Blender version(s)' + required: false + default: '["4.1.1"]' + type: choice + options: + - '["4.1.1", "2.90.1", "3.6.5", "2.90.1", "2.28.3"]' + - '["4.1.1"]' + - '["3.6.5"]' + - '["2.90.1"]' + - '["2.28.3"]' + pull_request: + types: [opened, reopened, synchronize] # drop synchronize not run per commit + branches: + - dev + - master + paths: + - 'MCprep_addon/**' + + +#run-name: Test on ${{ inputs.blender_versions || true && "4.1.1" }} +run-name: Test on ${{ inputs.blender_versions }} + +jobs: + test: + name: Test Blender + runs-on: ubuntu-latest + strategy: + matrix: + blender-version: ${{ github.event.inputs.blender_versions && fromJSON(github.event.inputs.blender_versions) || fromJSON('["4.1.1"]') }} + steps: + - uses: actions/checkout@v4 + - run: | + echo "Running with blender: $BVERSION" + echo "event name is:" ${{ github.event_name }} + echo "event type is:" ${{ github.event.action }} + env: + BVERSION: ${{ matrix.blender-version }} + - name: Set up Python v3.9 + uses: actions/setup-python@v5 + with: + python-version: 3.9 + - name: Pip installs + run: | + python -m pip install --upgrade pip + python -m pip install --upgrade blender-downloader + python -m pip install --upgrade -r requirements.txt + - name: Cache Blender ${{ matrix.blender-version }} + uses: actions/cache@v4 + id: cache-blender + with: + path: | + blender-* + blender_execs.txt + key: ${{ runner.os }}-${{ matrix.blender-version }} + - name: Download Blender ${{ matrix.blender-version }} + if: steps.cache-blender.outputs.cache-hit != 'true' + id: download-blender + run: | + printf "%s" "$(blender-downloader \ + ${{ matrix.blender-version }} --extract --remove-compressed \ + --quiet --print-blender-executable)" > blender_execs.txt + - name: Download assets + if: always() + run: | + # TODO: use git describe --tags or equivalent to automatically get latest stable. + wget -nv "https://github.com/Moo-Ack-Productions/MCprep/releases/download/3.5.3/MCprep_addon_3.5.3.zip" + git lfs pull + mkdir MCprep_stable_release + unzip -qq ./MCprep_addon_*.zip -d MCprep_stable_release + # Copy the checked-in data json to override the wget version + mv ./MCprep_addon/MCprep_resources/mcprep_data_update.json _tmp.json + mv -f ./MCprep_stable_release/MCprep_addon/MCprep_resources/** ./MCprep_addon/MCprep_resources + mv -f _tmp.json ./MCprep_addon/MCprep_resources/mcprep_data_update.json + echo "Prepare for install by creating addons folder" + mkdir -p /home/runner/.config/blender/4.1/scripts/addons + echo $(ls /home/runner/.config/blender/4.1/scripts/addons) + echo $(ls /home/runner/.config/blender/4.1/scripts/addons/MCprep_addon/MCprep_resources) + - name: Run tests + run: | + python run_tests.py + - name: Output results + run: cat test_results.csv diff --git a/.gitignore b/.gitignore index 0f6d212a..5b2e707f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,8 @@ .DS_Store *.blend +*.mo __pycache__ +.mypy_cache MCprep_addon/mcprep_addon_tracker.json MCprep_addon/mcprep_addon_updater/MCprep_addon_updater_status.json blender_execs.txt @@ -13,7 +15,8 @@ mcprep_venv_* .cache .python-version venv/ -MCprep_addon/MCprep_resources/ +.venv/ *.sublime-* MCprep_addon/import_bridge/conf MCprep_addon/import_bridge/nbt +MCprep_addon/MCprep_resources/ \ No newline at end of file diff --git a/BlenderChanges.md b/BlenderChanges.md index bb96ac75..0e86fb28 100644 --- a/BlenderChanges.md +++ b/BlenderChanges.md @@ -1,11 +1,13 @@ This list contains all deprecations and removals in every Blender version starting with Blender 3.0. Since Blender 4.0's breaking changes invoked the want for a list of all deprecations and changes, this list is public for addon developers to use. -Note that not all deprecations are listed, just the ones that may affect MCprep or changes that developers should be aware of in general, so please refer to the wiki entries for each version for more information. +Note that not all deprecations are listed, just the ones that may affect MCprep or changes that developers should be aware of in general, so please refer to the wiki entries for each version for more information. In addition, for simplicity, this page will only document changes in stable releases of Blender, unless a change has been found in a development version of Blender that needs to be addressed. _For Developers_: The use of any deprecated feature is an automatic bug. Such features should be wrapped around if statements for backwards compatibility if absolutely necesary in older versions. _For MCprep maintainers_: Any use of a deprecated feature in a pull request should be questioned. If the feature is needed in older versions, then remind developers to use `min_bv`, `bv28` ([Deprecated in MCprep 3.5](https://github.com/TheDuckCow/MCprep/pull/401)), or `bv30`, whichever is more appropriate. +In ascending order: + # [Blender 3.0](https://wiki.blender.org/wiki/Reference/Release_Notes/3.0/Python_API) ## Deprecations None that concern MCprep. @@ -93,11 +95,11 @@ None that concern MCprep. - `data` remains emulated, but with a performance penalty # [Blender 3.6](https://wiki.blender.org/wiki/Reference/Release_Notes/3.6/Python_API) -Nothing that concerns MCprep +None that concern MCprep. -# [Blender 4.0 (IN DEVELOPMENT)](https://wiki.blender.org/wiki/Reference/Release_Notes/4.0/Python_API) +# [Blender 4.0](https://wiki.blender.org/wiki/Reference/Release_Notes/4.0/Python_API) ## Deprecated -Nothing that concerned MCprep for now. +None that concern MCprep. ## Breaking Changes - Glossy BSDF and Anisotrophic BSDF nodes have been merged. @@ -113,3 +115,16 @@ Nothing that concerned MCprep for now. - Coat -> Coat Weight - Sheen -> Sheen Weight - Emission -> Emission Color +- Possible breakage of rig backwards compatibility (unconfirmed) + +# [Blender 4.1](https://wiki.blender.org/wiki/Reference/Release_Notes/4.1/Python_API) +## Deprecated +None that concern MCprep. + +## Breaking Changes/Additions +- Python has been upgraded to Python 3.11 +- VFX Reference Platform 2024 support has been added +- Layout panels to reduce boilerplate for submenus +- `displacement_method` is under `bpy.types.Material` now +- Possible breakage of rig backwards compatibility (unconfirmed) + diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 632cb332..6d4cd52a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -60,8 +60,8 @@ As a quick start: # Highly recommended, create a local virtual environment (could also define globally) python3 -m pip install --user virtualenv -python3 -m pip install --upgrade pip # Install/upgrade pip python3 -m venv ./venv # Add a local virtual env called `venv` +python3 -m pip install --upgrade pip # Install/upgrade pip # Activate that environment ## On windows: @@ -69,11 +69,15 @@ python3 -m venv ./venv # Add a local virtual env called `venv` ## On Mac/linux: source venv/bin/activate +pip install -r requirements.txt + # Now with the env active, do the pip install (or upgrade) pip install --upgrade bpy-addon-build # Finally, you can compile MCprep using: -bpy-addon-build --during-build dev # Use dev to use non-prod related resources and tracking. +bab -b dev # Use dev to use non-prod related resources and tracking. +bab -b dev translate # Dev, with translations +bab -b translate # For production ``` Moving forward, you can now build the addon for all intended supported versions using: `bpy-addon-build -b dev` @@ -214,6 +218,48 @@ Add this to a file called .gitmessage, and then execute the following command: To use for each commit, you can use `git config --local commit.verbose true` to tell Git to perform a verbose commit all the time for just the MCprep repo. + +## Signing Off Commits +Signing off of all commits, although not required, is good practice to certify the origin of a change. When you sign off of a commit, you certify that the commit was made in line with the Developer's Certificate of Origin: + +> Developer's Certificate of Origin 1.1 +> By making a contribution to this project, I certify that: +> +> a. The contribution was created in whole or in part by me and I have the right to submit it under the open source license indicated in the file; or \ +> b. The contribution is based upon previous work that, to the best of my knowledge, is covered under an appropriate open source license and I have the right under that license to submit that work with modifications, whether created in whole or in part by me, under the same open source license (unless I am permitted to submit under a different license), as indicated in the file; or \ +> c. The contribution was provided directly to me by some other person who certified (a), (b) or (c) and I have not modified it. \ +> d I understand and agree that this project and the contribution are public and that a record of the contribution (including all personal information I submit with it, including my sign-off) is maintained indefinitely and may be redistributed consistent with this project or the open source license(s) involved. + +If indeed the change was made in line with the Developer's Certificate of Origin, add the following at the end of the commit: +``` +Signed-off-by: Random J Developer +``` + +**This much be your real name and a working email address.** + +This can also be added with the `--signoff flag`: +``` +$ git commit --signoff -m "Commit message" +``` + +If the change was given to you by someone else, and you have permission to contribute it here, that change must be signed off by the person who gave the change to you, and anyone before that (basically a chain of sign offs). Example: +``` + + +Signed-off-by: John Doe +Signed-off-by: Jane Doe +``` + +If multiple authors were involved in writing the change, then `Co-developed-by` must be present for both you and any other authors involved in the change. As an example with 2 authors: +``` + + +Co-developed-by: John Doe +Signed-off-by: John Doe +Co-developed-by: Jane Doe +Signed-off-by: Jane Doe +``` + ## Dependencies If you're using an IDE, it's recommened to install `bpy` as a Python module. In our experience, the [fake-bpy package](https://github.com/nutti/fake-bpy-module) seems to be the best. diff --git a/LICENSE-3RD-PARTY.txt b/LICENSE-3RD-PARTY.txt new file mode 100644 index 00000000..8234520d --- /dev/null +++ b/LICENSE-3RD-PARTY.txt @@ -0,0 +1,32 @@ +----------------------------------------------------------------------------- +The BSD 3-Clause License + +Applies to: + commonmcobj_parser.py Copyright (c) 2024, Mahid Sheikh + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +----------------------------------------------------------------------------- diff --git a/MCprep_addon/MCprep_resources/Languages/en_US/LC_MESSAGES/mcprep.po b/MCprep_addon/MCprep_resources/Languages/en_US/LC_MESSAGES/mcprep.po new file mode 100644 index 00000000..81cd45e1 --- /dev/null +++ b/MCprep_addon/MCprep_resources/Languages/en_US/LC_MESSAGES/mcprep.po @@ -0,0 +1,523 @@ +# MCprep American English Locale. +# Copyright (C) 2024 +# This file is distributed under the same license as the MCprep addon. +# Mahid Ahmad Sheikh, , 2024. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: 3.6.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2024-01-01 10:18-0600\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: Mahid Ahmad Sheikh \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=CHARSET\n" +"Content-Transfer-Encoding: 8bit\n" + +#: MCprep_addon/mcprep_ui.py:98 +msgid "Restart blender" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:100 +msgid "to complete update" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:121 +msgid "Load mobs" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:157 +msgid "No meshswap blocks found!" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:185 +msgid "No items found!" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:252 +msgid "No entities found!" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:274 +msgid "No models found!" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:314 +msgid "Load spawners" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:516 +msgid "World Importing & Meshswapping" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:520 +msgid "Default Exporter:" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:525 +msgid "jmc2obj executable" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:530 +msgid "Mineways executable" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:535 +msgid "World OBJ Exports Folder" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:540 +msgid "Meshwap assets" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:546 MCprep_addon/mcprep_ui.py:869 +#: MCprep_addon/mcprep_ui.py:1335 MCprep_addon/mcprep_ui.py:1483 +msgid "MeshSwap file not found" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:550 +msgid "Entity assets" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:556 MCprep_addon/mcprep_ui.py:1433 +msgid "Entity file not found" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:562 MCprep_addon/mcprep_ui.py:1656 +msgid "Effects folder not found" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:566 +msgid "Texture / Resource packs" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:570 MCprep_addon/mcprep_ui.py:841 +#: MCprep_addon/mcprep_ui.py:1649 +msgid "Texture pack folder" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:575 +msgid "Install to folder" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:579 +msgid "Open texture pack folder" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:584 +msgid "Mob spawning" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:588 +msgid "Rig Folder" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:593 +msgid "Select/install mobs" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:596 +msgid "Install file for mob spawning" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:598 +msgid "Open rig folder" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:603 MCprep_addon/mcprep_ui.py:675 +msgid "Skin swapping" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:607 +msgid "Skin Folder" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:612 +msgid "Install skins" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:614 +msgid "Install skin file for swapping" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:616 +msgid "Open skin folder" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:621 +msgid "Effects" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:625 +msgid "Effect folder" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:631 +msgid "Open effects folder" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:643 +msgid "Using MCprep in experimental mode!" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:644 +msgid "Early access features and requests for feedback" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:645 +msgid "will be made visible. Thank you for contributing." +msgstr "" + +#: MCprep_addon/mcprep_ui.py:649 +msgid "Unsure on how to use the addon? Check out these resources" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:654 +msgid "MCprep page for instructions and updates" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:662 +msgid "Learn MCprep + Blender, 1-minute tutorials" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:669 +msgid "Import Minecraft worlds" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:672 +msgid "Mob (rig) spawning" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:681 +msgid "jmc2obj/Mineways" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:684 +msgid "World Tools" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:687 +msgid "Tutorial Series" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:695 +msgid "Anonymous user tracking settings" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:704 +msgid "Opt into anonymous usage tracking" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:709 +msgid "Opt OUT of anonymous usage tracking" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:713 +msgid "For info on anonymous usage tracking:" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:716 +msgid "Open the Privacy Policy" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:748 +msgid "World exporter" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:759 MCprep_addon/mcprep_ui.py:787 +msgid "Select exporter!" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:769 +msgid "OBJ world import" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:773 MCprep_addon/mcprep_ui.py:1877 +msgid "MCprep tools" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:774 +msgid "Prep Materials" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:781 +msgid "OBJ incompatible with textureswap" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:785 +msgid "Mesh Swap" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:807 +msgid "(UI already improved)" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:810 +msgid "Improve UI" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:817 +msgid "Cycles Optimizer" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:827 MCprep_addon/mcprep_ui.py:834 +#: MCprep_addon/mcprep_ui.py:1021 MCprep_addon/mcprep_ui.py:1028 +#: MCprep_addon/mcprep_ui.py:1210 MCprep_addon/mcprep_ui.py:1217 +#: MCprep_addon/mcprep_ui.py:1318 MCprep_addon/mcprep_ui.py:1323 +#: MCprep_addon/mcprep_ui.py:1388 MCprep_addon/mcprep_ui.py:1392 +#: MCprep_addon/mcprep_ui.py:1468 MCprep_addon/mcprep_ui.py:1472 +#: MCprep_addon/mcprep_ui.py:1547 MCprep_addon/mcprep_ui.py:1551 +#: MCprep_addon/mcprep_ui.py:1636 MCprep_addon/mcprep_ui.py:1640 +msgid "Advanced" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:857 +msgid "Combine Materials" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:859 +msgid "Combine Images" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:861 +msgid "Meshswap source:" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:867 +msgid "MeshSwap file must be .blend" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:909 +msgid "World settings and lighting" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:924 +msgid "Time of day" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:931 +#, python-brace-format +msgid "{h}:{m}, day {d}" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:940 +msgid "No time controller," +msgstr "" + +#: MCprep_addon/mcprep_ui.py:941 +msgid "add dynamic MC world." +msgstr "" + +#: MCprep_addon/mcprep_ui.py:967 +msgid "Select skin" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:984 +msgid "No skins found/loaded" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:986 MCprep_addon/mcprep_ui.py:991 +msgid "Press to reload" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:989 +msgid "Reload skins" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:1009 +msgid "No skins found" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:1011 +msgid "Skin from file" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:1013 +msgid "Skin from username" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:1034 +msgid "Skin path" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:1048 +msgid "Reload mobs below" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:1052 +msgid "Reload skins above" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:1096 +msgid "No materials loaded" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:1105 +msgid "Load material" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:1123 MCprep_addon/mcprep_ui.py:1396 +#: MCprep_addon/mcprep_ui.py:1555 +msgid "Resource pack" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:1139 +msgid "Enter object mode" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:1140 +msgid "to use spawner" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:1149 +msgid "Import pre-rigged mobs & players" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:1168 +msgid "No mobs in category," +msgstr "" + +#: MCprep_addon/mcprep_ui.py:1169 +msgid "install a rig below or" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:1170 +msgid "copy file to folder." +msgstr "" + +#: MCprep_addon/mcprep_ui.py:1176 +msgid "No mobs loaded" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:1180 MCprep_addon/mcprep_ui.py:1289 +#: MCprep_addon/mcprep_ui.py:1371 MCprep_addon/mcprep_ui.py:1447 +#: MCprep_addon/mcprep_ui.py:1525 MCprep_addon/mcprep_ui.py:1602 +msgid "Reload assets" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:1224 +msgid "Mob spawner folder" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:1231 +msgid "Open mob folder" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:1240 +msgid "Change mob icon" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:1244 +msgid "Reload mobs" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:1252 +msgid "Import pre-made blocks (e.g. lights)" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:1266 +msgid "Meshswap file must be a .blend" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:1271 MCprep_addon/mcprep_ui.py:1280 +msgid "Reset meshswap path" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:1275 +msgid "Meshswap file not found" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:1284 +msgid "No blocks loaded" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:1308 +msgid "Place block" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:1327 +msgid "Meshswap file" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:1333 MCprep_addon/mcprep_ui.py:1481 +msgid "MeshSwap file must be a .blend" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:1346 +msgid "Generate items from textures" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:1366 +msgid "No items loaded" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:1377 +msgid "Place item" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:1410 +msgid "Import pre-rigged entities" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:1424 +msgid "Entity file must be a .blend" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:1429 MCprep_addon/mcprep_ui.py:1438 +msgid "Reset entity path" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:1442 +msgid "No entities loaded" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:1459 +msgid "Spawn Entity" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:1476 +msgid "Entity file" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:1495 +msgid "Generate models from .json files" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:1520 +msgid "No models loaded" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:1571 +msgid "Load/generate effects" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:1597 +msgid "No effects loaded" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:1608 +msgid "Add effect" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:1644 +msgid "Effects folder" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:1658 +msgid "Effects/collection folder not found" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:1660 +msgid "Effects/geonodes folder not found" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:1662 +msgid "Effects/particle folder not found" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:1680 +msgid "Click triangle to open" +msgstr "" diff --git a/MCprep_addon/MCprep_resources/Languages/mcprep.pot b/MCprep_addon/MCprep_resources/Languages/mcprep.pot new file mode 100644 index 00000000..b215b162 --- /dev/null +++ b/MCprep_addon/MCprep_resources/Languages/mcprep.pot @@ -0,0 +1,586 @@ +# +msgid "" +msgstr "" +"Project-Id-Version: 3.5.3\n" +"Report-Msgid-Bugs-To: https://github.com/Moo-Ack-Productions/MCprep/issues\n" +"POT-Creation-Date: 2024-06-17 00:40-0500\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +#: MCprep_addon/mcprep_ui.py:97 +msgid "Restart blender" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:99 +msgid "to complete update" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:108 +msgid "Mob Spawner" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:110 +msgid "Menu for placing in the shift-A add object menu" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:120 +msgid "Load mobs" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:149 +msgid "Meshswap Objects" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:156 +msgid "No meshswap blocks found!" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:178 +msgid "Item Spawner" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:184 +msgid "No items found!" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:201 +msgid "Effects Spawner" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:244 +msgid "Entity Spawner" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:251 +msgid "No entities found!" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:267 +msgid "Model Spawner" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:273 +msgid "No models found!" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:313 +msgid "Load spawners" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:515 +msgid "World Importing & Meshswapping" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:519 +msgid "Default Exporter:" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:524 +msgid "jmc2obj executable" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:529 +msgid "Mineways executable" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:534 +msgid "World OBJ Exports Folder" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:539 +msgid "Meshwap assets" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:545 MCprep_addon/mcprep_ui.py:858 +#: MCprep_addon/mcprep_ui.py:1324 MCprep_addon/mcprep_ui.py:1472 +msgid "MeshSwap file not found" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:549 +msgid "Entity assets" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:555 MCprep_addon/mcprep_ui.py:1422 +msgid "Entity file not found" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:561 MCprep_addon/mcprep_ui.py:1645 +msgid "Effects folder not found" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:565 +msgid "Texture / Resource packs" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:569 MCprep_addon/mcprep_ui.py:830 +#: MCprep_addon/mcprep_ui.py:1638 +msgid "Texture pack folder" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:574 +msgid "Install to folder" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:578 +msgid "Open texture pack folder" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:583 +msgid "Mob spawning" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:587 +msgid "Rig Folder" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:592 +msgid "Select/install mobs" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:595 +msgid "Install file for mob spawning" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:597 +msgid "Open rig folder" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:602 MCprep_addon/mcprep_ui.py:674 +msgid "Skin swapping" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:606 +msgid "Skin Folder" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:611 +msgid "Install skins" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:613 +msgid "Install skin file for swapping" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:615 +msgid "Open skin folder" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:620 +msgid "Effects" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:624 +msgid "Effect folder" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:630 +msgid "Open effects folder" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:642 +msgid "Using MCprep in experimental mode!" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:643 +msgid "Early access features and requests for feedback" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:644 +msgid "will be made visible. Thank you for contributing." +msgstr "" + +#: MCprep_addon/mcprep_ui.py:648 +msgid "Unsure on how to use the addon? Check out these resources" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:653 +msgid "MCprep page for instructions and updates" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:661 +msgid "Learn MCprep + Blender, 1-minute tutorials" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:668 +msgid "Import Minecraft worlds" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:671 +msgid "Mob (rig) spawning" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:680 +msgid "jmc2obj/Mineways" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:683 MCprep_addon/mcprep_ui.py:884 +msgid "World Tools" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:686 +msgid "Tutorial Series" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:694 +msgid "Anonymous user tracking settings" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:703 +msgid "Opt into anonymous usage tracking" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:708 +msgid "Opt OUT of anonymous usage tracking" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:712 +msgid "For info on anonymous usage tracking:" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:715 +msgid "Open the Privacy Policy" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:724 +msgid "World Imports" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:747 +msgid "World exporter" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:758 MCprep_addon/mcprep_ui.py:786 +msgid "Select exporter!" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:768 +msgid "OBJ world import" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:772 MCprep_addon/mcprep_ui.py:1866 +msgid "MCprep tools" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:773 +msgid "Prep Materials" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:780 +msgid "OBJ incompatible with textureswap" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:784 +msgid "Mesh Swap" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:806 +msgid "(UI already improved)" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:809 +msgid "Improve UI" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:816 MCprep_addon/mcprep_ui.py:823 +#: MCprep_addon/mcprep_ui.py:1010 MCprep_addon/mcprep_ui.py:1017 +#: MCprep_addon/mcprep_ui.py:1099 MCprep_addon/mcprep_ui.py:1199 +#: MCprep_addon/mcprep_ui.py:1206 MCprep_addon/mcprep_ui.py:1307 +#: MCprep_addon/mcprep_ui.py:1312 MCprep_addon/mcprep_ui.py:1377 +#: MCprep_addon/mcprep_ui.py:1381 MCprep_addon/mcprep_ui.py:1457 +#: MCprep_addon/mcprep_ui.py:1461 MCprep_addon/mcprep_ui.py:1536 +#: MCprep_addon/mcprep_ui.py:1540 MCprep_addon/mcprep_ui.py:1625 +#: MCprep_addon/mcprep_ui.py:1629 +msgid "Advanced" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:846 +msgid "Combine Materials" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:848 +msgid "Combine Images" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:850 +msgid "Meshswap source:" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:856 +msgid "MeshSwap file must be .blend" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:867 +msgid "World Bridge" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:898 +msgid "World settings and lighting" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:913 +msgid "Time of day" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:920 +msgid "{h}:{m}, day {d}" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:929 +msgid "No time controller," +msgstr "" + +#: MCprep_addon/mcprep_ui.py:930 +msgid "add dynamic MC world." +msgstr "" + +#: MCprep_addon/mcprep_ui.py:939 +msgid "Skin Swapper" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:956 +msgid "Select skin" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:973 +msgid "No skins found/loaded" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:975 MCprep_addon/mcprep_ui.py:980 +msgid "Press to reload" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:978 +msgid "Reload skins" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:998 +msgid "No skins found" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:1000 +msgid "Skin from file" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:1002 +msgid "Skin from username" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:1023 +msgid "Skin path" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:1037 +msgid "Reload mobs below" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:1041 +msgid "Reload skins above" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:1051 +msgid "MCprep materials" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:1085 +msgid "No materials loaded" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:1094 +msgid "Load material" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:1112 MCprep_addon/mcprep_ui.py:1385 +#: MCprep_addon/mcprep_ui.py:1544 +msgid "Resource pack" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:1128 +msgid "Enter object mode" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:1129 +msgid "to use spawner" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:1138 +msgid "Import pre-rigged mobs & players" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:1157 +msgid "No mobs in category," +msgstr "" + +#: MCprep_addon/mcprep_ui.py:1158 +msgid "install a rig below or" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:1159 +msgid "copy file to folder." +msgstr "" + +#: MCprep_addon/mcprep_ui.py:1165 +msgid "No mobs loaded" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:1169 MCprep_addon/mcprep_ui.py:1278 +#: MCprep_addon/mcprep_ui.py:1360 MCprep_addon/mcprep_ui.py:1436 +#: MCprep_addon/mcprep_ui.py:1514 MCprep_addon/mcprep_ui.py:1591 +msgid "Reload assets" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:1213 +msgid "Mob spawner folder" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:1220 +msgid "Open mob folder" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:1229 +msgid "Change mob icon" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:1233 +msgid "Reload mobs" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:1241 +msgid "Import pre-made blocks (e.g. lights)" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:1255 +msgid "Meshswap file must be a .blend" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:1260 MCprep_addon/mcprep_ui.py:1269 +msgid "Reset meshswap path" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:1264 +msgid "Meshswap file not found" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:1273 +msgid "No blocks loaded" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:1297 +msgid "Place block" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:1316 +msgid "Meshswap file" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:1322 MCprep_addon/mcprep_ui.py:1470 +msgid "MeshSwap file must be a .blend" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:1335 +msgid "Generate items from textures" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:1355 +msgid "No items loaded" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:1366 +msgid "Place item" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:1399 +msgid "Import pre-rigged entities" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:1413 +msgid "Entity file must be a .blend" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:1418 MCprep_addon/mcprep_ui.py:1427 +msgid "Reset entity path" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:1431 +msgid "No entities loaded" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:1448 +msgid "Spawn Entity" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:1465 +msgid "Entity file" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:1484 +msgid "Generate models from .json files" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:1509 +msgid "No models loaded" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:1560 +msgid "Load/generate effects" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:1586 +msgid "No effects loaded" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:1597 +msgid "Add effect" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:1633 +msgid "Effects folder" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:1647 +msgid "Effects/collection folder not found" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:1649 +msgid "Effects/geonodes folder not found" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:1651 +msgid "Effects/particle folder not found" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:1659 +msgid "Spawner" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:1669 +msgid "Click triangle to open" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:1678 +msgid "Mob spawner" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:1706 +msgid "Block (model) spawner" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:1734 +msgid "Item spawner" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:1763 +msgid "Effects + weather" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:1791 +msgid "Entity spawner" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:1819 +msgid "Meshswap spawner" +msgstr "" diff --git a/MCprep_addon/MCprep_resources/Languages/ru_RU/LC_MESSAGES/mcprep.po b/MCprep_addon/MCprep_resources/Languages/ru_RU/LC_MESSAGES/mcprep.po new file mode 100644 index 00000000..6155a83a --- /dev/null +++ b/MCprep_addon/MCprep_resources/Languages/ru_RU/LC_MESSAGES/mcprep.po @@ -0,0 +1,523 @@ +# MCprep Russian Locale. +# Copyright (C) 2024 +# This file is distributed under the same license as the MCprep addon. +# Aspirata, 2024. +# +msgid "" +msgstr "" +"Project-Id-Version: 3.6.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2024-01-01 10:18-0600\n" +"PO-Revision-Date: 2024-05-15 07:21+0700\n" +"Last-Translator: Aspirata <102383379+Aspirata@users.noreply.github.com>\n" +"Language-Team: \n" +"Language: ru_RU\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: Poedit 3.4.2\n" + +#: MCprep_addon/mcprep_ui.py:98 +msgid "Restart blender" +msgstr "Перезапустите блендер" + +#: MCprep_addon/mcprep_ui.py:100 +msgid "to complete update" +msgstr "для завершения обновления" + +#: MCprep_addon/mcprep_ui.py:121 +msgid "Load mobs" +msgstr "Загрузить мобов" + +#: MCprep_addon/mcprep_ui.py:157 +msgid "No meshswap blocks found!" +msgstr "Не найдено ни одного блока meshswap!" + +#: MCprep_addon/mcprep_ui.py:185 +msgid "No items found!" +msgstr "Предметы не найдены!" + +#: MCprep_addon/mcprep_ui.py:252 +msgid "No entities found!" +msgstr "Энтити не найдены!" + +#: MCprep_addon/mcprep_ui.py:274 +msgid "No models found!" +msgstr "Модели не найдены!" + +#: MCprep_addon/mcprep_ui.py:314 +msgid "Load spawners" +msgstr "Загрузить спавнера" + +#: MCprep_addon/mcprep_ui.py:516 +msgid "World Importing & Meshswapping" +msgstr "Импортирование мира & Meshswapping" + +#: MCprep_addon/mcprep_ui.py:520 +msgid "Default Exporter:" +msgstr "Экспортер по умолчанию:" + +#: MCprep_addon/mcprep_ui.py:525 +msgid "jmc2obj executable" +msgstr "jmc2obj.exe" + +#: MCprep_addon/mcprep_ui.py:530 +msgid "Mineways executable" +msgstr "Mineways.exe" + +#: MCprep_addon/mcprep_ui.py:535 +msgid "World OBJ Exports Folder" +msgstr "Папка экспорта obj миров" + +#: MCprep_addon/mcprep_ui.py:540 +msgid "Meshwap assets" +msgstr "Ассеты Meshswap" + +#: MCprep_addon/mcprep_ui.py:546 MCprep_addon/mcprep_ui.py:869 +#: MCprep_addon/mcprep_ui.py:1335 MCprep_addon/mcprep_ui.py:1483 +msgid "MeshSwap file not found" +msgstr "Файл MeshSwap не найден" + +#: MCprep_addon/mcprep_ui.py:550 +msgid "Entity assets" +msgstr "Ассеты энтити" + +#: MCprep_addon/mcprep_ui.py:556 MCprep_addon/mcprep_ui.py:1433 +msgid "Entity file not found" +msgstr "Файл энтити не найден" + +#: MCprep_addon/mcprep_ui.py:562 MCprep_addon/mcprep_ui.py:1656 +msgid "Effects folder not found" +msgstr "Файл эффектов не найден" + +#: MCprep_addon/mcprep_ui.py:566 +msgid "Texture / Resource packs" +msgstr "Текстура / Ресурс паки" + +#: MCprep_addon/mcprep_ui.py:570 MCprep_addon/mcprep_ui.py:841 +#: MCprep_addon/mcprep_ui.py:1649 +msgid "Texture pack folder" +msgstr "Папка текстур пака" + +#: MCprep_addon/mcprep_ui.py:575 +msgid "Install to folder" +msgstr "Установить в папку" + +#: MCprep_addon/mcprep_ui.py:579 +msgid "Open texture pack folder" +msgstr "Открыть папку текстур пака" + +#: MCprep_addon/mcprep_ui.py:584 +msgid "Mob spawning" +msgstr "Спавн моба" + +#: MCprep_addon/mcprep_ui.py:588 +msgid "Rig Folder" +msgstr "Папка ригов" + +#: MCprep_addon/mcprep_ui.py:593 +msgid "Select/install mobs" +msgstr "Выбрать/установить мобов" + +#: MCprep_addon/mcprep_ui.py:596 +msgid "Install file for mob spawning" +msgstr "Установите файл для спавна мобов" + +#: MCprep_addon/mcprep_ui.py:598 +msgid "Open rig folder" +msgstr "Открыть папку ригов" + +#: MCprep_addon/mcprep_ui.py:603 MCprep_addon/mcprep_ui.py:675 +msgid "Skin swapping" +msgstr "Смена скина" + +#: MCprep_addon/mcprep_ui.py:607 +msgid "Skin Folder" +msgstr "Папка Скина" + +#: MCprep_addon/mcprep_ui.py:612 +msgid "Install skins" +msgstr "Установить скины" + +#: MCprep_addon/mcprep_ui.py:614 +msgid "Install skin file for swapping" +msgstr "Установить скин для смены" + +#: MCprep_addon/mcprep_ui.py:616 +msgid "Open skin folder" +msgstr "Открыть папку скина" + +#: MCprep_addon/mcprep_ui.py:621 +msgid "Effects" +msgstr "Эффекты" + +#: MCprep_addon/mcprep_ui.py:625 +msgid "Effect folder" +msgstr "Папка эффектов" + +#: MCprep_addon/mcprep_ui.py:631 +msgid "Open effects folder" +msgstr "Открыть папку эффектов" + +#: MCprep_addon/mcprep_ui.py:643 +msgid "Using MCprep in experimental mode!" +msgstr "Использование MCprep в эксперементальном режиме!" + +#: MCprep_addon/mcprep_ui.py:644 +msgid "Early access features and requests for feedback" +msgstr "Функции раннего доступа и запросы на обратную связь" + +#: MCprep_addon/mcprep_ui.py:645 +msgid "will be made visible. Thank you for contributing." +msgstr "будут видимы. Спасибо за помощь." + +#: MCprep_addon/mcprep_ui.py:649 +msgid "Unsure on how to use the addon? Check out these resources" +msgstr "Не уверены как использовать аддон? Проверьте эти ресурсы" + +#: MCprep_addon/mcprep_ui.py:654 +msgid "MCprep page for instructions and updates" +msgstr "Страница MCprep для инструкций и обновлений" + +#: MCprep_addon/mcprep_ui.py:662 +msgid "Learn MCprep + Blender, 1-minute tutorials" +msgstr "Изучить MCprep + Blender, одноминутные туториалы" + +#: MCprep_addon/mcprep_ui.py:669 +msgid "Import Minecraft worlds" +msgstr "Импорт Майнкрафт миров" + +#: MCprep_addon/mcprep_ui.py:672 +msgid "Mob (rig) spawning" +msgstr "Спавн моба (рига)" + +#: MCprep_addon/mcprep_ui.py:681 +msgid "jmc2obj/Mineways" +msgstr "jmc2obj/Mineways" + +#: MCprep_addon/mcprep_ui.py:684 +msgid "World Tools" +msgstr "Инструменты мира" + +#: MCprep_addon/mcprep_ui.py:687 +msgid "Tutorial Series" +msgstr "Серия Гайдов" + +#: MCprep_addon/mcprep_ui.py:695 +msgid "Anonymous user tracking settings" +msgstr "Настройки анонимного отслеживания пользователя" + +#: MCprep_addon/mcprep_ui.py:704 +msgid "Opt into anonymous usage tracking" +msgstr "Принять анонимное отслеживание использования" + +#: MCprep_addon/mcprep_ui.py:709 +msgid "Opt OUT of anonymous usage tracking" +msgstr "ОТКАЗАТЬСЯ от анонимного отслеживания использования" + +#: MCprep_addon/mcprep_ui.py:713 +msgid "For info on anonymous usage tracking:" +msgstr "Для информации об анонимном отслеживании использования:" + +#: MCprep_addon/mcprep_ui.py:716 +msgid "Open the Privacy Policy" +msgstr "Открыть Политику Конфиденциальности" + +#: MCprep_addon/mcprep_ui.py:748 +msgid "World exporter" +msgstr "Экспортер мира" + +#: MCprep_addon/mcprep_ui.py:759 MCprep_addon/mcprep_ui.py:787 +msgid "Select exporter!" +msgstr "Выберите экспортер!" + +#: MCprep_addon/mcprep_ui.py:769 +msgid "OBJ world import" +msgstr "Импорт OBJ мира" + +#: MCprep_addon/mcprep_ui.py:773 MCprep_addon/mcprep_ui.py:1877 +msgid "MCprep tools" +msgstr "Инструменты MCprep" + +#: MCprep_addon/mcprep_ui.py:774 +msgid "Prep Materials" +msgstr "Подготовить материалы" + +#: MCprep_addon/mcprep_ui.py:781 +msgid "OBJ incompatible with textureswap" +msgstr "OBJ несовместим с заменой текстур" + +#: MCprep_addon/mcprep_ui.py:785 +msgid "Mesh Swap" +msgstr "Заменить Меш" + +#: MCprep_addon/mcprep_ui.py:807 +msgid "(UI already improved)" +msgstr "(UI уже улучшен)" + +#: MCprep_addon/mcprep_ui.py:810 +msgid "Improve UI" +msgstr "Улучшить UI" + +#: MCprep_addon/mcprep_ui.py:817 +msgid "Cycles Optimizer" +msgstr "Оптимизатор Cycles" + +#: MCprep_addon/mcprep_ui.py:827 MCprep_addon/mcprep_ui.py:834 +#: MCprep_addon/mcprep_ui.py:1021 MCprep_addon/mcprep_ui.py:1028 +#: MCprep_addon/mcprep_ui.py:1210 MCprep_addon/mcprep_ui.py:1217 +#: MCprep_addon/mcprep_ui.py:1318 MCprep_addon/mcprep_ui.py:1323 +#: MCprep_addon/mcprep_ui.py:1388 MCprep_addon/mcprep_ui.py:1392 +#: MCprep_addon/mcprep_ui.py:1468 MCprep_addon/mcprep_ui.py:1472 +#: MCprep_addon/mcprep_ui.py:1547 MCprep_addon/mcprep_ui.py:1551 +#: MCprep_addon/mcprep_ui.py:1636 MCprep_addon/mcprep_ui.py:1640 +msgid "Advanced" +msgstr "Расширенные" + +#: MCprep_addon/mcprep_ui.py:857 +msgid "Combine Materials" +msgstr "Совместить Материалы" + +#: MCprep_addon/mcprep_ui.py:859 +msgid "Combine Images" +msgstr "Совместить Текстуры" + +#: MCprep_addon/mcprep_ui.py:861 +msgid "Meshswap source:" +msgstr "Источник Meshswap:" + +#: MCprep_addon/mcprep_ui.py:867 +msgid "MeshSwap file must be .blend" +msgstr "Файл Meshswap должен быть .blend" + +#: MCprep_addon/mcprep_ui.py:909 +msgid "World settings and lighting" +msgstr "Настройки и освещение мира" + +#: MCprep_addon/mcprep_ui.py:924 +msgid "Time of day" +msgstr "Время дня" + +#: MCprep_addon/mcprep_ui.py:931 +#, python-brace-format +msgid "{h}:{m}, day {d}" +msgstr "{h}:{m}, день {d}" + +#: MCprep_addon/mcprep_ui.py:940 +msgid "No time controller," +msgstr "Нет контроллера времени," + +#: MCprep_addon/mcprep_ui.py:941 +msgid "add dynamic MC world." +msgstr "добавьте динамический MC мир." + +#: MCprep_addon/mcprep_ui.py:967 +msgid "Select skin" +msgstr "Выбрать скин" + +#: MCprep_addon/mcprep_ui.py:984 +msgid "No skins found/loaded" +msgstr "Скины не найдены/загружены" + +#: MCprep_addon/mcprep_ui.py:986 MCprep_addon/mcprep_ui.py:991 +msgid "Press to reload" +msgstr "Нажмите, чтобы перезагрузить" + +#: MCprep_addon/mcprep_ui.py:989 +msgid "Reload skins" +msgstr "Перезагрузить скины" + +#: MCprep_addon/mcprep_ui.py:1009 +msgid "No skins found" +msgstr "Скины не найдены" + +#: MCprep_addon/mcprep_ui.py:1011 +msgid "Skin from file" +msgstr "Скин из файла" + +#: MCprep_addon/mcprep_ui.py:1013 +msgid "Skin from username" +msgstr "Скин по нику" + +#: MCprep_addon/mcprep_ui.py:1034 +msgid "Skin path" +msgstr "Путь до скина" + +#: MCprep_addon/mcprep_ui.py:1048 +msgid "Reload mobs below" +msgstr "Перезагрузить мобов ниже" + +#: MCprep_addon/mcprep_ui.py:1052 +msgid "Reload skins above" +msgstr "Перезагрузить скины выше" + +#: MCprep_addon/mcprep_ui.py:1096 +msgid "No materials loaded" +msgstr "Материалы не загружены" + +#: MCprep_addon/mcprep_ui.py:1105 +msgid "Load material" +msgstr "Загрузить материал" + +#: MCprep_addon/mcprep_ui.py:1123 MCprep_addon/mcprep_ui.py:1396 +#: MCprep_addon/mcprep_ui.py:1555 +msgid "Resource pack" +msgstr "Ресурс пак" + +#: MCprep_addon/mcprep_ui.py:1139 +msgid "Enter object mode" +msgstr "Войдите в объектный режим" + +#: MCprep_addon/mcprep_ui.py:1140 +msgid "to use spawner" +msgstr "чтобы использовать спавнер" + +#: MCprep_addon/mcprep_ui.py:1149 +msgid "Import pre-rigged mobs & players" +msgstr "Импортировать заригганых мобов & игроков" + +#: MCprep_addon/mcprep_ui.py:1168 +msgid "No mobs in category," +msgstr "Мобов в категории нет," + +#: MCprep_addon/mcprep_ui.py:1169 +msgid "install a rig below or" +msgstr "установите риг ниже или" + +#: MCprep_addon/mcprep_ui.py:1170 +msgid "copy file to folder." +msgstr "скопируйте файл в папку" + +#: MCprep_addon/mcprep_ui.py:1176 +msgid "No mobs loaded" +msgstr "Мобы не загружены" + +#: MCprep_addon/mcprep_ui.py:1180 MCprep_addon/mcprep_ui.py:1289 +#: MCprep_addon/mcprep_ui.py:1371 MCprep_addon/mcprep_ui.py:1447 +#: MCprep_addon/mcprep_ui.py:1525 MCprep_addon/mcprep_ui.py:1602 +msgid "Reload assets" +msgstr "Перезагрузить ассеты" + +#: MCprep_addon/mcprep_ui.py:1224 +msgid "Mob spawner folder" +msgstr "Папка спавнера мобов" + +#: MCprep_addon/mcprep_ui.py:1231 +msgid "Open mob folder" +msgstr "Открыть папку мобов" + +#: MCprep_addon/mcprep_ui.py:1240 +msgid "Change mob icon" +msgstr "Изменить иконку моба" + +#: MCprep_addon/mcprep_ui.py:1244 +msgid "Reload mobs" +msgstr "Перезагрузить мобов" + +#: MCprep_addon/mcprep_ui.py:1252 +msgid "Import pre-made blocks (e.g. lights)" +msgstr "Импортировать заранее созданные блоки (например свет)" + +#: MCprep_addon/mcprep_ui.py:1266 +msgid "Meshswap file must be a .blend" +msgstr "Файл MeshSwap должен быть .blend" + +#: MCprep_addon/mcprep_ui.py:1271 MCprep_addon/mcprep_ui.py:1280 +msgid "Reset meshswap path" +msgstr "Сбросить путь meshswap" + +#: MCprep_addon/mcprep_ui.py:1275 +msgid "Meshswap file not found" +msgstr "Meshswap файл не найден" + +#: MCprep_addon/mcprep_ui.py:1284 +msgid "No blocks loaded" +msgstr "Блоки не загружены" + +#: MCprep_addon/mcprep_ui.py:1308 +msgid "Place block" +msgstr "Разместить блок" + +#: MCprep_addon/mcprep_ui.py:1327 +msgid "Meshswap file" +msgstr "Файл Meshswap" + +#: MCprep_addon/mcprep_ui.py:1333 MCprep_addon/mcprep_ui.py:1481 +msgid "MeshSwap file must be a .blend" +msgstr "Файл MeshSwap должен быть .blend" + +#: MCprep_addon/mcprep_ui.py:1346 +msgid "Generate items from textures" +msgstr "Сгенерировать предметы из текстур" + +#: MCprep_addon/mcprep_ui.py:1366 +msgid "No items loaded" +msgstr "Предметы не загружены" + +#: MCprep_addon/mcprep_ui.py:1377 +msgid "Place item" +msgstr "Поставить предмет" + +#: MCprep_addon/mcprep_ui.py:1410 +msgid "Import pre-rigged entities" +msgstr "Импортировать зариганных энтити" + +#: MCprep_addon/mcprep_ui.py:1424 +msgid "Entity file must be a .blend" +msgstr "Файл энтити должен быть .blend" + +#: MCprep_addon/mcprep_ui.py:1429 MCprep_addon/mcprep_ui.py:1438 +msgid "Reset entity path" +msgstr "Сбросить путь энтити" + +#: MCprep_addon/mcprep_ui.py:1442 +msgid "No entities loaded" +msgstr "Энтити не загружены" + +#: MCprep_addon/mcprep_ui.py:1459 +msgid "Spawn Entity" +msgstr "Заспавнить Этити" + +#: MCprep_addon/mcprep_ui.py:1476 +msgid "Entity file" +msgstr "Файл энтити" + +#: MCprep_addon/mcprep_ui.py:1495 +msgid "Generate models from .json files" +msgstr "Сгенерировать модели из .json файлов" + +#: MCprep_addon/mcprep_ui.py:1520 +msgid "No models loaded" +msgstr "Модели не загружены" + +#: MCprep_addon/mcprep_ui.py:1571 +msgid "Load/generate effects" +msgstr "Загрузить/сгенерировать эффекты" + +#: MCprep_addon/mcprep_ui.py:1597 +msgid "No effects loaded" +msgstr "Эффекты не загружены" + +#: MCprep_addon/mcprep_ui.py:1608 +msgid "Add effect" +msgstr "Добавить эффект" + +#: MCprep_addon/mcprep_ui.py:1644 +msgid "Effects folder" +msgstr "Папка эффектов" + +#: MCprep_addon/mcprep_ui.py:1658 +msgid "Effects/collection folder not found" +msgstr "Папка эффектов/коллекции не найдена" + +#: MCprep_addon/mcprep_ui.py:1660 +msgid "Effects/geonodes folder not found" +msgstr "Папка эффектов/геонод не найдена" + +#: MCprep_addon/mcprep_ui.py:1662 +msgid "Effects/particle folder not found" +msgstr "Папка эффектов/частиц не найдена" + +#: MCprep_addon/mcprep_ui.py:1680 +msgid "Click triangle to open" +msgstr "Нажмите на треугольник, чтобы открыть" diff --git a/MCprep_addon/MCprep_resources/Languages/zh_HANS/LC_MESSAGES/mcprep.po b/MCprep_addon/MCprep_resources/Languages/zh_HANS/LC_MESSAGES/mcprep.po new file mode 100644 index 00000000..5a7a5b14 --- /dev/null +++ b/MCprep_addon/MCprep_resources/Languages/zh_HANS/LC_MESSAGES/mcprep.po @@ -0,0 +1,596 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +msgid "" +msgstr "" +"Project-Id-Version: \n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2024-02-03 22:03-0600\n" +"PO-Revision-Date: 2024-02-03 22:16-0600\n" +"Last-Translator: \n" +"Language-Team: \n" +"Language: zh_CN\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: Poedit 3.4.1\n" + +#: MCprep_addon/mcprep_ui.py:98 +msgid "Restart blender" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:100 +msgid "to complete update" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:109 +msgid "Mob Spawner" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:111 +msgid "Menu for placing in the shift-A add object menu" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:121 +msgid "Load mobs" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:150 +msgid "Meshswap Objects" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:157 +msgid "No meshswap blocks found!" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:179 +msgid "Item Spawner" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:185 +msgid "No items found!" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:202 +msgid "Effects Spawner" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:245 +msgid "Entity Spawner" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:252 +msgid "No entities found!" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:268 +msgid "Model Spawner" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:274 +msgid "No models found!" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:314 +msgid "Load spawners" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:516 +msgid "World Importing & Meshswapping" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:520 +msgid "Default Exporter:" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:525 +msgid "jmc2obj executable" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:530 +msgid "Mineways executable" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:535 +msgid "World OBJ Exports Folder" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:540 +msgid "Meshwap assets" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:546 MCprep_addon/mcprep_ui.py:871 +#: MCprep_addon/mcprep_ui.py:1337 MCprep_addon/mcprep_ui.py:1485 +msgid "MeshSwap file not found" +msgstr "未找到网格变形文件" + +#: MCprep_addon/mcprep_ui.py:550 +msgid "Entity assets" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:556 MCprep_addon/mcprep_ui.py:1435 +msgid "Entity file not found" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:562 MCprep_addon/mcprep_ui.py:1658 +msgid "Effects folder not found" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:566 +msgid "Texture / Resource packs" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:570 MCprep_addon/mcprep_ui.py:843 +#: MCprep_addon/mcprep_ui.py:1651 +msgid "Texture pack folder" +msgstr "材质包文件夹" + +#: MCprep_addon/mcprep_ui.py:575 +msgid "Install to folder" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:579 +msgid "Open texture pack folder" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:584 +msgid "Mob spawning" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:588 +msgid "Rig Folder" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:593 +msgid "Select/install mobs" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:596 +msgid "Install file for mob spawning" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:598 +msgid "Open rig folder" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:603 MCprep_addon/mcprep_ui.py:675 +msgid "Skin swapping" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:607 +msgid "Skin Folder" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:612 +msgid "Install skins" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:614 +msgid "Install skin file for swapping" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:616 +msgid "Open skin folder" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:621 +msgid "Effects" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:625 +msgid "Effect folder" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:631 +msgid "Open effects folder" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:643 +msgid "Using MCprep in experimental mode!" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:644 +msgid "Early access features and requests for feedback" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:645 +msgid "will be made visible. Thank you for contributing." +msgstr "" + +#: MCprep_addon/mcprep_ui.py:649 +msgid "Unsure on how to use the addon? Check out these resources" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:654 +msgid "MCprep page for instructions and updates" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:662 +msgid "Learn MCprep + Blender, 1-minute tutorials" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:669 +msgid "Import Minecraft worlds" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:672 +msgid "Mob (rig) spawning" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:681 +msgid "jmc2obj/Mineways" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:684 MCprep_addon/mcprep_ui.py:897 +msgid "World Tools" +msgstr "世界工具" + +#: MCprep_addon/mcprep_ui.py:687 +msgid "Tutorial Series" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:695 +msgid "Anonymous user tracking settings" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:704 +msgid "Opt into anonymous usage tracking" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:709 +msgid "Opt OUT of anonymous usage tracking" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:713 +msgid "For info on anonymous usage tracking:" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:716 +msgid "Open the Privacy Policy" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:725 +msgid "World Imports" +msgstr "导入地图" + +#: MCprep_addon/mcprep_ui.py:748 +msgid "World exporter" +msgstr "地图导出工具" + +#: MCprep_addon/mcprep_ui.py:759 MCprep_addon/mcprep_ui.py:787 +msgid "Select exporter!" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:769 +msgid "OBJ world import" +msgstr "导入 OBJ 世界" + +#: MCprep_addon/mcprep_ui.py:773 MCprep_addon/mcprep_ui.py:1879 +msgid "MCprep tools" +msgstr "MCprep 工具" + +#: MCprep_addon/mcprep_ui.py:774 +msgid "Prep Materials" +msgstr "准备材质" + +#: MCprep_addon/mcprep_ui.py:781 +msgid "OBJ incompatible with textureswap" +msgstr "OBJ与纹理交换不兼容" + +#: MCprep_addon/mcprep_ui.py:785 +msgid "Mesh Swap" +msgstr "网格变形" + +#: MCprep_addon/mcprep_ui.py:807 +msgid "(UI already improved)" +msgstr "(UI 已经改善)" + +#: MCprep_addon/mcprep_ui.py:810 +msgid "Improve UI" +msgstr "改善 UI" + +#: MCprep_addon/mcprep_ui.py:817 +msgid "Cycles Optimizer" +msgstr "Cycles 优化器" + +#: MCprep_addon/mcprep_ui.py:829 MCprep_addon/mcprep_ui.py:836 +#: MCprep_addon/mcprep_ui.py:1023 MCprep_addon/mcprep_ui.py:1030 +#: MCprep_addon/mcprep_ui.py:1112 MCprep_addon/mcprep_ui.py:1212 +#: MCprep_addon/mcprep_ui.py:1219 MCprep_addon/mcprep_ui.py:1320 +#: MCprep_addon/mcprep_ui.py:1325 MCprep_addon/mcprep_ui.py:1390 +#: MCprep_addon/mcprep_ui.py:1394 MCprep_addon/mcprep_ui.py:1470 +#: MCprep_addon/mcprep_ui.py:1474 MCprep_addon/mcprep_ui.py:1549 +#: MCprep_addon/mcprep_ui.py:1553 MCprep_addon/mcprep_ui.py:1638 +#: MCprep_addon/mcprep_ui.py:1642 +msgid "Advanced" +msgstr "高级" + +#: MCprep_addon/mcprep_ui.py:859 +msgid "Combine Materials" +msgstr "合并材质" + +#: MCprep_addon/mcprep_ui.py:861 +msgid "Combine Images" +msgstr "合并图像" + +#: MCprep_addon/mcprep_ui.py:863 +msgid "Meshswap source:" +msgstr "网格变形源:" + +#: MCprep_addon/mcprep_ui.py:869 +msgid "MeshSwap file must be .blend" +msgstr "网格变形文件必须是.blend格式" + +#: MCprep_addon/mcprep_ui.py:880 +msgid "World Bridge" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:911 +msgid "World settings and lighting" +msgstr "世界设置和光照" + +#: MCprep_addon/mcprep_ui.py:926 +msgid "Time of day" +msgstr "时间" + +#: MCprep_addon/mcprep_ui.py:933 +#, python-brace-format +msgid "{h}:{m}, day {d}" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:942 +msgid "No time controller," +msgstr "没有时间控制器," + +#: MCprep_addon/mcprep_ui.py:943 +msgid "add dynamic MC world." +msgstr "请添加动态MC世界。" + +#: MCprep_addon/mcprep_ui.py:952 +msgid "Skin Swapper" +msgstr "皮肤交换器" + +#: MCprep_addon/mcprep_ui.py:969 +msgid "Select skin" +msgstr "选择皮肤" + +#: MCprep_addon/mcprep_ui.py:986 +msgid "No skins found/loaded" +msgstr "未找到/加载皮肤" + +#: MCprep_addon/mcprep_ui.py:988 MCprep_addon/mcprep_ui.py:993 +msgid "Press to reload" +msgstr "\"按下重新加载" + +#: MCprep_addon/mcprep_ui.py:991 +msgid "Reload skins" +msgstr "重新加载皮肤" + +#: MCprep_addon/mcprep_ui.py:1011 +msgid "No skins found" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:1013 +msgid "Skin from file" +msgstr "从文件导入皮肤" + +#: MCprep_addon/mcprep_ui.py:1015 +msgid "Skin from username" +msgstr "从用户名导入皮肤" + +#: MCprep_addon/mcprep_ui.py:1036 +msgid "Skin path" +msgstr "皮肤路径" + +#: MCprep_addon/mcprep_ui.py:1050 +msgid "Reload mobs below" +msgstr "重新加载下方的生物" + +#: MCprep_addon/mcprep_ui.py:1054 +msgid "Reload skins above" +msgstr "重新加载上方的皮肤" + +#: MCprep_addon/mcprep_ui.py:1064 +msgid "MCprep materials" +msgstr "MCprep 材质" + +#: MCprep_addon/mcprep_ui.py:1098 +msgid "No materials loaded" +msgstr "未加载材质" + +#: MCprep_addon/mcprep_ui.py:1107 +msgid "Load material" +msgstr "加载材质" + +#: MCprep_addon/mcprep_ui.py:1125 MCprep_addon/mcprep_ui.py:1398 +#: MCprep_addon/mcprep_ui.py:1557 +msgid "Resource pack" +msgstr "资源包" + +#: MCprep_addon/mcprep_ui.py:1141 +msgid "Enter object mode" +msgstr "进入对象模式" + +#: MCprep_addon/mcprep_ui.py:1142 +msgid "to use spawner" +msgstr "以使用生成器" + +#: MCprep_addon/mcprep_ui.py:1151 +msgid "Import pre-rigged mobs & players" +msgstr "导入预设置的生物和玩家" + +#: MCprep_addon/mcprep_ui.py:1170 +msgid "No mobs in category," +msgstr "该类别中没有生物," + +#: MCprep_addon/mcprep_ui.py:1171 +msgid "install a rig below or" +msgstr "安装一个下方的预设或" + +#: MCprep_addon/mcprep_ui.py:1172 +msgid "copy file to folder." +msgstr "将文件复制到文件夹中。" + +#: MCprep_addon/mcprep_ui.py:1178 +msgid "No mobs loaded" +msgstr "未加载生物" + +#: MCprep_addon/mcprep_ui.py:1182 MCprep_addon/mcprep_ui.py:1291 +#: MCprep_addon/mcprep_ui.py:1373 MCprep_addon/mcprep_ui.py:1449 +#: MCprep_addon/mcprep_ui.py:1527 MCprep_addon/mcprep_ui.py:1604 +msgid "Reload assets" +msgstr "重新加载资源" + +#: MCprep_addon/mcprep_ui.py:1226 +msgid "Mob spawner folder" +msgstr "生物生成器文件夹" + +#: MCprep_addon/mcprep_ui.py:1233 +msgid "Open mob folder" +msgstr "打开生物文件夹" + +#: MCprep_addon/mcprep_ui.py:1242 +msgid "Change mob icon" +msgstr "更改生物图标" + +#: MCprep_addon/mcprep_ui.py:1246 +msgid "Reload mobs" +msgstr "重新加载生物" + +#: MCprep_addon/mcprep_ui.py:1254 +msgid "Import pre-made blocks (e.g. lights)" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:1268 +msgid "Meshswap file must be a .blend" +msgstr "网格变形文件必须是.blend格式" + +#: MCprep_addon/mcprep_ui.py:1273 MCprep_addon/mcprep_ui.py:1282 +msgid "Reset meshswap path" +msgstr "重置网格变形路径" + +#: MCprep_addon/mcprep_ui.py:1277 +msgid "Meshswap file not found" +msgstr "未找到网格变形文件" + +#: MCprep_addon/mcprep_ui.py:1286 +msgid "No blocks loaded" +msgstr "未加载方块" + +#: MCprep_addon/mcprep_ui.py:1310 +msgid "Place block" +msgstr "放置方块2" + +#: MCprep_addon/mcprep_ui.py:1329 +msgid "Meshswap file" +msgstr "网格变形文件" + +#: MCprep_addon/mcprep_ui.py:1335 MCprep_addon/mcprep_ui.py:1483 +msgid "MeshSwap file must be a .blend" +msgstr "网格变形文件必须是.blend格式" + +#: MCprep_addon/mcprep_ui.py:1348 +msgid "Generate items from textures" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:1368 +msgid "No items loaded" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:1379 +msgid "Place item" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:1412 +msgid "Import pre-rigged entities" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:1426 +msgid "Entity file must be a .blend" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:1431 MCprep_addon/mcprep_ui.py:1440 +msgid "Reset entity path" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:1444 +msgid "No entities loaded" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:1461 +msgid "Spawn Entity" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:1478 +msgid "Entity file" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:1497 +msgid "Generate models from .json files" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:1522 +msgid "No models loaded" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:1573 +msgid "Load/generate effects" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:1599 +msgid "No effects loaded" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:1610 +msgid "Add effect" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:1646 +msgid "Effects folder" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:1660 +msgid "Effects/collection folder not found" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:1662 +msgid "Effects/geonodes folder not found" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:1664 +msgid "Effects/particle folder not found" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:1672 +msgid "Spawner" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:1682 +msgid "Click triangle to open" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:1691 +msgid "Mob spawner" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:1719 +msgid "Block (model) spawner" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:1747 +msgid "Item spawner" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:1776 +msgid "Effects + weather" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:1804 +msgid "Entity spawner" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:1832 +msgid "Meshswap spawner" +msgstr "" diff --git a/MCprep_addon/MCprep_resources/mcprep_data_update.json b/MCprep_addon/MCprep_resources/mcprep_data_update.json index f3c3b494..1e4b1962 100644 --- a/MCprep_addon/MCprep_resources/mcprep_data_update.json +++ b/MCprep_addon/MCprep_resources/mcprep_data_update.json @@ -869,7 +869,7 @@ "absorbing_hardcore_half": "gui/sprites/hud/heart/absorbing_hardcore_half", "absorbing_hardcore_half_blinking": "gui/sprites/hud/heart/absorbing_hardcore_half_blinking", "absorption": "mob_effect/absorption", - "acacia": "entity/chest_boat/acacia", + "acacia": "entity/signs/acacia", "acacia_door_bottom": "acacia_door_bottom", "acacia_door_top": "acacia_door_top", "acacia_leaves": "acacia_leaves", @@ -885,7 +885,7 @@ "activator_rail": "activator_rail", "activator_rail_on": "activator_rail_on", "advancement": "gui/sprites/toast/advancement", - "adventure": "gui/advancements/backgrounds/adventure", + "adventure": "gui/realms/adventure", "aggressive_panda": "entity/panda/aggressive_panda", "air": "gui/sprites/hud/air", "air_bursting": "gui/sprites/hud/air_bursting", @@ -906,6 +906,7 @@ "anvil_top": "anvil_top", "archer_pottery_pattern": "entity/decorated_pot/archer_pottery_pattern", "ari": "entity/player/wide/ari", + "armadillo": "entity/armadillo", "armor_empty": "gui/sprites/hud/armor_empty", "armor_full": "gui/sprites/hud/armor_full", "armor_half": "gui/sprites/hud/armor_half", @@ -933,7 +934,8 @@ "back": "painting/back", "background": "gui/sprites/social_interactions/background", "bad_omen": "mob_effect/bad_omen", - "bamboo": "entity/chest_boat/bamboo", + "bad_omen_121": "mob_effect/bad_omen_121", + "bamboo": "entity/signs/bamboo", "bamboo_block": "bamboo_block", "bamboo_block_top": "bamboo_block_top", "bamboo_door_bottom": "bamboo_door_bottom", @@ -1002,7 +1004,7 @@ "big_smoke_7": "particle/big_smoke_7", "big_smoke_8": "particle/big_smoke_8", "big_smoke_9": "particle/big_smoke_9", - "birch": "entity/chest_boat/birch", + "birch": "entity/signs/birch", "birch_door_bottom": "birch_door_bottom", "birch_door_top": "birch_door_top", "birch_leaves": "birch_leaves", @@ -1011,7 +1013,8 @@ "birch_planks": "birch_planks", "birch_sapling": "birch_sapling", "birch_trapdoor": "birch_trapdoor", - "black": "entity/cat/black", + "black": "entity/llama/decor/black", + "black_banner": "map/decorations/black_banner", "black_candle": "black_candle", "black_candle_lit": "black_candle_lit", "black_concrete": "black_concrete", @@ -1036,12 +1039,14 @@ "blocked_slot": "gui/sprites/container/bundle/blocked_slot", "blue": "entity/llama/decor/blue", "blue_background": "gui/sprites/boss_bar/blue_background", + "blue_banner": "map/decorations/blue_banner", "blue_candle": "blue_candle", "blue_candle_lit": "blue_candle_lit", "blue_concrete": "blue_concrete", "blue_concrete_powder": "blue_concrete_powder", "blue_glazed_terracotta": "blue_glazed_terracotta", "blue_ice": "blue_ice", + "blue_marker": "map/decorations/blue_marker", "blue_orchid": "blue_orchid", "blue_progress": "gui/sprites/boss_bar/blue_progress", "blue_shulker_box": "blue_shulker_box", @@ -1049,6 +1054,10 @@ "blue_stained_glass_pane_top": "blue_stained_glass_pane_top", "blue_terracotta": "blue_terracotta", "blue_wool": "blue_wool", + "bogged": "entity/skeleton/bogged", + "bogged_overlay": "entity/skeleton/bogged_overlay", + "bolt": "trims/models/armor/bolt", + "bolt_leggings": "trims/models/armor/bolt_leggings", "bomb": "painting/bomb", "bone_block_side": "bone_block_side", "bone_block_top": "bone_block_top", @@ -1063,6 +1072,7 @@ "brain_coral_fan": "brain_coral_fan", "break_particle": "entity/conduit/break_particle", "breeze": "entity/breeze/breeze", + "breeze_eyes": "entity/breeze/breeze_eyes", "breeze_wind": "entity/breeze/breeze_wind", "brew_progress": "gui/sprites/container/brewing_stand/brew_progress", "brewer_pottery_pattern": "entity/decorated_pot/brewer_pottery_pattern", @@ -1071,6 +1081,7 @@ "bricks": "bricks", "british_shorthair": "entity/cat/british_shorthair", "brown": "entity/llama/decor/brown", + "brown_banner": "map/decorations/brown_banner", "brown_candle": "brown_candle", "brown_candle_lit": "brown_candle_lit", "brown_concrete": "brown_concrete", @@ -1097,13 +1108,13 @@ "bubbles": "gui/sprites/container/brewing_stand/bubbles", "budding_amethyst": "budding_amethyst", "burn_pottery_pattern": "entity/decorated_pot/burn_pottery_pattern", - "burn_progress": "gui/sprites/container/furnace/burn_progress", + "burn_progress": "gui/sprites/container/blast_furnace/burn_progress", "burning_skull": "painting/burning_skull", "bust": "painting/bust", "butcher": "entity/villager/profession/butcher", - "button": "gui/sprites/recipe_book/button", + "button": "gui/sprites/widget/button", "button_disabled": "gui/sprites/widget/button_disabled", - "button_highlighted": "gui/sprites/recipe_book/button_highlighted", + "button_highlighted": "gui/sprites/widget/button_highlighted", "button_selected": "gui/sprites/container/beacon/button_selected", "cactus_bottom": "cactus_bottom", "cactus_side": "cactus_side", @@ -1156,15 +1167,13 @@ "chainmail_layer_2": "models/armor/chainmail_layer_2", "challenge_frame_obtained": "gui/sprites/advancements/challenge_frame_obtained", "challenge_frame_unobtained": "gui/sprites/advancements/challenge_frame_unobtained", - "changes": "gui/sprites/backup/changes", - "changes_highlighted": "gui/sprites/backup/changes_highlighted", "chat_modified": "gui/sprites/icon/chat_modified", "checkbox": "gui/sprites/widget/checkbox", "checkbox_highlighted": "gui/sprites/widget/checkbox_highlighted", "checkbox_selected": "gui/sprites/widget/checkbox_selected", "checkbox_selected_highlighted": "gui/sprites/widget/checkbox_selected_highlighted", "checkmark": "gui/sprites/icon/checkmark", - "cherry": "entity/chest_boat/cherry", + "cherry": "entity/signs/cherry", "cherry_0": "particle/cherry_0", "cherry_1": "particle/cherry_1", "cherry_10": "particle/cherry_10", @@ -1293,6 +1302,7 @@ "crafting_table_side": "crafting_table_side", "crafting_table_top": "crafting_table_top", "creamy": "entity/llama/creamy", + "credits_vignette": "misc/credits_vignette", "creebet": "painting/creebet", "creeper": "entity/banner/creeper", "creeper_armor": "entity/creeper/creeper_armor", @@ -1322,6 +1332,7 @@ "cut_red_sandstone": "cut_red_sandstone", "cut_sandstone": "cut_sandstone", "cyan": "entity/llama/decor/cyan", + "cyan_banner": "map/decorations/cyan_banner", "cyan_candle": "cyan_candle", "cyan_candle_lit": "cyan_candle_lit", "cyan_concrete": "cyan_concrete", @@ -1336,7 +1347,7 @@ "damaged_anvil_top": "damaged_anvil_top", "dandelion": "dandelion", "danger_pottery_pattern": "entity/decorated_pot/danger_pottery_pattern", - "dark_oak": "entity/chest_boat/dark_oak", + "dark_oak": "entity/signs/hanging/dark_oak", "dark_oak_door_bottom": "dark_oak_door_bottom", "dark_oak_door_top": "dark_oak_door_top", "dark_oak_leaves": "dark_oak_leaves", @@ -1384,6 +1395,7 @@ "deepslate_top": "deepslate_top", "demo_background": "gui/demo_background", "desert": "entity/villager/type/desert", + "desert_village": "map/decorations/desert_village", "destroy_stage_0": "destroy_stage_0", "destroy_stage_1": "destroy_stage_1", "destroy_stage_2": "destroy_stage_2", @@ -1554,6 +1566,9 @@ "fletching_table_front": "fletching_table_front", "fletching_table_side": "fletching_table_side", "fletching_table_top": "fletching_table_top", + "flow": "entity/banner/flow", + "flow_leggings": "trims/models/armor/flow_leggings", + "flow_pottery_pattern": "entity/decorated_pot/flow_pottery_pattern", "flower": "entity/banner/flower", "flower_pot": "flower_pot", "flowering_azalea_leaves": "flowering_azalea_leaves", @@ -1570,6 +1585,7 @@ "forcefield": "misc/forcefield", "fox": "entity/fox/fox", "fox_sleep": "entity/fox/fox_sleep", + "frame": "map/decorations/frame", "friend_pottery_pattern": "entity/decorated_pot/friend_pottery_pattern", "frogspawn": "frogspawn", "frosted_ice_0": "frosted_ice_0", @@ -1653,7 +1669,8 @@ "grass_block_snow": "grass_block_snow", "grass_block_top": "grass_block_top", "gravel": "gravel", - "gray": "entity/llama/gray", + "gray": "entity/llama/decor/gray", + "gray_banner": "map/decorations/gray_banner", "gray_candle": "gray_candle", "gray_candle_lit": "gray_candle_lit", "gray_concrete": "gray_concrete", @@ -1666,6 +1683,7 @@ "gray_wool": "gray_wool", "green": "entity/llama/decor/green", "green_background": "gui/sprites/boss_bar/green_background", + "green_banner": "map/decorations/green_banner", "green_candle": "green_candle", "green_candle_lit": "green_candle_lit", "green_concrete": "green_concrete", @@ -1696,6 +1714,8 @@ "gust_7": "particle/gust_7", "gust_8": "particle/gust_8", "gust_9": "particle/gust_9", + "guster": "entity/banner/guster", + "guster_pottery_pattern": "entity/decorated_pot/guster_pottery_pattern", "half": "gui/sprites/hud/heart/half", "half_blinking": "gui/sprites/hud/heart/half_blinking", "half_horizontal": "entity/banner/half_horizontal", @@ -1716,6 +1736,7 @@ "heart": "particle/heart", "heart_pottery_pattern": "entity/decorated_pot/heart_pottery_pattern", "heartbreak_pottery_pattern": "entity/decorated_pot/heartbreak_pottery_pattern", + "heavy_core": "heavy_core", "helmet_trim": "trims/items/helmet_trim", "hero_of_the_village": "mob_effect/hero_of_the_village", "hoglin": "entity/hoglin/hoglin", @@ -1763,6 +1784,7 @@ "ice": "ice", "illusioner": "entity/illager/illusioner", "incompatible": "gui/sprites/server_list/incompatible", + "infested": "mob_effect/infested", "info": "gui/sprites/icon/info", "inspiration": "gui/realms/inspiration", "instant_damage": "mob_effect/instant_damage", @@ -1770,6 +1792,10 @@ "inventory": "gui/container/inventory", "invisibility": "mob_effect/invisibility", "invite": "gui/sprites/icon/invite", + "inworld_footer_separator": "gui/inworld_footer_separator", + "inworld_header_separator": "gui/inworld_header_separator", + "inworld_menu_background": "gui/inworld_menu_background", + "inworld_menu_list_background": "gui/inworld_menu_list_background", "iron": "entity/villager/profession_level/iron", "iron_bars": "iron_bars", "iron_block": "iron_block", @@ -1805,7 +1831,7 @@ "jump_bar_cooldown": "gui/sprites/hud/jump_bar_cooldown", "jump_bar_progress": "gui/sprites/hud/jump_bar_progress", "jump_boost": "mob_effect/jump_boost", - "jungle": "entity/villager/type/jungle", + "jungle": "entity/signs/jungle", "jungle_door_bottom": "jungle_door_bottom", "jungle_door_top": "jungle_door_top", "jungle_leaves": "jungle_leaves", @@ -1813,6 +1839,7 @@ "jungle_log_top": "jungle_log_top", "jungle_planks": "jungle_planks", "jungle_sapling": "jungle_sapling", + "jungle_temple": "map/decorations/jungle_temple", "jungle_trapdoor": "jungle_trapdoor", "kai": "entity/player/wide/kai", "kebab": "painting/kebab", @@ -1851,6 +1878,7 @@ "levitation": "mob_effect/levitation", "librarian": "entity/villager/profession/librarian", "light_blue": "entity/llama/decor/light_blue", + "light_blue_banner": "map/decorations/light_blue_banner", "light_blue_candle": "light_blue_candle", "light_blue_candle_lit": "light_blue_candle_lit", "light_blue_concrete": "light_blue_concrete", @@ -1861,8 +1889,8 @@ "light_blue_stained_glass_pane_top": "light_blue_stained_glass_pane_top", "light_blue_terracotta": "light_blue_terracotta", "light_blue_wool": "light_blue_wool", - "light_dirt_background": "gui/light_dirt_background", "light_gray": "entity/llama/decor/light_gray", + "light_gray_banner": "map/decorations/light_gray_banner", "light_gray_candle": "light_gray_candle", "light_gray_candle_lit": "light_gray_candle_lit", "light_gray_concrete": "light_gray_concrete", @@ -1880,6 +1908,7 @@ "lily_of_the_valley": "lily_of_the_valley", "lily_pad": "lily_pad", "lime": "entity/llama/decor/lime", + "lime_banner": "map/decorations/lime_banner", "lime_candle": "lime_candle", "lime_candle_lit": "lime_candle_lit", "lime_concrete": "lime_concrete", @@ -1892,7 +1921,7 @@ "lime_wool": "lime_wool", "link": "gui/sprites/icon/link", "link_highlighted": "gui/sprites/icon/link_highlighted", - "lit_progress": "gui/sprites/container/furnace/lit_progress", + "lit_progress": "gui/sprites/container/blast_furnace/lit_progress", "llama_armor_slot": "gui/sprites/container/horse/llama_armor_slot", "locked": "gui/sprites/container/cartography_table/locked", "locked_button": "gui/sprites/widget/locked_button", @@ -1907,6 +1936,7 @@ "loom_top": "loom_top", "luck": "mob_effect/luck", "magenta": "entity/llama/decor/magenta", + "magenta_banner": "map/decorations/magenta_banner", "magenta_candle": "magenta_candle", "magenta_candle_lit": "magenta_candle_lit", "magenta_concrete": "magenta_concrete", @@ -1920,9 +1950,8 @@ "magma": "magma", "magmacube": "entity/slime/magmacube", "make_operator": "gui/sprites/player_list/make_operator", - "make_operator_highlighted": "gui/sprites/player_list/make_operator_highlighted", "makena": "entity/player/wide/makena", - "mangrove": "entity/chest_boat/mangrove", + "mangrove": "entity/signs/mangrove", "mangrove_door_bottom": "mangrove_door_bottom", "mangrove_door_top": "mangrove_door_top", "mangrove_leaves": "mangrove_leaves", @@ -1937,7 +1966,6 @@ "map": "gui/sprites/container/cartography_table/map", "map_background": "map/map_background", "map_background_checkerboard": "map/map_background_checkerboard", - "map_icons": "map/map_icons", "marked_join": "gui/sprites/world_list/marked_join", "marked_join_highlighted": "gui/sprites/world_list/marked_join_highlighted", "mason": "entity/villager/profession/mason", @@ -1946,6 +1974,8 @@ "melon_side": "melon_side", "melon_stem": "melon_stem", "melon_top": "melon_top", + "menu_background": "gui/menu_background", + "menu_list_background": "gui/menu_list_background", "minceraft": "gui/title/minceraft", "minecart": "entity/minecart", "minecraft": "gui/title/minecraft", @@ -1960,10 +1990,10 @@ "mossy_stone_bricks": "mossy_stone_bricks", "mourner_pottery_pattern": "entity/decorated_pot/mourner_pottery_pattern", "mouse": "gui/sprites/toast/mouse", - "move_down": "gui/sprites/server_list/move_down", - "move_down_highlighted": "gui/sprites/server_list/move_down_highlighted", - "move_up": "gui/sprites/server_list/move_up", - "move_up_highlighted": "gui/sprites/server_list/move_up_highlighted", + "move_down": "gui/sprites/transferable_list/move_down", + "move_down_highlighted": "gui/sprites/transferable_list/move_down_highlighted", + "move_up": "gui/sprites/transferable_list/move_up", + "move_up_highlighted": "gui/sprites/transferable_list/move_up_highlighted", "movement_keys": "gui/sprites/toast/movement_keys", "mud": "mud", "mud_bricks": "mud_bricks", @@ -1976,7 +2006,7 @@ "mute_button_highlighted": "gui/sprites/social_interactions/mute_button_highlighted", "mycelium_side": "mycelium_side", "mycelium_top": "mycelium_top", - "nausea": "misc/nausea", + "nausea": "mob_effect/nausea", "nautilus": "particle/nautilus", "nether": "gui/advancements/backgrounds/nether", "nether_bricks": "nether_bricks", @@ -2015,7 +2045,7 @@ "notched_6_progress": "gui/sprites/boss_bar/notched_6_progress", "note": "particle/note", "note_block": "note_block", - "oak": "entity/chest_boat/oak", + "oak": "entity/signs/hanging/oak", "oak_door_bottom": "oak_door_bottom", "oak_door_top": "oak_door_top", "oak_leaves": "oak_leaves", @@ -2030,13 +2060,16 @@ "observer_side": "observer_side", "observer_top": "observer_top", "obsidian": "obsidian", + "ocean_monument": "map/decorations/ocean_monument", "ocelot": "entity/cat/ocelot", "ochre_froglight_side": "ochre_froglight_side", "ochre_froglight_top": "ochre_froglight_top", + "ominous_spawning": "particle/ominous_spawning", + "oozing": "mob_effect/oozing", "open": "gui/sprites/realm_status/open", "open_eye": "entity/conduit/open_eye", - "options_background": "gui/options_background", "orange": "entity/llama/decor/orange", + "orange_banner": "map/decorations/orange_banner", "orange_candle": "orange_candle", "orange_candle_lit": "orange_candle_lit", "orange_concrete": "orange_concrete", @@ -2065,10 +2098,10 @@ "pack": null, "packed_ice": "packed_ice", "packed_mud": "packed_mud", - "page_backward": "gui/sprites/recipe_book/page_backward", - "page_backward_highlighted": "gui/sprites/recipe_book/page_backward_highlighted", - "page_forward": "gui/sprites/recipe_book/page_forward", - "page_forward_highlighted": "gui/sprites/recipe_book/page_forward_highlighted", + "page_backward": "gui/sprites/widget/page_backward", + "page_backward_highlighted": "gui/sprites/widget/page_backward_highlighted", + "page_forward": "gui/sprites/widget/page_forward", + "page_forward_highlighted": "gui/sprites/widget/page_forward_highlighted", "panda": "entity/panda/panda", "panorama_0": "gui/title/background/panorama_0", "panorama_1": "gui/title/background/panorama_1", @@ -2095,15 +2128,15 @@ "phantom_eyes": "entity/phantom_eyes", "pig": "entity/pig/pig", "pig_saddle": "entity/pig/pig_saddle", - "piglin": "entity/piglin/piglin", + "piglin": "entity/banner/piglin", "piglin_brute": "entity/piglin/piglin_brute", "pigscene": "painting/pigscene", "pillager": "entity/illager/pillager", - "ping_1": "gui/sprites/server_list/ping_1", - "ping_2": "gui/sprites/server_list/ping_2", - "ping_3": "gui/sprites/server_list/ping_3", - "ping_4": "gui/sprites/server_list/ping_4", - "ping_5": "gui/sprites/server_list/ping_5", + "ping_1": "gui/sprites/icon/ping_1", + "ping_2": "gui/sprites/icon/ping_2", + "ping_3": "gui/sprites/icon/ping_3", + "ping_4": "gui/sprites/icon/ping_4", + "ping_5": "gui/sprites/icon/ping_5", "ping_unknown": "gui/sprites/icon/ping_unknown", "pinging_1": "gui/sprites/server_list/pinging_1", "pinging_2": "gui/sprites/server_list/pinging_2", @@ -2112,6 +2145,7 @@ "pinging_5": "gui/sprites/server_list/pinging_5", "pink": "entity/llama/decor/pink", "pink_background": "gui/sprites/boss_bar/pink_background", + "pink_banner": "map/decorations/pink_banner", "pink_candle": "pink_candle", "pink_candle_lit": "pink_candle_lit", "pink_concrete": "pink_concrete", @@ -2141,7 +2175,11 @@ "pitcher_crop_top_stage_3": "pitcher_crop_top_stage_3", "pitcher_crop_top_stage_4": "pitcher_crop_top_stage_4", "plains": "entity/villager/type/plains", + "plains_village": "map/decorations/plains_village", "plant": "painting/plant", + "player": "map/decorations/player", + "player_off_limits": "map/decorations/player_off_limits", + "player_off_map": "map/decorations/player_off_map", "playful_panda": "entity/panda/playful_panda", "plenty_pottery_pattern": "entity/decorated_pot/plenty_pottery_pattern", "podzol_side": "podzol_side", @@ -2203,6 +2241,7 @@ "pumpkinblur": "misc/pumpkinblur", "purple": "entity/llama/decor/purple", "purple_background": "gui/sprites/boss_bar/purple_background", + "purple_banner": "map/decorations/purple_banner", "purple_candle": "purple_candle", "purple_candle_lit": "purple_candle_lit", "purple_concrete": "purple_concrete", @@ -2225,6 +2264,7 @@ "quartz_pillar": "quartz_pillar", "quartz_pillar_top": "quartz_pillar_top", "ragdoll": "entity/cat/ragdoll", + "raid_omen": "mob_effect/raid_omen", "rail": "rail", "rail_corner": "rail_corner", "rain": "environment/rain", @@ -2239,13 +2279,15 @@ "recipe_book": "gui/recipe_book", "recipe_highlighted": "gui/sprites/container/stonecutter/recipe_highlighted", "recipe_selected": "gui/sprites/container/stonecutter/recipe_selected", - "red": "entity/cat/red", + "red": "entity/llama/decor/red", "red_background": "gui/sprites/boss_bar/red_background", + "red_banner": "map/decorations/red_banner", "red_candle": "red_candle", "red_candle_lit": "red_candle_lit", "red_concrete": "red_concrete", "red_concrete_powder": "red_concrete_powder", "red_glazed_terracotta": "red_glazed_terracotta", + "red_marker": "map/decorations/red_marker", "red_mooshroom": "entity/cow/red_mooshroom", "red_mushroom": "red_mushroom", "red_mushroom_block": "red_mushroom_block", @@ -2261,6 +2303,7 @@ "red_terracotta": "red_terracotta", "red_tulip": "red_tulip", "red_wool": "red_wool", + "red_x": "map/decorations/red_x", "redstone": "trims/color_palettes/redstone", "redstone_block": "redstone_block", "redstone_dust_dot": "redstone_dust_dot", @@ -2279,9 +2322,7 @@ "reject": "gui/sprites/pending_invite/reject", "reject_highlighted": "gui/sprites/pending_invite/reject_highlighted", "remove_operator": "gui/sprites/player_list/remove_operator", - "remove_operator_highlighted": "gui/sprites/player_list/remove_operator_highlighted", "remove_player": "gui/sprites/player_list/remove_player", - "remove_player_highlighted": "gui/sprites/player_list/remove_player_highlighted", "repeater": "repeater", "repeater_on": "repeater_on", "repeating_command_block_back": "repeating_command_block_back", @@ -2300,8 +2341,6 @@ "respawn_anchor_side4": "respawn_anchor_side4", "respawn_anchor_top": "respawn_anchor_top", "respawn_anchor_top_off": "respawn_anchor_top_off", - "restore": "gui/sprites/backup/restore", - "restore_highlighted": "gui/sprites/backup/restore_highlighted", "rhombus": "entity/banner/rhombus", "rib": "trims/models/armor/rib", "rib_leggings": "trims/models/armor/rib_leggings", @@ -2318,14 +2357,17 @@ "sandstone_top": "sandstone_top", "saturation": "mob_effect/saturation", "savanna": "entity/villager/type/savanna", + "savanna_village": "map/decorations/savanna_village", "scaffolding_bottom": "scaffolding_bottom", "scaffolding_side": "scaffolding_side", "scaffolding_top": "scaffolding_top", "scaled_map": "gui/sprites/container/cartography_table/scaled_map", + "scrape_pottery_pattern": "entity/decorated_pot/scrape_pottery_pattern", "scroll_left": "gui/sprites/spectator/scroll_left", "scroll_right": "gui/sprites/spectator/scroll_right", "scroller": "gui/sprites/widget/scroller", - "scroller_disabled": "gui/sprites/container/creative_inventory/scroller_disabled", + "scroller_background": "gui/sprites/widget/scroller_background", + "scroller_disabled": "gui/sprites/container/loom/scroller_disabled", "sculk": "sculk", "sculk_catalyst_bottom": "sculk_catalyst_bottom", "sculk_catalyst_side": "sculk_catalyst_side", @@ -2459,6 +2501,13 @@ "small_dripleaf_stem_bottom": "small_dripleaf_stem_bottom", "small_dripleaf_stem_top": "small_dripleaf_stem_top", "small_dripleaf_top": "small_dripleaf_top", + "small_gust_0": "particle/small_gust_0", + "small_gust_1": "particle/small_gust_1", + "small_gust_2": "particle/small_gust_2", + "small_gust_3": "particle/small_gust_3", + "small_gust_4": "particle/small_gust_4", + "small_gust_5": "particle/small_gust_5", + "small_gust_6": "particle/small_gust_6", "small_stripes": "entity/banner/small_stripes", "smithing": "gui/container/smithing", "smithing_table_bottom": "smithing_table_bottom", @@ -2501,6 +2550,7 @@ "snow_fox": "entity/fox/snow_fox", "snow_fox_sleep": "entity/fox/snow_fox_sleep", "snow_golem": "entity/snow_golem", + "snowy_village": "map/decorations/snowy_village", "social_interactions": "gui/sprites/toast/social_interactions", "sonic_boom_0": "particle/sonic_boom_0", "sonic_boom_1": "particle/sonic_boom_1", @@ -2572,7 +2622,7 @@ "sponge": "sponge", "spore_blossom": "spore_blossom", "spore_blossom_base": "spore_blossom_base", - "spruce": "entity/chest_boat/spruce", + "spruce": "entity/signs/spruce", "spruce_door_bottom": "spruce_door_bottom", "spruce_door_top": "spruce_door_top", "spruce_leaves": "spruce_leaves", @@ -2656,6 +2706,7 @@ "suspicious_sand_2": "suspicious_sand_2", "suspicious_sand_3": "suspicious_sand_3", "swamp": "entity/villager/type/swamp", + "swamp_hut": "map/decorations/swamp_hut", "sweep_0": "particle/sweep_0", "sweep_1": "particle/sweep_1", "sweep_2": "particle/sweep_2", @@ -2669,7 +2720,7 @@ "sweet_berry_bush_stage2": "sweet_berry_bush_stage2", "sweet_berry_bush_stage3": "sweet_berry_bush_stage3", "system": "gui/sprites/toast/system", - "tab": "gui/sprites/recipe_book/tab", + "tab": "gui/sprites/widget/tab", "tab_above_left": "gui/sprites/advancements/tab_above_left", "tab_above_left_selected": "gui/sprites/advancements/tab_above_left_selected", "tab_above_middle": "gui/sprites/advancements/tab_above_middle", @@ -2696,6 +2747,7 @@ "tab_bottom_unselected_5": "gui/sprites/container/creative_inventory/tab_bottom_unselected_5", "tab_bottom_unselected_6": "gui/sprites/container/creative_inventory/tab_bottom_unselected_6", "tab_bottom_unselected_7": "gui/sprites/container/creative_inventory/tab_bottom_unselected_7", + "tab_header_background": "gui/tab_header_background", "tab_highlighted": "gui/sprites/widget/tab_highlighted", "tab_inventory": "gui/container/creative_inventory/tab_inventory", "tab_item_search": "gui/container/creative_inventory/tab_item_search", @@ -2712,7 +2764,7 @@ "tab_right_middle_selected": "gui/sprites/advancements/tab_right_middle_selected", "tab_right_top": "gui/sprites/advancements/tab_right_top", "tab_right_top_selected": "gui/sprites/advancements/tab_right_top_selected", - "tab_selected": "gui/sprites/recipe_book/tab_selected", + "tab_selected": "gui/sprites/widget/tab_selected", "tab_selected_highlighted": "gui/sprites/widget/tab_selected_highlighted", "tab_top_selected_1": "gui/sprites/container/creative_inventory/tab_top_selected_1", "tab_top_selected_2": "gui/sprites/container/creative_inventory/tab_top_selected_2", @@ -2731,12 +2783,15 @@ "tabby": "entity/cat/tabby", "tadpole": "entity/tadpole/tadpole", "taiga": "entity/villager/type/taiga", + "taiga_village": "map/decorations/taiga_village", "tall_grass_bottom": "tall_grass_bottom", "tall_grass_top": "tall_grass_top", "tall_seagrass_bottom": "tall_seagrass_bottom", "tall_seagrass_top": "tall_seagrass_top", + "target_point": "map/decorations/target_point", "target_side": "target_side", "target_top": "target_top", + "target_x": "map/decorations/target_x", "task_frame_obtained": "gui/sprites/advancements/task_frame_obtained", "task_frame_unobtained": "gui/sprites/advancements/task_frame_unobtained", "teleport_to_player": "gui/sprites/spectator/teleport_to_player", @@ -2768,17 +2823,29 @@ "trapped_right": "entity/chest/trapped_right", "tree": "gui/sprites/toast/tree", "trial_available": "gui/sprites/icon/trial_available", + "trial_chambers": "map/decorations/trial_chambers", + "trial_omen": "mob_effect/trial_omen", "trial_spawner_bottom": "trial_spawner_bottom", "trial_spawner_detection_0": "particle/trial_spawner_detection_0", "trial_spawner_detection_1": "particle/trial_spawner_detection_1", "trial_spawner_detection_2": "particle/trial_spawner_detection_2", "trial_spawner_detection_3": "particle/trial_spawner_detection_3", "trial_spawner_detection_4": "particle/trial_spawner_detection_4", + "trial_spawner_detection_ominous_0": "particle/trial_spawner_detection_ominous_0", + "trial_spawner_detection_ominous_1": "particle/trial_spawner_detection_ominous_1", + "trial_spawner_detection_ominous_2": "particle/trial_spawner_detection_ominous_2", + "trial_spawner_detection_ominous_3": "particle/trial_spawner_detection_ominous_3", + "trial_spawner_detection_ominous_4": "particle/trial_spawner_detection_ominous_4", "trial_spawner_side_active": "trial_spawner_side_active", + "trial_spawner_side_active_ominous": "trial_spawner_side_active_ominous", "trial_spawner_side_inactive": "trial_spawner_side_inactive", + "trial_spawner_side_inactive_ominous": "trial_spawner_side_inactive_ominous", "trial_spawner_top_active": "trial_spawner_top_active", + "trial_spawner_top_active_ominous": "trial_spawner_top_active_ominous", "trial_spawner_top_ejecting_reward": "trial_spawner_top_ejecting_reward", + "trial_spawner_top_ejecting_reward_ominous": "trial_spawner_top_ejecting_reward_ominous", "trial_spawner_top_inactive": "trial_spawner_top_inactive", + "trial_spawner_top_inactive_ominous": "trial_spawner_top_inactive_ominous", "triangle_bottom": "entity/banner/triangle_bottom", "triangle_top": "entity/banner/triangle_top", "triangles_bottom": "entity/banner/triangles_bottom", @@ -2829,6 +2896,23 @@ "unselect": "gui/sprites/transferable_list/unselect", "unselect_highlighted": "gui/sprites/transferable_list/unselect_highlighted", "upload": "gui/realms/upload", + "vault_bottom": "vault_bottom", + "vault_bottom_ominous": "vault_bottom_ominous", + "vault_connection": "particle/vault_connection", + "vault_front_ejecting": "vault_front_ejecting", + "vault_front_ejecting_ominous": "vault_front_ejecting_ominous", + "vault_front_off": "vault_front_off", + "vault_front_off_ominous": "vault_front_off_ominous", + "vault_front_on": "vault_front_on", + "vault_front_on_ominous": "vault_front_on_ominous", + "vault_side_off": "vault_side_off", + "vault_side_off_ominous": "vault_side_off_ominous", + "vault_side_on": "vault_side_on", + "vault_side_on_ominous": "vault_side_on_ominous", + "vault_top": "vault_top", + "vault_top_ejecting": "vault_top_ejecting", + "vault_top_ejecting_ominous": "vault_top_ejecting_ominous", + "vault_top_ominous": "vault_top_ominous", "vehicle_container": "gui/sprites/hud/heart/vehicle_container", "vehicle_full": "gui/sprites/hud/heart/vehicle_full", "vehicle_half": "gui/sprites/hud/heart/vehicle_half", @@ -2892,6 +2976,7 @@ "weathered_copper_grate": "weathered_copper_grate", "weathered_copper_trapdoor": "weathered_copper_trapdoor", "weathered_cut_copper": "weathered_cut_copper", + "weaving": "mob_effect/weaving", "weeping_vines": "weeping_vines", "weeping_vines_plant": "weeping_vines_plant", "wet_sponge": "wet_sponge", @@ -2903,8 +2988,9 @@ "wheat_stage5": "wheat_stage5", "wheat_stage6": "wheat_stage6", "wheat_stage7": "wheat_stage7", - "white": "entity/cat/white", + "white": "entity/llama/decor/white", "white_background": "gui/sprites/boss_bar/white_background", + "white_banner": "map/decorations/white_banner", "white_candle": "white_candle", "white_candle_lit": "white_candle_lit", "white_concrete": "white_concrete", @@ -2922,10 +3008,11 @@ "wild_leggings": "trims/models/armor/wild_leggings", "wind": "entity/conduit/wind", "wind_charge": "entity/projectiles/wind_charge", + "wind_charged": "mob_effect/wind_charged", "wind_vertical": "entity/conduit/wind_vertical", "window": "gui/advancements/window", "witch": "entity/witch", - "wither": "entity/wither/wither", + "wither": "mob_effect/wither", "wither_armor": "entity/wither/wither_armor", "wither_invulnerable": "entity/wither/wither_invulnerable", "wither_rose": "wither_rose", @@ -2940,13 +3027,44 @@ "withered_hardcore_half_blinking": "gui/sprites/hud/heart/withered_hardcore_half_blinking", "wolf": "entity/wolf/wolf", "wolf_angry": "entity/wolf/wolf_angry", + "wolf_armor": "entity/wolf/wolf_armor", + "wolf_armor_crackiness_high": "entity/wolf/wolf_armor_crackiness_high", + "wolf_armor_crackiness_low": "entity/wolf/wolf_armor_crackiness_low", + "wolf_armor_crackiness_medium": "entity/wolf/wolf_armor_crackiness_medium", + "wolf_armor_overlay": "entity/wolf/wolf_armor_overlay", + "wolf_ashen": "entity/wolf/wolf_ashen", + "wolf_ashen_angry": "entity/wolf/wolf_ashen_angry", + "wolf_ashen_tame": "entity/wolf/wolf_ashen_tame", + "wolf_black": "entity/wolf/wolf_black", + "wolf_black_angry": "entity/wolf/wolf_black_angry", + "wolf_black_tame": "entity/wolf/wolf_black_tame", + "wolf_chestnut": "entity/wolf/wolf_chestnut", + "wolf_chestnut_angry": "entity/wolf/wolf_chestnut_angry", + "wolf_chestnut_tame": "entity/wolf/wolf_chestnut_tame", "wolf_collar": "entity/wolf/wolf_collar", + "wolf_rusty": "entity/wolf/wolf_rusty", + "wolf_rusty_angry": "entity/wolf/wolf_rusty_angry", + "wolf_rusty_tame": "entity/wolf/wolf_rusty_tame", + "wolf_snowy": "entity/wolf/wolf_snowy", + "wolf_snowy_angry": "entity/wolf/wolf_snowy_angry", + "wolf_snowy_tame": "entity/wolf/wolf_snowy_tame", + "wolf_spotted": "entity/wolf/wolf_spotted", + "wolf_spotted_angry": "entity/wolf/wolf_spotted_angry", + "wolf_spotted_tame": "entity/wolf/wolf_spotted_tame", + "wolf_striped": "entity/wolf/wolf_striped", + "wolf_striped_angry": "entity/wolf/wolf_striped_angry", + "wolf_striped_tame": "entity/wolf/wolf_striped_tame", "wolf_tame": "entity/wolf/wolf_tame", + "wolf_woods": "entity/wolf/wolf_woods", + "wolf_woods_angry": "entity/wolf/wolf_woods_angry", + "wolf_woods_tame": "entity/wolf/wolf_woods_tame", "wood": "entity/armorstand/wood", "wooden_planks": "gui/sprites/toast/wooden_planks", + "woodland_mansion": "map/decorations/woodland_mansion", "worried_panda": "entity/panda/worried_panda", "yellow": "entity/llama/decor/yellow", "yellow_background": "gui/sprites/boss_bar/yellow_background", + "yellow_banner": "map/decorations/yellow_banner", "yellow_candle": "yellow_candle", "yellow_candle_lit": "yellow_candle_lit", "yellow_concrete": "yellow_concrete", @@ -4407,5 +4525,10 @@ "sunflower", "water_still", "water" + ], + "non_color_options" : [ + "Non-Color", + "Non-Colour Data", + "NONE" ] -} \ No newline at end of file +} diff --git a/MCprep_addon/__init__.py b/MCprep_addon/__init__.py index e727f3f8..474a4db7 100755 --- a/MCprep_addon/__init__.py +++ b/MCprep_addon/__init__.py @@ -41,7 +41,7 @@ bl_info = { "name": "MCprep", "category": "Object", - "version": (3, 5, 3), + "version": (3, 6, 0), "blender": (2, 80, 0), "location": "3D window toolshelf > MCprep tab", "description": "Minecraft workflow addon for rendering and animation", diff --git a/MCprep_addon/commonmcobj_parser.py b/MCprep_addon/commonmcobj_parser.py new file mode 100644 index 00000000..fd73cf33 --- /dev/null +++ b/MCprep_addon/commonmcobj_parser.py @@ -0,0 +1,253 @@ +# BSD 3-Clause License +# +# Copyright (c) 2024, Mahid Sheikh +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, this +# list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# 3. Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +# The parser is under a more permissive BSD 3-Clause license to make it easier +# for developers to use in non-GPL code. Normally, I wouldn't do dual licensing, +# but in this case, it makes sense as it would allow developers to reuse this +# parser for their own uses under more permissive terms. This doesn't change anything +# related to MCprep, which is GPL, as BSD 3-Clause is compatible with GPL. The +# only part that might conflict is Clause 3, but it could be argued that one +# can't do that under GPL anyway, or any license for that matter, and that +# Clause 3 is just a reminder to developers. +# +# - Mahid Sheikh + +from dataclasses import dataclass +from enum import Enum +from typing import List, Optional, Tuple, TextIO + +MAX_SUPPORTED_VERSION = 1 + +class CommonMCOBJTextureType(Enum): + ATLAS = "ATLAS" + INDIVIDUAL_TILES = "INDIVIDUAL_TILES" + +@dataclass +class CommonMCOBJ: + """ + Python representation of the CommonMCOBJ header + """ + # Version of the CommonMCOBJ spec + version: int + + # Exporter name in all lowercase + exporter: str + + # Name of source world + world_name: str + + # Path of source world* + world_path: str + + # Min values of the selection bounding box + export_bounds_min: Tuple[int, int, int] + + # Max values of the selection bounding box + export_bounds_max: Tuple[int, int, int] + + # Offset from (0, 0, 0) + export_offset: Tuple[float, float, float] + + # Scale of each block in meters; by default, this should be 1 meter + block_scale: float + + # Coordinate offset for blocks + block_origin_offset: Tuple[float, float, float] + + # Is the Z axis of the OBJ considered up? + z_up: bool + + # Are the textures using large texture atlases or + # individual textures? + texture_type: CommonMCOBJTextureType + + # Are blocks split by type? + has_split_blocks: bool + + # Original header + original_header: Optional[str] + + +def parse_common_header(header_lines: List[str]) -> CommonMCOBJ: + """ + Parses the CommonMCOBJ header information from a list of strings. + + header_lines list[str]: + list of strings representing each line of the header. + + returns: + CommonMCOBJ object + """ + + # Split at the colon and clean up formatting + def clean_and_extract(line: str) -> Tuple[str, str]: + split = line.split(':', 1) + pos = 0 + for i,x in enumerate(split[0]): + if x.isalpha(): + pos = i + break + return (split[0][pos:], split[1].strip()) + + # Basic values + header = CommonMCOBJ( + version=0, + exporter="NULL", + world_name="NULL", + world_path="NULL", + export_bounds_min=(0, 0, 0), + export_bounds_max=(0, 0, 0), + export_offset=(0, 0, 0), + block_scale=0, + block_origin_offset=(0, 0, 0), + z_up=False, + texture_type=CommonMCOBJTextureType.ATLAS, + has_split_blocks=False, + original_header=None + ) + + # Keys whose values do not need extra processing + NO_VALUE_PARSE = [ + "exporter", + "world_name", + "world_path", + ] + + # Keys whose values are tuples + TUPLE_PARSE_INT = [ + "export_bounds_min", + "export_bounds_max", + ] + + TUPLE_PARSE_FLOAT = [ + "export_offset", + "block_origin_offset" + ] + + # Keys whose values are booleans + BOOLEAN_PARSE = [ + "z_up", + "has_split_blocks" + ] + + # Although CommonMCOBJ states that + # order does matter in the header, + # future versions may change the order + # of some values, so it's best to + # use a non-order specific parser + for line in header_lines: + if ":" not in line: + continue + key, value = clean_and_extract(line) + + if key == "version": + try: + header.version = int(value) + if header.version > MAX_SUPPORTED_VERSION: + header.original_header = "\n".join(header_lines) + except Exception: + pass + + elif key == "block_scale": + try: + header.block_scale = float(value) + except Exception: + pass + + elif key == "texture_type": + try: + header.texture_type = CommonMCOBJTextureType[value] + except Exception: + pass + + # All of these are parsed the same, with + # no parsing need to value + # + # No keys here will be classed as failed + elif key in NO_VALUE_PARSE: + setattr(header, key, value) + + # All of these are parsed the same, with + # parsing the value to a tuple + elif key in TUPLE_PARSE_INT: + try: + setattr(header, key, tuple(map(int, value[1:-1].split(', ')))) + except Exception: + pass + + elif key in TUPLE_PARSE_FLOAT: + try: + setattr(header, key, tuple(map(float, value[1:-1].split(', ')))) + except Exception: + pass + + elif key in BOOLEAN_PARSE: + try: + setattr(header, key, value == "true") + except Exception: + pass + + return header + + +def parse_header(f: TextIO) -> Optional[CommonMCOBJ]: + """ + Parses a file and returns a CommonMCOBJ object if the header exists. + + f: TextIO + File object + + Returns: + - CommonMCOBJ object if header exists + - None otherwise + """ + + header: List[str] = [] + found_header = False + + # Read in the header + lines_read = 0 + for _l in f: + tl = " ".join(_l.rstrip().split()) + lines_read += 1 + if lines_read > 100 and tl and not tl.startswith("#"): + break # no need to parse further than the true header area + elif tl == "# COMMON_MC_OBJ_START": + header.append(tl) + found_header = True + continue + elif tl == "# COMMON_MC_OBJ_END": + header.append(tl) + break + if not found_header or tl == "#": + continue + header.append(tl) + if not len(header): + return None + return parse_common_header(header) diff --git a/MCprep_addon/conf.py b/MCprep_addon/conf.py index 5bb1bd0e..2038d430 100644 --- a/MCprep_addon/conf.py +++ b/MCprep_addon/conf.py @@ -18,9 +18,12 @@ from mathutils import Vector from pathlib import Path -from typing import Union, Tuple, List, Dict +from typing import Optional, Union, Tuple, List, Dict +import inspect +from dataclasses import dataclass import enum import os +import gettext import bpy from bpy.utils.previews import ImagePreviewCollection @@ -41,9 +44,11 @@ class Form(enum.Enum): class Engine(enum.Enum): """String exact match to output from blender itself for branching.""" CYCLES = "CYCLES" + + # Blender 2.8 to 4.1 BLENDER_EEVEE = "BLENDER_EEVEE" - # EEVEE Next is the next generation EEVEE. So in preperation for that, - # we've added "BLENDER_EEVEE_NEXT" as an Engine option + + # Blender 4.2 and beyond BLENDER_EEVEE_NEXT = "BLENDER_EEVEE_NEXT" @@ -52,6 +57,12 @@ class Engine(enum.Enum): Skin = Tuple[str, Path] Entity = Tuple[str, str, str] +# Represents an unknown location +# for MCprepError. Given a global +# constant to make it easier to use +# and check for +UNKNOWN_LOCATION = (-1, "UNKNOWN LOCATION") + # check if custom preview icons available try: import bpy.utils.previews @@ -68,11 +79,13 @@ class Engine(enum.Enum): class MCprepEnv: def __init__(self): self.data = None - self.json_data = None + self.json_data: Optional[Dict] = None self.json_path: Path = Path(os.path.dirname(__file__), "MCprep_resources", "mcprep_data.json") self.json_path_update: Path = Path(os.path.dirname(__file__), "MCprep_resources", "mcprep_data_update.json") self.dev_file: Path = Path(os.path.dirname(__file__), "mcprep_dev.txt") + self.languages_folder: Path = Path(os.path.dirname(__file__), "MCprep_resources", "Languages") + self.translations: Path = Path(os.path.dirname(__file__), "translations.py") self.last_check_for_updated = 0 @@ -122,7 +135,31 @@ def __init__(self): # list of material names, each is a string. None by default to indicate # that no reading has occurred. If lib not found, will update to []. # If ever changing the resource pack, should also reset to None. - self.material_sync_cache = [] + self.material_sync_cache: List = [] + + # Whether we use PO files directly or use the converted form + self.use_direct_i18n = False + # i18n using Python's gettext module + # + # This only runs if translations.py does not exist + if not self.translations.exists(): + self.languages: dict[str, gettext.NullTranslations] = {} + for language in self.languages_folder.iterdir(): + self.languages[language.name] = gettext.translation("mcprep", + self.languages_folder, + fallback=True, + languages=[language.name]) + self.use_direct_i18n = True + self.log("Loaded direct i18n!") + + # This allows us to translate strings on the fly + def _(self, msg: str) -> str: + if not self.use_direct_i18n: + return msg + if bpy.context.preferences.view.language in self.languages: + return self.languages[bpy.context.preferences.view.language].gettext(msg) + else: + return self.languages["en_US"].gettext(msg) def update_json_dat_path(self): """If new update file found from install, replace old one with new. @@ -207,6 +244,77 @@ def deprecation_warning(self): self.log("Deprecation Warning: This will be removed in MCprep 3.5.1!") traceback.print_stack() + def current_line_and_file(self) -> Tuple[int, str]: + """ + Wrapper to get the current line number and file path for + MCprepError. + + This function can not return an MCprepError value as doing + so would be more complicated for the caller. As such, if + this fails, we return values -1 and "UNKNOWN LOCATION" to + indicate that we do not know the line number or file path + the error occured on. + + Returns: + - If success: Tuple[int, str] representing the current + line and file path + + - If fail: (-1, "UNKNOWN LOCATION") + """ + # currentframe can return a None value + # in certain cases + cur_frame = inspect.currentframe() + if not cur_frame: + return UNKNOWN_LOCATION + + # Get the previous frame since the + # current frame is made for this function, + # not the function/code that called + # this function + prev_frame = cur_frame.f_back + if not prev_frame: + return UNKNOWN_LOCATION + + frame_info = inspect.getframeinfo(prev_frame) + return frame_info.lineno, frame_info.filename + +@dataclass +class MCprepError(object): + """ + Object that is returned when + an error occurs. This is meant + to give more information to the + caller so that a better error + message can be made + + Attributes + ------------ + err_type: BaseException + The error type; uses standard + Python exceptions + + + line: int + Line the exception object was + created on. The preferred method + to do this is to use currentframe + and getframeinfo from the inspect + module + + file: str + Path of file the exception object + was created in. The preferred way + to get this is __file__ + + msg: Optional[str] + Optional message to display for an + exception. Use this if the exception + type may not be so clear cut + """ + err_type: BaseException + line: int + file: str + msg: Optional[str] = None env = MCprepEnv() diff --git a/MCprep_addon/load_modules.py b/MCprep_addon/load_modules.py index 5fc71c79..e5841f38 100644 --- a/MCprep_addon/load_modules.py +++ b/MCprep_addon/load_modules.py @@ -47,11 +47,6 @@ else: from .materials import prep -if "optimize_scene" in locals(): - importlib.reload(optimize_scene) -else: - from . import optimize_scene - if "sequences" in locals(): importlib.reload(sequences) else: @@ -147,7 +142,6 @@ else: from .materials import generate - # Only include those with a register function, which is not all module_list = ( conf, @@ -168,11 +162,14 @@ world_tools, # bridge, mcprep_ui, - optimize_scene ) def register(bl_info): + if not conf.env.use_direct_i18n: + from . import translations + bpy.app.translations.register(__name__, translations.MCPREP_TRANSLATIONS) + tracking.register(bl_info) for mod in module_list: mod.register() @@ -187,6 +184,7 @@ def register(bl_info): conf.env.log("MCprep: Very Verbose is enabled", vv_only=True) + def unregister(bl_info): for mod in reversed(module_list): mod.unregister() @@ -196,3 +194,5 @@ def unregister(bl_info): # addon updater code and configurations addon_updater_ops.unregister() + if not conf.env.use_direct_i18n: + bpy.app.translations.unregister(__name__) diff --git a/MCprep_addon/materials/default_materials.py b/MCprep_addon/materials/default_materials.py deleted file mode 100644 index 7b2a6909..00000000 --- a/MCprep_addon/materials/default_materials.py +++ /dev/null @@ -1,219 +0,0 @@ -# ##### BEGIN GPL LICENSE BLOCK ##### -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software Foundation, -# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. -# -# ##### END GPL LICENSE BLOCK ##### - - -import os -from typing import Union, Optional - -import bpy -from bpy.types import Context, Material - -from .. import tracking -from .. import util -from . import sync - -from ..conf import env, Engine - - -def default_material_in_sync_library(default_material: str, context: Context) -> bool: - """Returns true if the material is in the sync mat library blend file.""" - if env.material_sync_cache is None: - sync.reload_material_sync_library(context) - if util.nameGeneralize(default_material) in env.material_sync_cache: - return True - elif default_material in env.material_sync_cache: - return True - return False - - -def sync_default_material(context: Context, material: Material, default_material: str, engine: Engine) -> Optional[Union[Material, str]]: - """Normal sync material method but with duplication and name change.""" - if default_material in env.material_sync_cache: - import_name = default_material - elif util.nameGeneralize(default_material) in env.material_sync_cache: - import_name = util.nameGeneralize(default_material) - - # If link is true, check library material not already linked. - sync_file = sync.get_sync_blend(context) - - init_mats = list(bpy.data.materials) - path = os.path.join(sync_file, "Material") - util.bAppendLink(path, import_name, False) # No linking. - - imported = set(list(bpy.data.materials)) - set(init_mats) - if not imported: - return f"Could not import {material.name}" - new_default_material = list(imported)[0] - - # Checking if there's a node with the label Texture. - new_material_nodes = new_default_material.node_tree.nodes - if not new_material_nodes.get("MCPREP_diffuse"): - return "Material has no MCPREP_diffuse node" - - if not material.node_tree.nodes: - return "Material has no nodes" - - # Change the texture. - new_default_material_nodes = new_default_material.node_tree.nodes - material_nodes = material.node_tree.nodes - - if not material_nodes.get("Image Texture"): - return "Material has no Image Texture node" - - default_texture_node = new_default_material_nodes.get("MCPREP_diffuse") - image_texture = material_nodes.get("Image Texture").image.name - texture_file = bpy.data.images.get(image_texture) - default_texture_node.image = texture_file - - if engine == "CYCLES" or engine == "BLENDER_EEVEE": - default_texture_node.interpolation = 'Closest' - - material.user_remap(new_default_material) - - # remove the old material since we're changing the default and we don't - # want to overwhelm users - bpy.data.materials.remove(material) - new_default_material.name = material.name - return None - - -class MCPREP_OT_default_material(bpy.types.Operator): - bl_idname = "mcprep.sync_default_materials" - bl_label = "Sync Default Materials" - bl_options = {'REGISTER', 'UNDO'} - - use_pbr: bpy.props.BoolProperty( - name="Use PBR", - description="Use PBR or not", - default=False) - - engine: bpy.props.StringProperty( - name="engine To Use", - description="Defines the engine to use", - default="CYCLES") - - SIMPLE = "simple" - PBR = "pbr" - - track_function = "sync_default_materials" - track_param = None - @tracking.report_error - def execute(self, context): - # Sync file stuff. - sync_file = sync.get_sync_blend(context) - if not os.path.isfile(sync_file): - self.report({'ERROR'}, f"Sync file not found: {sync_file}") - return {'CANCELLED'} - - if sync_file == bpy.data.filepath: - return {'CANCELLED'} - - # Find the default material. - workflow = self.SIMPLE if not self.use_pbr else self.PBR - material_name = material_name = f"default_{workflow}_{self.engine.lower()}" - if not default_material_in_sync_library(material_name, context): - self.report({'ERROR'}, "No default material found") - return {'CANCELLED'} - - # Sync materials. - mat_list = list(bpy.data.materials) - for mat in mat_list: - try: - err = sync_default_material(context, mat, material_name, self.engine.upper()) # no linking - if err: - env.log(err) - except Exception as e: - print(e) - - return {'FINISHED'} - - -class MCPREP_OT_create_default_material(bpy.types.Operator): - bl_idname = "mcprep.create_default_material" - bl_label = "Create Default Material" - bl_options = {'REGISTER', 'UNDO'} - - SIMPLE = "simple" - PBR = "pbr" - - def execute(self, context): - engine = context.scene.render.engine - self.create_default_material(context, engine, "simple") - return {'FINISHED'} - - def create_default_material(self, context, engine, type): - """ - create_default_material: takes 3 arguments and returns nothing - context: Blender Context - engine: the render engine - type: the type of texture that's being dealt with - """ - if not len(bpy.context.selected_objects): - # If there's no selected objects. - self.report({'ERROR'}, "Select an object to create the material") - return - - material_name = f"default_{type}_{engine.lower()}" - default_material = bpy.data.materials.new(name=material_name) - default_material.use_nodes = True - nodes = default_material.node_tree.nodes - links = default_material.node_tree.links - nodes.clear() - - default_texture_node = nodes.new(type="ShaderNodeTexImage") - principled = nodes.new("ShaderNodeBsdfPrincipled") - nodeOut = nodes.new("ShaderNodeOutputMaterial") - - default_texture_node.name = "MCPREP_diffuse" - default_texture_node.label = "Diffuse Texture" - default_texture_node.location = (120, 0) - - principled.inputs["Specular"].default_value = 0 - principled.location = (600, 0) - - nodeOut.location = (820, 0) - - links.new(default_texture_node.outputs[0], principled.inputs[0]) - links.new(principled.outputs["BSDF"], nodeOut.inputs[0]) - - if engine == "EEVEE": - if hasattr(default_material, "blend_method"): - default_material.blend_method = 'HASHED' - if hasattr(default_material, "shadow_method"): - default_material.shadow_method = 'HASHED' - - -classes = ( - MCPREP_OT_default_material, - MCPREP_OT_create_default_material, -) - - -def register(): - for cls in classes: - bpy.utils.register_class(cls) - bpy.app.handlers.load_post.append(sync.clear_sync_cache) - - -def unregister(): - for cls in reversed(classes): - bpy.utils.unregister_class(cls) - try: - bpy.app.handlers.load_post.remove(sync.clear_sync_cache) - except: - pass diff --git a/MCprep_addon/materials/generate.py b/MCprep_addon/materials/generate.py index 86b58866..5dc54375 100644 --- a/MCprep_addon/materials/generate.py +++ b/MCprep_addon/materials/generate.py @@ -17,7 +17,7 @@ # ##### END GPL LICENSE BLOCK ##### import os -from typing import Dict, Optional, List, Any, Tuple, Union +from typing import Dict, Optional, List, Any, Tuple, Union, cast from pathlib import Path from dataclasses import dataclass from enum import Enum @@ -26,10 +26,15 @@ from bpy.types import Context, Material, Image, Texture, Nodes, NodeLinks, Node from .. import util -from ..conf import env, Form +from ..conf import MCprepError, env, Form AnimatedTex = Dict[str, int] +# Error codes used during mat prep +NO_DIFFUSE_NODE = 1 +IMG_MISSING = 2 + + class PackFormat(Enum): SIMPLE = 0 SEUS = 1 @@ -125,43 +130,51 @@ def get_mc_canonical_name(name: str) -> Tuple[str, Optional[Form]]: return canon, form -def find_from_texturepack(blockname: str, resource_folder: Optional[Path]=None) -> Path: +def find_from_texturepack(blockname: str, resource_folder: Optional[Path]=None) -> Union[Path, MCprepError]: """Given a blockname (and resource folder), find image filepath. Finds textures following any pack which should have this structure, and the input folder or default resource folder could target at any of the following sublevels above the level. //pack_name/assets/minecraft/textures// + + Returns: + - Path if successful + - MCprepError if error occurs (may return with a message) """ - if not resource_folder: + if resource_folder is None: # default to internal pack - resource_folder = bpy.path.abspath(bpy.context.scene.mcprep_texturepack_path) + resource_folder = Path(cast( + str, + bpy.path.abspath(bpy.context.scene.mcprep_texturepack_path) + )) - if not os.path.isdir(resource_folder): + if not resource_folder.exists() or not resource_folder.is_dir(): env.log("Error, resource folder does not exist") - return + line, file = env.current_line_and_file() + return MCprepError(FileNotFoundError(), line, file, f"Resource pack folder at {resource_folder} does not exist!") # Check multiple paths, picking the first match (order is important), # goal of picking out the /textures folder. check_dirs = [ - os.path.join(resource_folder, "textures"), - os.path.join(resource_folder, "minecraft", "textures"), - os.path.join(resource_folder, "assets", "minecraft", "textures")] + Path(resource_folder, "textures"), + Path(resource_folder, "minecraft", "textures"), + Path(resource_folder, "assets", "minecraft", "textures")] for path in check_dirs: - if os.path.isdir(path): + if path.exists(): resource_folder = path break search_paths = [ resource_folder, # Both singular and plural shown below as it has varied historically. - os.path.join(resource_folder, "blocks"), - os.path.join(resource_folder, "block"), - os.path.join(resource_folder, "items"), - os.path.join(resource_folder, "item"), - os.path.join(resource_folder, "entity"), - os.path.join(resource_folder, "models"), - os.path.join(resource_folder, "model"), + Path(resource_folder, "blocks"), + Path(resource_folder, "block"), + Path(resource_folder, "items"), + Path(resource_folder, "item"), + Path(resource_folder, "entity"), + Path(resource_folder, "models"), + Path(resource_folder, "model"), ] res = None @@ -170,32 +183,36 @@ def find_from_texturepack(blockname: str, resource_folder: Optional[Path]=None) if "/" in blockname: newpath = blockname.replace("/", os.path.sep) for ext in extensions: - if os.path.isfile(os.path.join(resource_folder, newpath + ext)): - res = os.path.join(resource_folder, newpath + ext) + if Path(resource_folder, newpath + ext).exists(): + res = Path(resource_folder, newpath + ext) return res newpath = os.path.basename(blockname) # case where goes into other subpaths for ext in extensions: - if os.path.isfile(os.path.join(resource_folder, newpath + ext)): - res = os.path.join(resource_folder, newpath + ext) + if Path(resource_folder, newpath + ext).exists(): + res = Path(resource_folder, newpath + ext) return res # fallback (more common case), wide-search for for path in search_paths: - if not os.path.isdir(path): + if not path.is_dir(): continue for ext in extensions: - check_path = os.path.join(path, blockname + ext) - if os.path.isfile(check_path): - res = os.path.join(path, blockname + ext) + check_path = Path(path, blockname + ext) + if check_path.exists() and check_path.is_file(): + res = Path(path, blockname + ext) return res + # Mineways fallback for suffix in ["-Alpha", "-RGB", "-RGBA"]: if blockname.endswith(suffix): - res = os.path.join( + res = Path( resource_folder, "mineways_assets", f"mineways{suffix}.png") - if os.path.isfile(res): + if res.exists() and res.is_file(): return res + if res is None: + line, file = env.current_line_and_file() + return MCprepError(FileNotFoundError(), line, file) return res @@ -204,6 +221,13 @@ def detect_form(materials: List[Material]) -> Optional[Form]: Useful for pre-determining elibibility of a function and also for tracking reporting to give sense of how common which exporter is used. + + materials: List[Material]: + List of materials to check from + + Returns: + - Form if detected + - None if not detected """ jmc2obj = 0 mc = 0 @@ -344,10 +368,12 @@ def set_texture_pack( """ mc_name, _ = get_mc_canonical_name(material.name) image = find_from_texturepack(mc_name, folder) - if image is None: + if isinstance(image, MCprepError): + if image.msg: + env.log(image.msg) return 0 - image_data = util.loadTexture(image) + image_data = util.loadTexture(str(image)) _ = set_cycles_texture( image_data, material, extra_passes=use_extra_passes) return 1 @@ -392,7 +418,7 @@ def set_cycles_texture( # check if there is more data to see pass types img_sets = {} if extra_passes: - img_sets = find_additional_passes(image.filepath) + img_sets = find_additional_passes(Path(image.filepath)) changed = False is_grayscale = False @@ -419,9 +445,12 @@ def set_cycles_texture( if "normal" in img_sets: new_img = util.loadTexture(img_sets["normal"]) node.image = new_img - util.apply_colorspace(node, 'Non-Color') node.mute = False node.hide = False + + res = util.apply_noncolor_data(node) + if res is not None: + env.log(f"TypeError on {res.line} in {res.file}: {res.err_type}") else: node.mute = True node.hide = True @@ -436,7 +465,9 @@ def set_cycles_texture( node.image = new_img node.mute = False node.hide = False - util.apply_colorspace(node, 'Non-Color') + res = util.apply_noncolor_data(node) + if res is not None: + env.log(f"TypeError on {res.line} in {res.file}: {res.err_type}") else: node.mute = True node.hide = True @@ -557,7 +588,8 @@ def get_textures(material: Material) -> Dict[str, Image]: def find_additional_passes(image_file: Path) -> Dict[str, Image]: """Find relevant passes like normal and spec in same folder as image.""" - abs_img_file = bpy.path.abspath(image_file) + print("What is this?", image_file) + abs_img_file = bpy.path.abspath(str(image_file)) # needs to be blend file relative env.log(f"\tFind additional passes for: {image_file}", vv_only=True) if not os.path.isfile(abs_img_file): return {} @@ -625,18 +657,23 @@ def replace_missing_texture(image: Image) -> bool: env.log(f"Missing datablock detected: {image.name}") name = image.name - if len(name) > 4 and name[-4] == ".": - name = name[:-4] # cuts off e.g. .png - elif len(name) > 5 and name[-5] == ".": - name = name[:-5] # cuts off e.g. .jpeg + name = os.path.splitext(name)[0] # cut off png / jpg / etc canon, _ = get_mc_canonical_name(name) # TODO: detect for pass structure like normal and still look for right pass image_path = find_from_texturepack(canon) - if not image_path: + if isinstance(image_path, MCprepError): + if image_path.msg: + env.log(image_path.msg) return False - image.filepath = image_path + image.filepath = str(image_path) # image.reload() # not needed? # pack? + # Due to the issue below, must trick blender to reload the datablock + # https://projects.blender.org/blender/blender/issues/115984 + if image.source == 'FILE' and image.source != 'SEQUENCE': + old = image.source + image.source = 'SEQUENCE' + image.source = old return True # updated image block @@ -971,8 +1008,12 @@ def texgen_specular(mat: Material, passes: Dict[str, Image], nodeInputs: List, u nodeNormal.mute = True # Update to use non-color data for spec and normal - util.apply_colorspace(nodeTexSpec, 'Non-Color') - util.apply_colorspace(nodeTexNorm, 'Non-Color') + res = util.apply_noncolor_data(nodeTexSpec) + if res is not None: + env.log(f"TypeError on {res.line} in {res.file}: {res.err_type}") + res = util.apply_noncolor_data(nodeTexNorm) + if res is not None: + env.log(f"TypeError on {res.line} in {res.file}: {res.err_type}") # Graystyle Blending if not checklist(canon, "desaturated"): @@ -1113,8 +1154,12 @@ def texgen_seus(mat: Material, passes: Dict[str, Image], nodeInputs: List, use_r nodeNormal.mute = True # Update to use non-color data for spec and normal - util.apply_colorspace(nodeTexSpec, 'Non-Color') - util.apply_colorspace(nodeTexNorm, 'Non-Color') + res = util.apply_noncolor_data(nodeTexSpec) + if res is not None: + env.log(f"TypeError on {res.line} in {res.file}: {res.err_type}") + res = util.apply_noncolor_data(nodeTexNorm) + if res is not None: + env.log(f"TypeError on {res.line} in {res.file}: {res.err_type}") # Graystyle Blending if not checklist(canon, "desaturated"): @@ -1156,7 +1201,7 @@ def generate_base_material( mat = bpy.data.materials.new(name=name) engine = context.scene.render.engine - if engine in ['CYCLES', 'BLENDER_EEVEE']: + if engine in ['CYCLES', 'BLENDER_EEVEE', 'BLENDER_EEVEE_NEXT']: # need to create at least one texture node first, then the rest works mat.use_nodes = True nodes = mat.node_tree.nodes @@ -1204,16 +1249,16 @@ def matgen_cycles_simple(mat: Material, options: PrepOptions) -> Optional[bool]: if not image_diff: print(f"Could not find diffuse image, halting generation: {mat.name}") - return + return NO_DIFFUSE_NODE elif image_diff.size[0] == 0 or image_diff.size[1] == 0: if image_diff.source != 'SEQUENCE': # Common non animated case; this means the image is missing and would # have already checked for replacement textures by now, so skip. - return + return IMG_MISSING if not os.path.isfile(bpy.path.abspath(image_diff.filepath)): # can't check size or pixels as it often is not immediately avaialble # so instead, check against firs frame of sequence to verify load - return + return IMG_MISSING mat.use_nodes = True animated_data = copy_texture_animation_pass_settings(mat) @@ -1324,16 +1369,16 @@ def matgen_cycles_principled(mat: Material, options: PrepOptions) -> Optional[bo if not image_diff: print(f"Could not find diffuse image, halting generation: {mat.name}") - return + return NO_DIFFUSE_NODE elif image_diff.size[0] == 0 or image_diff.size[1] == 0: if image_diff.source != 'SEQUENCE': # Common non animated case; this means the image is missing and would # have already checked for replacement textures by now, so skip - return + return IMG_MISSING if not os.path.isfile(bpy.path.abspath(image_diff.filepath)): # can't check size or pixels as it often is not immediately avaialble # so instead, check against first frame of sequence to verify load - return + return IMG_MISSING mat.use_nodes = True animated_data = copy_texture_animation_pass_settings(mat) @@ -1352,7 +1397,7 @@ def matgen_cycles_principled(mat: Material, options: PrepOptions) -> Optional[bo # Sets default transparency value nodeMixTrans.inputs[0].default_value = 1 - + # Sets default reflective values if options.use_reflections and checklist(canon, "reflective"): principled.inputs["Roughness"].default_value = 0 @@ -1372,11 +1417,11 @@ def matgen_cycles_principled(mat: Material, options: PrepOptions) -> Optional[bo links.new(nodeMixTrans.outputs["Shader"], nodeOut.inputs[0]) nodeEmit = create_node( - nodes, "ShaderNodeEmission", location=(120, 140)) + nodes, "ShaderNodeEmission", location=(120, 140)) nodeEmitCam = create_node( - nodes, "ShaderNodeEmission", location=(120, 260)) + nodes, "ShaderNodeEmission", location=(120, 260)) nodeMixEmit = create_node( - nodes, "ShaderNodeMixShader", location=(420, 0)) + nodes, "ShaderNodeMixShader", location=(420, 0)) if options.use_emission_nodes: # Create emission nodes nodeMixCam = create_node( @@ -1385,7 +1430,7 @@ def matgen_cycles_principled(mat: Material, options: PrepOptions) -> Optional[bo nodes, "ShaderNodeLightFalloff", location=(-80, 320)) nodeLightPath = create_node( nodes, "ShaderNodeLightPath", location=(-320, 520)) - + # Set values nodeFalloff.inputs["Strength"].default_value = 32 nodeEmitCam.inputs["Strength"].default_value = 4 @@ -1406,7 +1451,6 @@ def matgen_cycles_principled(mat: Material, options: PrepOptions) -> Optional[bo else: links.new(principled.outputs[0], nodeMixTrans.inputs[2]) - nodeInputs = [ [ principled.inputs["Base Color"], @@ -1418,7 +1462,7 @@ def matgen_cycles_principled(mat: Material, options: PrepOptions) -> Optional[bo [principled.inputs["Metallic"]], [principled.inputs["Specular IOR Level" if util.min_bv((4, 0, 0)) else "Specular"]], [principled.inputs["Normal"]]] - + if not options.use_emission_nodes: nodes.remove(nodeEmit) nodes.remove(nodeEmitCam) @@ -1480,16 +1524,16 @@ def matgen_cycles_original(mat: Material, options: PrepOptions): if not image_diff: print(f"Could not find diffuse image, halting generation: {mat.name}") - return + return NO_DIFFUSE_NODE elif image_diff.size[0] == 0 or image_diff.size[1] == 0: if image_diff.source != 'SEQUENCE': # Common non animated case; this means the image is missing and would # have already checked for replacement textures by now, so skip - return + return IMG_MISSING if not os.path.isfile(bpy.path.abspath(image_diff.filepath)): # can't check size or pixels as it often is not immediately avaialble # so instea, check against firs frame of sequence to verify load - return + return IMG_MISSING mat.use_nodes = True animated_data = copy_texture_animation_pass_settings(mat) @@ -1795,7 +1839,9 @@ def matgen_special_water(mat: Material, passes: Dict[str, Image]) -> Optional[bo links.new(nodeNormal.outputs[0], nodeGlass.inputs[3]) # Normal update - util.apply_colorspace(nodeTexNorm, 'Non-Color') + res = util.apply_noncolor_data(nodeTexNorm) + if res is not None: + env.log(f"TypeError on {res.line} in {res.file}: {res.err_type}") if image_norm: nodeTexNorm.image = image_norm nodeTexNorm.mute = False @@ -1929,7 +1975,9 @@ def matgen_special_glass(mat: Material, passes: Dict[str, Image]) -> Optional[bo links.new(nodeNormal.outputs[0], nodeDiff.inputs[2]) # Normal update - util.apply_colorspace(nodeTexNorm, 'Non-Color') + res = util.apply_noncolor_data(nodeTexNorm) + if res is not None: + env.log(f"TypeError on {res.line} in {res.file}: {res.err_type}") if image_norm: nodeTexNorm.image = image_norm nodeTexNorm.mute = False diff --git a/MCprep_addon/materials/material_manager.py b/MCprep_addon/materials/material_manager.py index 6a47f794..0b96ee69 100644 --- a/MCprep_addon/materials/material_manager.py +++ b/MCprep_addon/materials/material_manager.py @@ -26,7 +26,7 @@ from .. import tracking from .. import util -from ..conf import env +from ..conf import MCprepError, env # ----------------------------------------------------------------------------- @@ -431,6 +431,8 @@ def execute(self, context): self.report({'INFO'}, f"Updated {count} materials") self.track_param = context.scene.render.engine + + # NOTE: This is temporary addon_prefs = util.get_user_preferences(context) self.track_exporter = addon_prefs.MCprep_exporter_type return {'FINISHED'} @@ -440,17 +442,20 @@ def load_from_texturepack(self, mat): env.log(f"Loading from texpack for {mat.name}", vv_only=True) canon, _ = generate.get_mc_canonical_name(mat.name) image_path = generate.find_from_texturepack(canon) - if not image_path or not os.path.isfile(image_path): - env.log(f"Find missing images: No source file found for {mat.name}") + if isinstance(image_path, MCprepError): + if image_path.msg: + env.log(image_path.msg) + else: + env.log(f"Find missing images: No source file found for {mat.name}") return False # even if images of same name already exist, load new block env.log(f"Find missing images: Creating new image datablock for {mat.name}") # do not use 'check_existing=False' to keep compatibility pre 2.79 - image = bpy.data.images.load(image_path, check_existing=True) + image = bpy.data.images.load(str(image_path), check_existing=True) engine = bpy.context.scene.render.engine - if engine == 'CYCLES' or engine == 'BLENDER_EEVEE': + if engine == 'CYCLES' or engine == 'BLENDER_EEVEE' or engine == 'BLENDER_EEVEE_NEXT': status = generate.set_cycles_texture(image, mat) elif engine == 'BLENDER_RENDER' or engine == 'BLENDER_GAME': status = generate.set_cycles_texture(image, mat) diff --git a/MCprep_addon/materials/prep.py b/MCprep_addon/materials/prep.py index 194416a7..1d29993c 100644 --- a/MCprep_addon/materials/prep.py +++ b/MCprep_addon/materials/prep.py @@ -18,6 +18,7 @@ import os +from pathlib import Path import bpy from bpy_extras.io_utils import ImportHelper @@ -28,6 +29,7 @@ from . import uv_tools from .. import tracking from .. import util +from .. import world_tools from ..conf import env # ----------------------------------------------------------------------------- @@ -130,7 +132,7 @@ def draw_mats_common(self, context: Context) -> None: row = self.layout.row() col = row.column() engine = context.scene.render.engine - if engine == 'CYCLES' or engine == 'BLENDER_EEVEE': + if engine == 'CYCLES' or engine == 'BLENDER_EEVEE' or engine == 'BLENDER_EEVEE_NEXT': col.prop(self, "packFormat") col.prop(self, "usePrincipledShader") col.prop(self, "useReflections") @@ -205,14 +207,17 @@ def execute(self, context): engine = context.scene.render.engine count = 0 count_lib_skipped = 0 + count_img_not_found = 0 + count_misc_no_prep = 0 + + print("Orig mat list: ", len(mat_list)) for mat in mat_list: if not mat: env.log( "During prep, found null material:" + str(mat), vv_only=True) continue - - elif mat.library: + elif mat.library or mat.get("MCPREP_NO_PREP", False): count_lib_skipped += 1 continue @@ -244,7 +249,7 @@ def execute(self, context): if res > 0: mat["texture_swapped"] = True # used to apply saturation - if engine == 'CYCLES' or engine == 'BLENDER_EEVEE': + if engine == 'CYCLES' or engine == 'BLENDER_EEVEE' or engine == 'BLENDER_EEVEE_NEXT': options = generate.PrepOptions( passes, self.useReflections, @@ -260,10 +265,15 @@ def execute(self, context): ) if res == 0: count += 1 + elif res == generate.IMG_MISSING: + count_img_not_found += 1 + else: + count_misc_no_prep += 1 else: self.report( {'ERROR'}, - "Only Cycles and Eevee are supported") + "Only Cycles and Eevee are supported" + ) return {'CANCELLED'} if self.animateTextures: @@ -291,22 +301,37 @@ def execute(self, context): if self.optimizeScene and engine == 'CYCLES': bpy.ops.mcprep.optimize_scene() + has_lib_skipped = count_lib_skipped > 0 + has_mat = count > 0 + has_missing = count_img_not_found > 0 + + info = [] + if has_mat: + info.append(f"modified {count}") + if has_lib_skipped: + info.append(f"skipped {count_lib_skipped}") + + mat_info = ", ".join(x for x in info).capitalize() + if self.skipUsage is True: pass # Don't report if a meta-call. - elif count_lib_skipped > 0: + elif has_mat or has_lib_skipped: + self.report({"INFO"}, mat_info) + elif has_missing: self.report( - {"INFO"}, - f"Modified {count} materials, skipped {count_lib_skipped} linked ones.") - elif count > 0: - self.report({"INFO"}, f"Modified {count} materials") + {"ERROR"}, + "Nothing modified, due missing image paths - try advanced: Find Missing Textures or File > External Data > Find Missing Files" + ) else: self.report( {"ERROR"}, "Nothing modified, be sure you selected objects with existing materials!" ) - addon_prefs = util.get_user_preferences(context) self.track_param = context.scene.render.engine + + # NOTE: This is temporary + addon_prefs = util.get_user_preferences(context) self.track_exporter = addon_prefs.MCprep_exporter_type return {'FINISHED'} @@ -411,6 +436,9 @@ class MCPREP_OT_swap_texture_pack( @classmethod def poll(cls, context): + if world_tools.get_exporter(context) != None: + return util.is_atlas_export(context) + # Fallback to legacy addon_prefs = util.get_user_preferences(context) if addon_prefs.MCprep_exporter_type != "(choose)": return util.is_atlas_export(context) @@ -437,6 +465,7 @@ def draw(self, context): col.prop(self, "syncMaterials") col.prop(self, "improveUiSettings") col.prop(self, "combineMaterials") + col.prop(self, "useEmission") track_function = "texture_pack" track_param = None @@ -470,6 +499,8 @@ def execute(self, context): _ = generate.detect_form(mat_list) invalid_uv, affected_objs = uv_tools.detect_invalid_uvs_from_objs(obj_list) + # NOTE: This is temporary + addon_prefs = util.get_user_preferences(context) self.track_exporter = addon_prefs.MCprep_exporter_type # set the scene's folder for the texturepack being swapped @@ -479,7 +510,7 @@ def execute(self, context): res = 0 for mat in mat_list: self.preprocess_material(mat) - res += generate.set_texture_pack(mat, folder, self.useExtraMaps) + res += generate.set_texture_pack(mat, Path(folder), self.useExtraMaps) if self.animateTextures: sequences.animate_single_material( mat, @@ -612,7 +643,7 @@ def update_material(self, context, mat): if res > 0: mat["texture_swapped"] = True # used to apply saturation - if engine == 'CYCLES' or engine == 'BLENDER_EEVEE': + if engine == 'CYCLES' or engine == 'BLENDER_EEVEE' or engine == 'BLENDER_EEVEE_NEXT': options = generate.PrepOptions( passes=passes, use_reflections=self.useReflections, diff --git a/MCprep_addon/materials/sequences.py b/MCprep_addon/materials/sequences.py index ba112f57..4a0f9203 100644 --- a/MCprep_addon/materials/sequences.py +++ b/MCprep_addon/materials/sequences.py @@ -32,7 +32,7 @@ from . import uv_tools from .. import tracking from .. import util -from ..conf import env, Engine, Form +from ..conf import MCprepError, env, Engine, Form class ExportLocation(enum.Enum): @@ -72,8 +72,12 @@ def animate_single_material( # get the base image from the texturepack (cycles/BI general) image_path_canon = generate.find_from_texturepack(canon) - if not image_path_canon: - env.log(f"Canon path not found for {mat_gen}:{canon}, form {form}, path: {image_path_canon}", vv_only=True) + + if isinstance(image_path_canon, MCprepError): + if image_path_canon.msg: + env.log(image_path_canon.msg) + else: + env.log(f"Error occured during texturepack search for {mat_gen}:{canon}, form {form}") return affectable, False, None if not os.path.isfile(f"{image_path_canon}.mcmeta"): @@ -111,7 +115,7 @@ def animate_single_material( if not tile_path_dict[pass_name]: # ie '' env.log(f"Skipping passname: {pass_name}") continue - if engine == 'CYCLES' or engine == 'BLENDER_EEVEE': + if engine == 'CYCLES' or engine == 'BLENDER_EEVEE' or engine == 'BLENDER_EEVEE_NEXT': node = generate.get_node_for_pass(mat, pass_name) if not node: continue @@ -215,7 +219,7 @@ def generate_material_sequence(source_path: Path, image_path: Path, form: Option "try running blender as admin") for img_pass in img_pass_dict: - passfile = img_pass_dict[img_pass] + passfile = str(img_pass_dict[img_pass]) # Convert from Path env.log("Running on file:") env.log(bpy.path.abspath(passfile)) diff --git a/MCprep_addon/mcprep_ui.py b/MCprep_addon/mcprep_ui.py index f07416d0..0789ee5e 100644 --- a/MCprep_addon/mcprep_ui.py +++ b/MCprep_addon/mcprep_ui.py @@ -25,7 +25,6 @@ # addon imports from . import addon_updater_ops -from . import optimize_scene from . import tracking from . import util from . import world_tools @@ -95,9 +94,9 @@ def restart_layout(layout): alert_row.alert = True alert_row.operator( "wm.quit_blender", - text="Restart blender", + text=env._("Restart blender"), icon="ERROR") - col.label(text="to complete update") + col.label(text=env._("to complete update")) # ----------------------------------------------------------------------------- @@ -106,9 +105,9 @@ def restart_layout(layout): class MCPREP_MT_mob_spawner(bpy.types.Menu): """Shift-A menu in the 3D view""" - bl_label = "Mob Spawner" + bl_label = env._("Mob Spawner") bl_idname = "MCPREP_MT_mob_spawner" - bl_description = "Menu for placing in the shift-A add object menu" + bl_description = env._("Menu for placing in the shift-A add object menu") def draw(self, context): layout = self.layout @@ -118,7 +117,7 @@ def draw(self, context): if not scn_props.mob_list_all: row = layout.row() row.operator( - "mcprep.reload_mobs", text="Load mobs", icon=HAND_ICON) + "mcprep.reload_mobs", text=env._("Load mobs"), icon=HAND_ICON) row.scale_y = 2 row.alignment = 'CENTER' return @@ -147,14 +146,14 @@ def draw(self, context): class MCPREP_MT_meshswap_place(bpy.types.Menu): """Menu for all the meshswap objects""" - bl_label = "Meshswap Objects" + bl_label = env._("Meshswap Objects") bl_idname = "MCPREP_MT_meshswap_place" def draw(self, context): layout = self.layout meshswap_blocks = meshswap.getMeshswapList(context) if not meshswap_blocks: - layout.label(text="No meshswap blocks found!") + layout.label(text=env._("No meshswap blocks found!")) for blockset in meshswap_blocks: # do some kind of check for if no blocks found icn = "BLANK1" @@ -176,13 +175,13 @@ def draw(self, context): class MCPREP_MT_item_spawn(bpy.types.Menu): """Menu for loaded item spawners""" - bl_label = "Item Spawner" + bl_label = env._("Item Spawner") bl_idname = "MCPREP_MT_item_spawn" def draw(self, context): layout = self.layout if not context.scene.mcprep_props.item_list: - layout.label(text="No items found!") + layout.label(text=env._("No items found!")) for item in context.scene.mcprep_props.item_list: icn = f"item-{item.index}" if env.use_icons and icn in env.preview_collections["items"]: @@ -199,7 +198,7 @@ def draw(self, context): class MCPREP_MT_effect_spawn(bpy.types.Menu): """Menu for loaded effect spawners""" - bl_label = "Effects Spawner" + bl_label = env._("Effects Spawner") bl_idname = "MCPREP_MT_effect_spawn" def draw(self, context): @@ -242,14 +241,14 @@ def draw(self, context): class MCPREP_MT_entity_spawn(bpy.types.Menu): """Menu for loaded entity spawners""" - bl_label = "Entity Spawner" + bl_label = env._("Entity Spawner") bl_idname = "MCPREP_MT_entity_spawn" def draw(self, context): layout = self.layout entity_list = entities.getEntityList(context) if not entity_list: - layout.label(text="No entities found!") + layout.label(text=env._("No entities found!")) for entity in entity_list: # do some kind of check for if no entities found icn = "BLANK1" @@ -265,13 +264,13 @@ def draw(self, context): class MCPREP_MT_model_spawn(bpy.types.Menu): """Menu for loaded model spawners""" - bl_label = "Model Spawner" + bl_label = env._("Model Spawner") bl_idname = "MCPREP_MT_model_spawn" def draw(self, context): layout = self.layout if not context.scene.mcprep_props.model_list: - layout.label(text="No models found!") + layout.label(text=env._("No models found!")) for model in context.scene.mcprep_props.model_list: opr = layout.operator( mcmodel.MCPREP_OT_spawn_minecraft_model.bl_idname, @@ -311,7 +310,7 @@ def draw(self, context): if not env.loaded_all_spawners and not all_loaded: row = layout.row() row.operator( - "mcprep.reload_spawners", text="Load spawners", icon=HAND_ICON) + "mcprep.reload_spawners", text=env._("Load spawners"), icon=HAND_ICON) row.scale_y = 2 row.alignment = 'CENTER' return @@ -513,122 +512,122 @@ def draw(self, context): row = layout.row() row.scale_y = 0.7 - row.label(text="World Importing & Meshswapping") + row.label(text=env._("World Importing & Meshswapping")) box = layout.box() split = util.layout_split(box, factor=factor_width) col = split.column() - col.label(text="Default Exporter:") + col.label(text=env._("Default Exporter:")) col = split.column() col.prop(self, "MCprep_exporter_type", text="") split = util.layout_split(box, factor=factor_width) col = split.column() - col.label(text="jmc2obj executable") + col.label(text=env._("jmc2obj executable")) col = split.column() col.prop(self, "open_jmc2obj_path", text="") split = util.layout_split(box, factor=factor_width) col = split.column() - col.label(text="Mineways executable") + col.label(text=env._("Mineways executable")) col = split.column() col.prop(self, "open_mineways_path", text="") split = util.layout_split(box, factor=factor_width) col = split.column() - col.label(text="World OBJ Exports Folder") + col.label(text=env._("World OBJ Exports Folder")) col = split.column() col.prop(self, "world_obj_path", text="") split = util.layout_split(box, factor=factor_width) col = split.column() - col.label(text="Meshwap assets") + col.label(text=env._("Meshwap assets")) col = split.column() col.prop(self, "meshswap_path", text="") if not os.path.isfile(bpy.path.abspath(self.meshswap_path)): row = box.row() - row.label(text="MeshSwap file not found", icon="ERROR") + row.label(text=env._("MeshSwap file not found"), icon="ERROR") split = util.layout_split(box, factor=factor_width) col = split.column() - col.label(text="Entity assets") + col.label(text=env._("Entity assets")) col = split.column() col.prop(self, "entity_path", text="") if not os.path.isfile(bpy.path.abspath(self.entity_path)): row = box.row() - row.label(text="Entity file not found", icon="ERROR") + row.label(text=env._("Entity file not found"), icon="ERROR") col = split.column() col.prop(self, "effects_path", text="") if not os.path.isdir(bpy.path.abspath(self.effects_path)): row = box.row() - row.label(text="Effects folder not found", icon="ERROR") + row.label(text=env._("Effects folder not found"), icon="ERROR") row = layout.row() row.scale_y = 0.7 - row.label(text="Texture / Resource packs") + row.label(text=env._("Texture / Resource packs")) box = layout.box() split = util.layout_split(box, factor=factor_width) col = split.column() - col.label(text="Texture pack folder") + col.label(text=env._("Texture pack folder")) col = split.column() col.prop(self, "custom_texturepack_path", text="") split = util.layout_split(box, factor=factor_width) col = split.column() - col.label(text="Install to folder") + col.label(text=env._("Install to folder")) # col = split.column() - # col.operator("mcprep.skin_swapper", text="Install resource pack") + # col.operator("mcprep.skin_swapper", text=env._("Install resource pack")) col = split.column() - p = col.operator("mcprep.openfolder", text="Open texture pack folder") + p = col.operator("mcprep.openfolder", text=env._("Open texture pack folder")) p.folder = self.custom_texturepack_path row = layout.row() row.scale_y = 0.7 - row.label(text="Mob spawning") + row.label(text=env._("Mob spawning")) box = layout.box() split = util.layout_split(box, factor=factor_width) col = split.column() - col.label(text="Rig Folder") + col.label(text=env._("Rig Folder")) col = split.column() col.prop(self, "mob_path", text="") split = util.layout_split(box, factor=factor_width) col = split.column() - col.label(text="Select/install mobs") + col.label(text=env._("Select/install mobs")) col = split.column() col.operator( - "mcprep.mob_install_menu", text="Install file for mob spawning") + "mcprep.mob_install_menu", text=env._("Install file for mob spawning")) col = split.column() - p = col.operator("mcprep.openfolder", text="Open rig folder") + p = col.operator("mcprep.openfolder", text=env._("Open rig folder")) p.folder = self.mob_path row = layout.row() row.scale_y = 0.7 - row.label(text="Skin swapping") + row.label(text=env._("Skin swapping")) box = layout.box() split = util.layout_split(box, factor=factor_width) col = split.column() - col.label(text="Skin Folder") + col.label(text=env._("Skin Folder")) col = split.column() col.prop(self, "skin_path", text="") split = util.layout_split(box, factor=factor_width) col = split.column() - col.label(text="Install skins") + col.label(text=env._("Install skins")) col = split.column() - col.operator("mcprep.add_skin", text="Install skin file for swapping") + col.operator("mcprep.add_skin", text=env._("Install skin file for swapping")) col = split.column() - p = col.operator("mcprep.openfolder", text="Open skin folder") + p = col.operator("mcprep.openfolder", text=env._("Open skin folder")) p.folder = self.skin_path row = layout.row() row.scale_y = 0.7 - row.label(text="Effects") + row.label(text=env._("Effects")) box = layout.box() split = util.layout_split(box, factor=factor_width) col = split.column() - col.label(text="Effect folder") + col.label(text=env._("Effect folder")) col = split.column() col.prop(self, "effects_path", text="") split = util.layout_split(box, factor=factor_width) col = split.column() col = split.column() - p = col.operator("mcprep.openfolder", text="Open effects folder") + p = col.operator("mcprep.openfolder", text=env._("Open effects folder")) p.folder = self.effects_path # misc settings @@ -640,18 +639,18 @@ def draw(self, context): row = layout.row() box = row.box() box.scale_y = 0.7 - box.label(text="Using MCprep in experimental mode!", icon="ERROR") - box.label(text="Early access features and requests for feedback") - box.label(text="will be made visible. Thank you for contributing.") + box.label(text=env._("Using MCprep in experimental mode!"), icon="ERROR") + box.label(text=env._("Early access features and requests for feedback")) + box.label(text=env._("will be made visible. Thank you for contributing.")) elif self.preferences_tab == "tutorials": layout.label( - text="Unsure on how to use the addon? Check out these resources") + text=env._("Unsure on how to use the addon? Check out these resources")) row = layout.row() row.scale_y = 2 row.operator( "wm.url_open", - text="MCprep page for instructions and updates", + text=env._("MCprep page for instructions and updates"), icon="WORLD" ).url = "https://theduckcow.com/dev/blender/mcprep/" @@ -659,32 +658,32 @@ def draw(self, context): row.scale_y = 2 row.operator( "wm.url_open", - text="Learn MCprep + Blender, 1-minute tutorials", + text=env._("Learn MCprep + Blender, 1-minute tutorials"), icon="FILE_MOVIE" ).url = "https://bit.ly/MCprepTutorials" row = layout.row() row.scale_y = 1.5 row.operator( - "wm.url_open", text="Import Minecraft worlds" + "wm.url_open", text=env._("Import Minecraft worlds") ).url = "https://theduckcow.com/dev/blender/mcprep/mcprep-minecraft-world-imports/" row.operator( - "wm.url_open", text="Mob (rig) spawning" + "wm.url_open", text=env._("Mob (rig) spawning") ).url = "https://theduckcow.com/dev/blender/mcprep/mcprep-spawner/" row.operator( - "wm.url_open", text="Skin swapping" + "wm.url_open", text=env._("Skin swapping") ).url = "https://theduckcow.com/dev/blender/mcprep/skin-swapping/" row = layout.row() row.scale_y = 1.5 row.operator( - "wm.url_open", text="jmc2obj/Mineways" + "wm.url_open", text=env._("jmc2obj/Mineways") ).url = "https://theduckcow.com/dev/blender/mcprep/setup-world-exporters/" row.operator( - "wm.url_open", text="World Tools" + "wm.url_open", text=env._("World Tools") ).url = "https://theduckcow.com/dev/blender/mcprep/world-tools/" row.operator( - "wm.url_open", text="Tutorial Series" + "wm.url_open", text=env._("Tutorial Series") ).url = "https://bit.ly/MCprepTutorials" elif self.preferences_tab == "tracker_updater": @@ -692,7 +691,7 @@ def draw(self, context): row = layout.row() box = row.box() brow = box.row() - brow.label(text="Anonymous user tracking settings") + brow.label(text=env._("Anonymous user tracking settings")) brow = box.row() bcol = brow.column() @@ -701,19 +700,19 @@ def draw(self, context): if tracking.Tracker.tracking_enabled is False: bcol.operator( "mcprep.toggle_enable_tracking", - text="Opt into anonymous usage tracking", + text=env._("Opt into anonymous usage tracking"), icon=OPT_IN) else: bcol.operator( "mcprep.toggle_enable_tracking", - text="Opt OUT of anonymous usage tracking", + text=env._("Opt OUT of anonymous usage tracking"), icon="CANCEL") bcol = brow.column() - bcol.label(text="For info on anonymous usage tracking:") + bcol.label(text=env._("For info on anonymous usage tracking:")) brow = box.row() bcol.operator( - "wm.url_open", text="Open the Privacy Policy" + "wm.url_open", text=env._("Open the Privacy Policy") ).url = "https://theduckcow.com/privacy-policy" # updater draw function @@ -722,7 +721,7 @@ def draw(self, context): class MCPREP_PT_world_imports(bpy.types.Panel): """World importing related settings and tools""" - bl_label = "World Imports" + bl_label = env._("World Imports") bl_space_type = 'VIEW_3D' bl_region_type = 'UI' # bl_context = "objectmode" @@ -745,7 +744,7 @@ def draw(self, context): split = layout.split() col = split.column(align=True) row = col.row() - row.label(text="World exporter") + row.label(text=env._("World exporter")) row.operator( "mcprep.open_help", text="", icon="QUESTION", emboss=False ).url = "https://theduckcow.com/dev/blender/mcprep/mcprep-minecraft-world-imports/" @@ -754,37 +753,39 @@ def draw(self, context): row = col.row(align=True) row.prop(addon_prefs, "MCprep_exporter_type", expand=True) row = col.row(align=True) - if addon_prefs.MCprep_exporter_type == "(choose)": + exporter = world_tools.get_exporter(context) + if exporter is None: row.operator( - "mcprep.open_jmc2obj", text="Select exporter!", icon='ERROR') + "mcprep.open_jmc2obj", text=env._("Select exporter!"), icon='ERROR') row.enabled = False - elif addon_prefs.MCprep_exporter_type == "Mineways": + elif exporter is world_tools.WorldExporter.Mineways or exporter is world_tools.WorldExporter.ClassicMW: row.operator("mcprep.open_mineways") - else: + elif exporter is world_tools.WorldExporter.Jmc2OBJ or exporter is world_tools.WorldExporter.ClassicJmc: row.operator("mcprep.open_jmc2obj") wpath = addon_prefs.world_obj_path col.operator( "mcprep.import_world_split", - text="OBJ world import").filepath = wpath + text=env._("OBJ world import")).filepath = wpath split = layout.split() col = split.column(align=True) - col.label(text="MCprep tools") - col.operator("mcprep.prep_materials", text="Prep Materials") + col.label(text=env._("MCprep tools")) + col.operator("mcprep.prep_materials", text=env._("Prep Materials")) if not util.is_atlas_export(context): row = col.row() row.operator( "mcprep.open_help", text="", icon="QUESTION", emboss=False ).url = "https://github.com/TheDuckCow/MCprep/blob/master/docs/common_errors.md#common-error-messages-and-what-they-mean" - row.label(text="OBJ incompatible with textureswap") + row.label(text=env._("OBJ incompatible with textureswap")) p = col.operator("mcprep.swap_texture_pack") p.filepath = context.scene.mcprep_texturepack_path if context.mode == "OBJECT": - col.operator("mcprep.meshswap", text="Mesh Swap") - if addon_prefs.MCprep_exporter_type == "(choose)": - col.label(text="Select exporter!", icon='ERROR') + col.operator("mcprep.meshswap", text=env._("Mesh Swap")) + exporter = world_tools.get_exporter(context) + if exporter is None or exporter is world_tools.WorldExporter.Unknown: + col.label(text=env._("Select exporter!"), icon='ERROR') if context.mode == 'EDIT_MESH': col.operator("mcprep.scale_uv") col.operator("mcprep.select_alpha_faces") @@ -804,43 +805,31 @@ def draw(self, context): row.enabled = False row.operator( "mcprep.improve_ui", - text="(UI already improved)", icon='SETTINGS') + text=env._("(UI already improved)"), icon='SETTINGS') else: col.operator( - "mcprep.improve_ui", text="Improve UI", icon='SETTINGS') - - # Optimizer Panel (only for blender 2.80+) - row = col.row(align=True) - icon = "TRIA_DOWN" if scn_props.show_settings_optimizer else "TRIA_RIGHT" - row.prop( - scn_props, "show_settings_optimizer", - text="Cycles Optimizer (Deprecated)", icon=icon) - if scn_props.show_settings_optimizer: - row = col.row(align=True) - row.label(text="The Cycles optimizer will be removed in 3.6!") - row = col.row(align=True) - optimize_scene.panel_draw(context, row) + "mcprep.improve_ui", text=env._("Improve UI"), icon='SETTINGS') # Advanced settings. row = col.row(align=True) if not scn_props.show_settings_material: row.prop( scn_props, "show_settings_material", - text="Advanced", icon="TRIA_RIGHT") + text=env._("Advanced"), icon="TRIA_RIGHT") row.operator( "mcprep.open_preferences", text="", icon="PREFERENCES").tab = "settings" else: row.prop( scn_props, "show_settings_material", - text="Advanced", icon="TRIA_DOWN") + text=env._("Advanced"), icon="TRIA_DOWN") row.operator( "mcprep.open_preferences", text="", icon="PREFERENCES").tab = "settings" box = col.box() b_row = box.row() b_col = b_row.column(align=False) - b_col.label(text="Texture pack folder") + b_col.label(text=env._("Texture pack folder")) row = b_col.row(align=True) row.prop(context.scene, "mcprep_texturepack_path", text="") row.operator("mcprep.reset_texture_path", text="", icon=LOAD_FACTORY) @@ -856,19 +845,19 @@ def draw(self, context): # TODO: operator to make all local, all packed, or set to other location b_col.operator( "mcprep.combine_materials", - text="Combine Materials").selection_only = True + text=env._("Combine Materials")).selection_only = True if bpy.app.version > (2, 77): - b_col.operator("mcprep.combine_images", text="Combine Images") + b_col.operator("mcprep.combine_images", text=env._("Combine Images")) - b_col.label(text="Meshswap source:") + b_col.label(text=env._("Meshswap source:")) subrow = b_col.row(align=True) subrow.prop(context.scene, "meshswap_path", text="") subrow.operator( "mcprep.meshswap_path_reset", icon=LOAD_FACTORY, text="") if not context.scene.meshswap_path.lower().endswith('.blend'): - b_col.label(text="MeshSwap file must be .blend", icon="ERROR") + b_col.label(text=env._("MeshSwap file must be .blend"), icon="ERROR") if not os.path.isfile(bpy.path.abspath(context.scene.meshswap_path)): - b_col.label(text="MeshSwap file not found", icon="ERROR") + b_col.label(text=env._("MeshSwap file not found"), icon="ERROR") layout = self.layout # clear out the box formatting split = layout.split() @@ -877,7 +866,7 @@ def draw(self, context): class MCPREP_PT_bridge(bpy.types.Panel): """MCprep panel for directly importing and reloading minecraft saves""" - bl_label = "World Bridge" + bl_label = env._("World Bridge") bl_space_type = "VIEW_3D" bl_region_type = 'UI' bl_context = "objectmode" @@ -894,7 +883,7 @@ def draw(self, context): class MCPREP_PT_world_tools(bpy.types.Panel): """World settings and tools""" - bl_label = "World Tools" + bl_label = env._("World Tools") bl_space_type = 'VIEW_3D' bl_region_type = 'UI' bl_category = "MCprep" @@ -908,7 +897,7 @@ def draw(self, context): rw = layout.row() col = rw.column() row = col.row(align=True) - row.label(text="World settings and lighting") # world time + row.label(text=env._("World settings and lighting")) # world time row.operator( "mcprep.open_help", text="", icon="QUESTION", emboss=False ).url = "https://theduckcow.com/dev/blender/mcprep/world-tools/" @@ -923,14 +912,14 @@ def draw(self, context): rw = layout.row() col = rw.column(align=True) obj = world_tools.get_time_object() - col.label(text="Time of day") + col.label(text=env._("Time of day")) if obj and "MCprepHour" in obj: time = obj["MCprepHour"] col.prop( obj, '["MCprepHour"]', text="") - col.label(text="{h}:{m}, day {d}".format( + col.label(text=env._("{h}:{m}, day {d}").format( h=str(int(time % 24 - time % 1)).zfill(2), m=str(int(time % 1 * 60)).zfill(2), d=int((time - time % 24) / 24) @@ -939,17 +928,17 @@ def draw(self, context): box = col.box() subcol = box.column() subcol.scale_y = 0.8 - subcol.label(text="No time controller,") - subcol.label(text="add dynamic MC world.") - # col.label(text="World setup") + subcol.label(text=env._("No time controller,")) + subcol.label(text=env._("add dynamic MC world.")) + # col.label(text=env._("World setup")) # col.operator("mcprep.world") - # col.operator("mcprep.world", text="Add clouds") - # col.operator("mcprep.world", text="Set Weather") + # col.operator("mcprep.world", text=env._("Add clouds")) + # col.operator("mcprep.world", text=env._("Set Weather")) class MCPREP_PT_skins(bpy.types.Panel): """MCprep panel for skin swapping""" - bl_label = "Skin Swapper" + bl_label = env._("Skin Swapper") bl_space_type = 'VIEW_3D' bl_region_type = 'UI' bl_category = "MCprep" @@ -966,7 +955,7 @@ def draw(self, context): skinname = None row = layout.row() - row.label(text="Select skin") + row.label(text=env._("Select skin")) row.operator( "mcprep.open_help", text="", icon="QUESTION", emboss=False ).url = "https://theduckcow.com/dev/blender/mcprep/skin-swapping/" @@ -983,14 +972,14 @@ def draw(self, context): # any other conditions for needing reloading? if not env.skin_list: col = layout.column() - col.label(text="No skins found/loaded") + col.label(text=env._("No skins found/loaded")) p = col.operator( - "mcprep.reload_skins", text="Press to reload", icon="ERROR") + "mcprep.reload_skins", text=env._("Press to reload"), icon="ERROR") elif env.skin_list and len(env.skin_list) <= sind: col = layout.column() - col.label(text="Reload skins") + col.label(text=env._("Reload skins")) p = col.operator( - "mcprep.reload_skins", text="Press to reload", icon="ERROR") + "mcprep.reload_skins", text=env._("Press to reload"), icon="ERROR") else: col.template_list( "MCPREP_UL_skins", "", @@ -1008,11 +997,11 @@ def draw(self, context): p.filepath = env.skin_list[sind][1] else: row.enabled = False - p = row.operator("mcprep.skin_swapper", text="No skins found") + p = row.operator("mcprep.skin_swapper", text=env._("No skins found")) row = col.row(align=True) - row.operator("mcprep.skin_swapper", text="Skin from file") + row.operator("mcprep.skin_swapper", text=env._("Skin from file")) row = col.row(align=True) - row.operator("mcprep.applyusernameskin", text="Skin from username") + row.operator("mcprep.applyusernameskin", text=env._("Skin from username")) split = layout.split() col = split.column(align=True) @@ -1020,20 +1009,20 @@ def draw(self, context): if not scn_props.show_settings_skin: row.prop( scn_props, "show_settings_skin", - text="Advanced", icon="TRIA_RIGHT") + text=env._("Advanced"), icon="TRIA_RIGHT") row.operator( "mcprep.open_preferences", text="", icon="PREFERENCES").tab = "settings" else: row.prop( scn_props, "show_settings_skin", - text="Advanced", icon="TRIA_DOWN") + text=env._("Advanced"), icon="TRIA_DOWN") row.operator( "mcprep.open_preferences", text="", icon="PREFERENCES").tab = "settings" box = col.box() b_row = box.column(align=True) - b_row.label(text="Skin path") + b_row.label(text=env._("Skin path")) b_subrow = b_row.row(align=True) b_subrow.prop(context.scene, "mcprep_skin_path", text="") b_subrow.operator( @@ -1047,11 +1036,11 @@ def draw(self, context): if not scn_props.mob_list: row.enabled = False row.operator( - "mcprep.spawn_with_skin", text="Reload mobs below") + "mcprep.spawn_with_skin", text=env._("Reload mobs below")) elif not env.skin_list: row.enabled = False row.operator( - "mcprep.spawn_with_skin", text="Reload skins above") + "mcprep.spawn_with_skin", text=env._("Reload skins above")) else: name = scn_props.mob_list[mob_ind].name # datapass = scn_props.mob_list[mob_ind].mcmob_type @@ -1061,7 +1050,7 @@ def draw(self, context): class MCPREP_PT_materials(bpy.types.Panel): """MCprep panel for materials""" - bl_label = "MCprep materials" + bl_label = env._("MCprep materials") bl_space_type = "PROPERTIES" bl_region_type = 'WINDOW' bl_context = "material" @@ -1076,7 +1065,6 @@ def draw(self, context): return row = layout.row() - # row.operator("mcprep.create_default_material") split = layout.split() col = split.column(align=True) @@ -1095,7 +1083,7 @@ def draw(self, context): else: box = col.box() b_row = box.row() - b_row.label(text="No materials loaded") + b_row.label(text=env._("No materials loaded")) b_row = box.row() b_row.scale_y = 2 b_row.operator("mcprep.reload_materials", icon="ERROR") @@ -1104,12 +1092,12 @@ def draw(self, context): col.enabled = False row = col.row(align=True) row.scale_y = 1.5 - ops = row.operator("mcprep.load_material", text="Load material") + ops = row.operator("mcprep.load_material", text=env._("Load material")) class MCPREP_PT_materials_subsettings(bpy.types.Panel): """MCprep panel for advanced material settings and functions""" - bl_label = "Advanced" + bl_label = env._("Advanced") bl_parent_id = "MCPREP_PT_materials" bl_space_type = "PROPERTIES" bl_region_type = 'WINDOW' @@ -1122,7 +1110,7 @@ def draw(self, context): b_row = self.layout.row() b_col = b_row.column(align=False) - b_col.label(text="Resource pack") + b_col.label(text=env._("Resource pack")) subrow = b_col.row(align=True) subrow.prop(context.scene, "mcprep_texturepack_path", text="") subrow.operator( @@ -1138,8 +1126,8 @@ def draw(self, context): def draw_mode_warning(ui_element: UILayout) -> None: col = ui_element.column(align=True) - col.label(text="Enter object mode", icon="ERROR") - col.label(text="to use spawner", icon="BLANK1") + col.label(text=env._("Enter object mode"), icon="ERROR") + col.label(text=env._("to use spawner"), icon="BLANK1") col.operator("object.mode_set").mode = "OBJECT" col.label(text="") @@ -1148,7 +1136,7 @@ def mob_spawner(self, context: Context) -> None: scn_props = context.scene.mcprep_props layout = self.layout - layout.label(text="Import pre-rigged mobs & players") + layout.label(text=env._("Import pre-rigged mobs & players")) split = layout.split() col = split.column(align=True) @@ -1167,19 +1155,19 @@ def mob_spawner(self, context: Context) -> None: b_row.label(text="") b_col = box.column() b_col.scale_y = 0.7 - b_col.label(text="No mobs in category,") - b_col.label(text="install a rig below or") - b_col.label(text="copy file to folder.") + b_col.label(text=env._("No mobs in category,")) + b_col.label(text=env._("install a rig below or")) + b_col.label(text=env._("copy file to folder.")) b_row = box.row() b_row.label(text="") else: box = col.box() b_row = box.row() - b_row.label(text="No mobs loaded") + b_row.label(text=env._("No mobs loaded")) b_row = box.row() b_row.scale_y = 2 b_row.operator( - "mcprep.reload_spawners", text="Reload assets", icon="ERROR") + "mcprep.reload_spawners", text=env._("Reload assets"), icon="ERROR") # get which rig is selected if scn_props.mob_list: @@ -1209,28 +1197,28 @@ def mob_spawner(self, context: Context) -> None: if not scn_props.show_settings_spawner: row.prop( scn_props, "show_settings_spawner", - text="Advanced", icon="TRIA_RIGHT") + text=env._("Advanced"), icon="TRIA_RIGHT") row.operator( "mcprep.open_preferences", text="", icon="PREFERENCES").tab = "settings" else: row.prop( scn_props, "show_settings_spawner", - text="Advanced", icon="TRIA_DOWN") + text=env._("Advanced"), icon="TRIA_DOWN") row.operator( "mcprep.open_preferences", text="", icon="PREFERENCES").tab = "settings" box = col.box() b_row = box.row() b_col = b_row.column(align=False) - b_col.label(text="Mob spawner folder") + b_col.label(text=env._("Mob spawner folder")) subrow = b_col.row(align=True) subrow.prop(context.scene, "mcprep_mob_path", text="") subrow.operator( "mcprep.spawn_path_reset", icon=LOAD_FACTORY, text="") b_row = box.row() b_col = b_row.column(align=True) - ops = b_col.operator("mcprep.openfolder", text="Open mob folder") + ops = b_col.operator("mcprep.openfolder", text=env._("Open mob folder")) ops.folder = context.scene.mcprep_mob_path if not scn_props.mob_list: @@ -1239,11 +1227,11 @@ def mob_spawner(self, context: Context) -> None: icon_index = scn_props.mob_list[scn_props.mob_list_index].index if f"mob-{icon_index}" in env.preview_collections["mobs"]: b_col.operator( - "mcprep.mob_install_icon", text="Change mob icon") + "mcprep.mob_install_icon", text=env._("Change mob icon")) else: b_col.operator("mcprep.mob_install_icon") b_col.operator("mcprep.mob_uninstall") - b_col.operator("mcprep.reload_mobs", text="Reload mobs") + b_col.operator("mcprep.reload_mobs", text=env._("Reload mobs")) b_col.label(text=mcmob_type) @@ -1251,7 +1239,7 @@ def meshswap_spawner(self, context: Context) -> None: scn_props = context.scene.mcprep_props layout = self.layout - layout.label(text="Import pre-made blocks (e.g. lights)") + layout.label(text=env._("Import pre-made blocks (e.g. lights)")) split = layout.split() col = split.column(align=True) @@ -1265,30 +1253,30 @@ def meshswap_spawner(self, context: Context) -> None: elif not context.scene.meshswap_path.lower().endswith('.blend'): box = col.box() b_row = box.row() - b_row.label(text="Meshswap file must be a .blend") + b_row.label(text=env._("Meshswap file must be a .blend")) b_row = box.row() b_row.scale_y = 2 b_row.operator( "mcprep.meshswap_path_reset", icon=LOAD_FACTORY, - text="Reset meshswap path") + text=env._("Reset meshswap path")) elif not os.path.isfile(bpy.path.abspath(context.scene.meshswap_path)): box = col.box() b_row = box.row() - b_row.label(text="Meshswap file not found") + b_row.label(text=env._("Meshswap file not found")) b_row = box.row() b_row.scale_y = 2 b_row.operator( "mcprep.meshswap_path_reset", icon=LOAD_FACTORY, - text="Reset meshswap path") + text=env._("Reset meshswap path")) else: box = col.box() b_row = box.row() - b_row.label(text="No blocks loaded") + b_row.label(text=env._("No blocks loaded")) b_row = box.row() b_row.scale_y = 2 b_row.operator( "mcprep.reload_spawners", - text="Reload assets", icon="ERROR") + text=env._("Reload assets"), icon="ERROR") col = layout.column(align=True) row = col.row() @@ -1307,7 +1295,7 @@ def meshswap_spawner(self, context: Context) -> None: p.make_real = True else: - row.operator("mcprep.meshswap_spawner", text="Place block") + row.operator("mcprep.meshswap_spawner", text=env._("Place block")) # something to directly open meshswap file?? split = layout.split() @@ -1317,24 +1305,24 @@ def meshswap_spawner(self, context: Context) -> None: if not scn_props.show_settings_spawner: col.prop( scn_props, "show_settings_spawner", - text="Advanced", icon="TRIA_RIGHT") + text=env._("Advanced"), icon="TRIA_RIGHT") else: col.prop( scn_props, "show_settings_spawner", - text="Advanced", icon="TRIA_DOWN") + text=env._("Advanced"), icon="TRIA_DOWN") box = col.box() b_row = box.row() b_col = b_row.column(align=False) - b_col.label(text="Meshswap file") + b_col.label(text=env._("Meshswap file")) subrow = b_col.row(align=True) subrow.prop(context.scene, "meshswap_path", text="") subrow.operator( "mcprep.meshswap_path_reset", icon=LOAD_FACTORY, text="") if not context.scene.meshswap_path.lower().endswith('.blend'): - b_col.label(text="MeshSwap file must be a .blend", icon="ERROR") + b_col.label(text=env._("MeshSwap file must be a .blend"), icon="ERROR") elif not os.path.isfile(bpy.path.abspath(context.scene.meshswap_path)): - b_col.label(text="MeshSwap file not found", icon="ERROR") + b_col.label(text=env._("MeshSwap file not found"), icon="ERROR") b_row = box.row() b_col = b_row.column(align=True) b_col.operator("mcprep.reload_meshswap") @@ -1345,7 +1333,7 @@ def item_spawner(self, context: Context) -> None: scn_props = context.scene.mcprep_props layout = self.layout - layout.label(text="Generate items from textures") + layout.label(text=env._("Generate items from textures")) split = layout.split() col = split.column(align=True) @@ -1365,18 +1353,18 @@ def item_spawner(self, context: Context) -> None: else: box = col.box() b_row = box.row() - b_row.label(text="No items loaded") + b_row.label(text=env._("No items loaded")) b_row = box.row() b_row.scale_y = 2 b_row.operator( "mcprep.reload_spawners", - text="Reload assets", icon="ERROR") + text=env._("Reload assets"), icon="ERROR") col = layout.column(align=True) col.enabled = False row = col.row(align=True) row.scale_y = 1.5 - row.operator("mcprep.spawn_item", text="Place item") + row.operator("mcprep.spawn_item", text=env._("Place item")) row = col.row(align=True) row.operator("mcprep.spawn_item_file") @@ -1387,15 +1375,15 @@ def item_spawner(self, context: Context) -> None: if not scn_props.show_settings_spawner: col.prop( scn_props, "show_settings_spawner", - text="Advanced", icon="TRIA_RIGHT") + text=env._("Advanced"), icon="TRIA_RIGHT") else: col.prop( scn_props, "show_settings_spawner", - text="Advanced", icon="TRIA_DOWN") + text=env._("Advanced"), icon="TRIA_DOWN") box = col.box() b_row = box.row() b_col = b_row.column(align=False) - b_col.label(text="Resource pack") + b_col.label(text=env._("Resource pack")) subrow = b_col.row(align=True) subrow.prop(context.scene, "mcprep_texturepack_path", text="") subrow.operator( @@ -1409,7 +1397,7 @@ def entity_spawner(self, context: Context) -> None: scn_props = context.scene.mcprep_props layout = self.layout - layout.label(text="Import pre-rigged entities") + layout.label(text=env._("Import pre-rigged entities")) split = layout.split() col = split.column(align=True) @@ -1423,30 +1411,30 @@ def entity_spawner(self, context: Context) -> None: elif not context.scene.entity_path.lower().endswith('.blend'): box = col.box() b_row = box.row() - b_row.label(text="Entity file must be a .blend") + b_row.label(text=env._("Entity file must be a .blend")) b_row = box.row() b_row.scale_y = 2 b_row.operator( "mcprep.entity_path_reset", icon=LOAD_FACTORY, - text="Reset entity path") + text=env._("Reset entity path")) elif not os.path.isfile(bpy.path.abspath(context.scene.entity_path)): box = col.box() b_row = box.row() - b_row.label(text="Entity file not found") + b_row.label(text=env._("Entity file not found")) b_row = box.row() b_row.scale_y = 2 b_row.operator( "mcprep.entity_path_reset", icon=LOAD_FACTORY, - text="Reset entity path") + text=env._("Reset entity path")) else: box = col.box() b_row = box.row() - b_row.label(text="No entities loaded") + b_row.label(text=env._("No entities loaded")) b_row = box.row() b_row.scale_y = 2 b_row.operator( "mcprep.reload_spawners", - text="Reload assets", icon="ERROR") + text=env._("Reload assets"), icon="ERROR") col = layout.column(align=True) row = col.row() @@ -1458,7 +1446,7 @@ def entity_spawner(self, context: Context) -> None: p = row.operator("mcprep.entity_spawner", text=f"Spawn: {name}") p.entity = entity else: - row.operator("mcprep.entity_spawner", text="Spawn Entity") + row.operator("mcprep.entity_spawner", text=env._("Spawn Entity")) split = layout.split() col = split.column(align=True) @@ -1467,22 +1455,22 @@ def entity_spawner(self, context: Context) -> None: if not scn_props.show_settings_spawner: col.prop( scn_props, "show_settings_spawner", - text="Advanced", icon="TRIA_RIGHT") + text=env._("Advanced"), icon="TRIA_RIGHT") else: col.prop( scn_props, "show_settings_spawner", - text="Advanced", icon="TRIA_DOWN") + text=env._("Advanced"), icon="TRIA_DOWN") box = col.box() b_row = box.row() b_col = b_row.column(align=False) - b_col.label(text="Entity file") + b_col.label(text=env._("Entity file")) subrow = b_col.row(align=True) subrow.prop(context.scene, "entity_path", text="") subrow.operator("mcprep.entity_path_reset", icon=LOAD_FACTORY, text="") if not context.scene.entity_path.lower().endswith('.blend'): - b_col.label(text="MeshSwap file must be a .blend", icon="ERROR") + b_col.label(text=env._("MeshSwap file must be a .blend"), icon="ERROR") elif not os.path.isfile(bpy.path.abspath(context.scene.entity_path)): - b_col.label(text="MeshSwap file not found", icon="ERROR") + b_col.label(text=env._("MeshSwap file not found"), icon="ERROR") b_row = box.row() b_col = b_row.column(align=True) b_col.operator("mcprep.reload_entities") @@ -1494,7 +1482,7 @@ def model_spawner(self, context: Context) -> None: addon_prefs = util.get_user_preferences(context) layout = self.layout - layout.label(text="Generate models from .json files") + layout.label(text=env._("Generate models from .json files")) split = layout.split() col = split.column(align=True) @@ -1512,19 +1500,20 @@ def model_spawner(self, context: Context) -> None: ops = row.operator("mcprep.spawn_model", text=f"Place: {model.name}") ops.location = util.get_cursor_location(context) ops.filepath = model.filepath - if addon_prefs.MCprep_exporter_type == "Mineways": + exporter = world_tools.get_exporter(context) + if exporter is world_tools.WorldExporter.Mineways or exporter is world_tools.WorldExporter.ClassicMW: ops.snapping = "offset" - elif addon_prefs.MCprep_exporter_type == "jmc2obj": + elif exporter is world_tools.WorldExporter.Jmc2OBJ or exporter is world_tools.WorldExporter.ClassicJmc: ops.snapping = "center" else: box = col.box() b_row = box.row() - b_row.label(text="No models loaded") + b_row.label(text=env._("No models loaded")) b_row = box.row() b_row.scale_y = 2 b_row.operator( "mcprep.reload_spawners", - text="Reload assets", icon="ERROR") + text=env._("Reload assets"), icon="ERROR") col = layout.column(align=True) row = col.row(align=True) @@ -1534,9 +1523,10 @@ def model_spawner(self, context: Context) -> None: ops = col.operator("mcprep.import_model_file") ops.location = util.get_cursor_location(context) - if addon_prefs.MCprep_exporter_type == "Mineways": + exporter = world_tools.get_exporter(context) + if exporter is world_tools.WorldExporter.Mineways or exporter is world_tools.WorldExporter.ClassicMW: ops.snapping = "center" - elif addon_prefs.MCprep_exporter_type == "jmc2obj": + elif exporter is world_tools.WorldExporter.Jmc2OBJ or exporter is world_tools.WorldExporter.ClassicJmc: ops.snapping = "offset" split = layout.split() @@ -1546,15 +1536,15 @@ def model_spawner(self, context: Context) -> None: if not scn_props.show_settings_spawner: col.prop( scn_props, "show_settings_spawner", - text="Advanced", icon="TRIA_RIGHT") + text=env._("Advanced"), icon="TRIA_RIGHT") else: col.prop( scn_props, "show_settings_spawner", - text="Advanced", icon="TRIA_DOWN") + text=env._("Advanced"), icon="TRIA_DOWN") box = col.box() b_row = box.row() b_col = b_row.column(align=False) - b_col.label(text="Resource pack") + b_col.label(text=env._("Resource pack")) subrow = b_col.row(align=True) subrow.prop(context.scene, "mcprep_texturepack_path", text="") subrow.operator( @@ -1570,7 +1560,7 @@ def effects_spawner(self, context: Context) -> None: layout = self.layout col = layout.column(align=True) - col.label(text="Load/generate effects") + col.label(text=env._("Load/generate effects")) # Alternate draw approach, using UI list. if scn_props.effects_list: @@ -1596,18 +1586,18 @@ def effects_spawner(self, context: Context) -> None: else: box = col.box() b_row = box.row() - b_row.label(text="No effects loaded") + b_row.label(text=env._("No effects loaded")) b_row = box.row() b_row.scale_y = 2 b_row.operator( "mcprep.reload_spawners", - text="Reload assets", icon="ERROR") + text=env._("Reload assets"), icon="ERROR") col = layout.column(align=True) row = col.row(align=True) row.scale_y = 1.5 row.enabled = False - row.operator("mcprep.spawn_item", text="Add effect") + row.operator("mcprep.spawn_item", text=env._("Add effect")) row = col.row(align=True) ops = row.operator("mcprep.spawn_particle_planes") ops.location = util.get_cursor_location(context) @@ -1635,33 +1625,33 @@ def effects_spawner(self, context: Context) -> None: if not scn_props.show_settings_effect: col.prop( scn_props, "show_settings_effect", - text="Advanced", icon="TRIA_RIGHT") + text=env._("Advanced"), icon="TRIA_RIGHT") else: col.prop( scn_props, "show_settings_effect", - text="Advanced", icon="TRIA_DOWN") + text=env._("Advanced"), icon="TRIA_DOWN") box = col.box() b_row = box.row() b_col = b_row.column(align=False) - b_col.label(text="Effects folder") + b_col.label(text=env._("Effects folder")) subrow = b_col.row(align=True) subrow.prop(context.scene, "mcprep_effects_path", text="") subrow.operator("mcprep.effects_path_reset", icon=LOAD_FACTORY, text="") - box.label(text="Texture pack folder") + box.label(text=env._("Texture pack folder")) row = box.row(align=True) row.prop(context.scene, "mcprep_texturepack_path", text="") row.operator("mcprep.reset_texture_path", text="", icon=LOAD_FACTORY) base = bpy.path.abspath(context.scene.mcprep_effects_path) if not os.path.isdir(base): - b_col.label(text="Effects folder not found", icon="ERROR") + b_col.label(text=env._("Effects folder not found"), icon="ERROR") elif not os.path.isdir(os.path.join(base, "collection")): - b_col.label(text="Effects/collection folder not found", icon="ERROR") + b_col.label(text=env._("Effects/collection folder not found"), icon="ERROR") elif not os.path.isdir(os.path.join(base, "geonodes")): - b_col.label(text="Effects/geonodes folder not found", icon="ERROR") + b_col.label(text=env._("Effects/geonodes folder not found"), icon="ERROR") elif not os.path.isdir(os.path.join(base, "particle")): - b_col.label(text="Effects/particle folder not found", icon="ERROR") + b_col.label(text=env._("Effects/particle folder not found"), icon="ERROR") b_row = box.row() b_col = b_row.column(align=True) b_col.operator("mcprep.reload_effects") @@ -1669,7 +1659,7 @@ def effects_spawner(self, context: Context) -> None: class MCPREP_PT_spawn(bpy.types.Panel): """MCprep panel for mob spawning""" - bl_label = "Spawner" + bl_label = env._("Spawner") bl_space_type = "VIEW_3D" bl_region_type = 'UI' bl_category = "MCprep" @@ -1679,7 +1669,7 @@ def draw(self, context): restart_layout(self.layout) return row = self.layout.row(align=True) - row.label(text="Click triangle to open") + row.label(text=env._("Click triangle to open")) ops = row.operator( "mcprep.open_help", text="", icon="QUESTION", emboss=False) ops.url = "https://theduckcow.com/dev/blender/mcprep/mcprep-spawner/" @@ -1688,7 +1678,7 @@ def draw(self, context): class MCPREP_PT_mob_spawner(bpy.types.Panel): """MCprep panel for mob spawning""" - bl_label = "Mob spawner" + bl_label = env._("Mob spawner") bl_parent_id = "MCPREP_PT_spawn" bl_space_type = "VIEW_3D" bl_region_type = 'UI' @@ -1716,7 +1706,7 @@ def draw_header(self, context): class MCPREP_PT_model_spawner(bpy.types.Panel): """MCprep panel for model/block spawning""" - bl_label = "Block (model) spawner" + bl_label = env._("Block (model) spawner") bl_parent_id = "MCPREP_PT_spawn" bl_space_type = "VIEW_3D" bl_region_type = 'UI' @@ -1744,7 +1734,7 @@ def draw_header(self, context): class MCPREP_PT_item_spawner(bpy.types.Panel): """MCprep panel for item spawning""" - bl_label = "Item spawner" + bl_label = env._("Item spawner") bl_parent_id = "MCPREP_PT_spawn" bl_space_type = "VIEW_3D" bl_region_type = 'UI' @@ -1773,7 +1763,7 @@ def draw_header(self, context): class MCPREP_PT_effects_spawner(bpy.types.Panel): """MCprep panel for effects spawning""" - bl_label = "Effects + weather" + bl_label = env._("Effects + weather") bl_parent_id = "MCPREP_PT_spawn" bl_space_type = "VIEW_3D" bl_region_type = 'UI' @@ -1801,7 +1791,7 @@ def draw_header(self, context): class MCPREP_PT_entity_spawner(bpy.types.Panel): """MCprep panel for entity spawning""" - bl_label = "Entity spawner" + bl_label = env._("Entity spawner") bl_parent_id = "MCPREP_PT_spawn" bl_space_type = "VIEW_3D" bl_region_type = 'UI' @@ -1829,7 +1819,7 @@ def draw_header(self, context): class MCPREP_PT_meshswap_spawner(bpy.types.Panel): """MCprep panel for meshswap spawning""" - bl_label = "Meshswap spawner" + bl_label = env._("Meshswap spawner") bl_parent_id = "MCPREP_PT_spawn" bl_space_type = "VIEW_3D" bl_region_type = 'UI' @@ -1876,7 +1866,7 @@ def mcprep_uv_tools(self, context: Context) -> None: """Appended to UV tools in UV image editor tab, in object edit mode.""" layout = self.layout layout.separator() - layout.label(text="MCprep tools") + layout.label(text=env._("MCprep tools")) col = layout.column(align=True) col.operator("mcprep.scale_uv") col.operator("mcprep.select_alpha_faces") @@ -1930,10 +1920,6 @@ class McprepProps(bpy.types.PropertyGroup): name="show skin settings", description="Show extra MCprep panel settings", default=False) - show_settings_optimizer: bpy.props.BoolProperty( - name="show optimizer settings", - description="Show extra MCprep panel settings", - default=False) show_settings_spawner: bpy.props.BoolProperty( name="show spawner settings", description="Show extra MCprep panel settings", diff --git a/MCprep_addon/optimize_scene.py b/MCprep_addon/optimize_scene.py deleted file mode 100644 index 2f3412a7..00000000 --- a/MCprep_addon/optimize_scene.py +++ /dev/null @@ -1,442 +0,0 @@ -# ##### BEGIN GPL LICENSE BLOCK ##### -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software Foundation, -# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. -# -# ##### END GPL LICENSE BLOCK ##### - -# Note from Mahid Sheikh, December 24th, 2023 -# -# The MCprep optimizer is to be deprecated in MCprep 3.5.2, and -# removed in MCprep 3.6. This is for the following reasons: -# - Extremely buggy algorithm -# - There's been times where MCprep has set the max bounces -# to 80 or higher, because of the way the algorithm assumes -# the scene's properties. -# -# - The algorithm was made to work around the hardware I assumed -# a regular user would have (based on what I've seen on Minecraft -# animation servers), which means no actual scene profiling, just -# assumptions of how the scene could turn out based on materials. -# -# - Not caught up with modern Cycles improvements -# - This was written when Cycles X was still in development. Today, -# Cycles has massively improved, and Cycles X is the default, but -# the optimizer hasn't changed at all. -# -# - Better tools exist for automatic optimization, though nothing -# beats manual optimization - -import bpy -from bpy.types import Context, UILayout, Node -import addon_utils - -from . import util -from .materials import generate - - -MAX_BOUNCES = 8 # 8 is generally the standard for light bounces -MIN_BOUNCES = 2 # 2 is the lowest as it avoids issues regarding glossy -CMP_BOUNCES = MIN_BOUNCES * 2 # To avoid bounces from going bellow 2 -MAX_FILTER_glossy = 1.0 # Standard in Blender -MAX_STEPS = 200 # 200 is fine for most scenes -MIN_SCRAMBLING_MULTIPLIER = 0.35 # 0.35 seems to help performance a lot, but we'll do edits to this value depending on materials -SCRAMBLING_MULTIPLIER_ADD = 0.05 # This is how much we'll add to the srambling distance multiplier -CMP_SCRAMBLING_MULTIPLIER = MIN_SCRAMBLING_MULTIPLIER * 3 # The max since beyond this performance doesn't improve as much -VOLUMETRIC_NODES = ["ShaderNodeVolumeScatter", "ShaderNodeVolumeAbsorption", "ShaderNodeVolumePrincipled"] - -# MCprep Node Settings -MCPREP_HOMOGENOUS_volume = "MCPREP_HOMOGENOUS_VOLUME" -MCPREP_NOT_HOMOGENOUS_volume = "MCPREP_NOT_HOMOGENOUS_VOLUME" - - -class MCprepOptimizerProperties(bpy.types.PropertyGroup): - def scene_brightness(self, context): - itms = [ - ("BRIGHT", "Scene is bright", "Use this setting if your scene is mostly bright\nEx. outside during the day"), - ("DARK", "Scene is dark", "Use this setting if your scene is mostly dark\nEx. caves, interiors, and outside at night") - ] - return itms - - caustics_bool: bpy.props.BoolProperty( - name="Caustics (slower)", - default=False, - description="If checked allows cautics to be enabled" - ) - motion_blur_bool: bpy.props.BoolProperty( - name="Motion Blur (slower)", - default=False, - description="If checked allows motion blur to be enabled" - ) - scene_brightness: bpy.props.EnumProperty( - name="", - description="Brightness of the scene: Affects how the optimizer adjusts sampling", - items=scene_brightness - ) - quality_vs_speed: bpy.props.BoolProperty( - name="Optimize scene for quality: Makes the optimizer adjust settings in a less \"destructive\" way", - default=True - ) - simplify: bpy.props.BoolProperty( - name="Simplify the viewport: Reduces subdivisions to 0. Only disable if any assets will break when using this", - default=True - ) - scrambling_unsafe: bpy.props.BoolProperty( - name="Automatic Scrambling Distance: Can cause artifacts when rendering", - default=False - ) - preview_scrambling: bpy.props.BoolProperty( - name="Preview Scrambling in the viewport", - default=True - ) - - -def panel_draw(context: Context, element: UILayout): - box = element.box() - col = box.column() - engine = context.scene.render.engine - scn_props = context.scene.optimizer_props - if engine == 'CYCLES': - col.label(text="Options") - quality_icon = "INDIRECT_ONLY_ON" if scn_props.quality_vs_speed else "INDIRECT_ONLY_OFF" - col.prop(scn_props, "quality_vs_speed", icon=quality_icon) - col.prop(scn_props, "simplify", icon=quality_icon) - - col.label(text="Time of Day") - col.prop(scn_props, "scene_brightness") - col.prop(scn_props, "caustics_bool", icon="TRIA_UP") - col.prop(scn_props, "motion_blur_bool", icon="TRIA_UP") - col.row() - col.label(text="Unsafe Options! Use at your own risk!") - if util.bv30(): - scrambling_unsafe_icon = "TRIA_DOWN" if scn_props.scrambling_unsafe else "TRIA_RIGHT" - col.prop(scn_props, "scrambling_unsafe", icon=scrambling_unsafe_icon) - if scn_props.scrambling_unsafe: - col.prop(scn_props, "preview_scrambling") - col.row() - col.label(text="") - subrow = col.row() - subrow.scale_y = 1.5 - subrow.operator("mcprep.optimize_scene", text="Optimize Scene (Deprecated)") - else: - col.label(text="Cycles Only :C") - - -class MCPrep_OT_optimize_scene(bpy.types.Operator): - bl_idname = "mcprep.optimize_scene" - bl_label = "Optimize Scene" - bl_options = {'REGISTER', 'UNDO'} - - def __init__(self) -> None: - # Sampling Settings. - self.samples = bpy.context.scene.cycles.samples # We will be doing some minor adjustments to the sample count - self.minimum_samples = None - self.noise_threshold = 0.2 - - # Light Bounces. - self.diffuse = 2 # This is default because diffuse bounces don't need to be high - self.glossy = 1 - self.transmissive = 1 - self.volume = 2 - - # volumetric Settings. - self.max_steps = 100 - self.stepping_rate = 5 - self.homogenous_volumes = 0 - self.not_homogenous_volumes = 0 - - # Filter glossy and clamping settings. - self.filter_glossy = 1 - self.clamping_indirect = 1 - - # Motion blur, caustics, etc. - self.motion_blur = None - self.reflective_caustics = None - self.refractive_caustics = None - - # Optimizer Settings. - self.quality = None - self.uses_scrambling = None - self.preview_scrambling = None - self.scrambling_multiplier = MIN_SCRAMBLING_MULTIPLIER - - def is_vol(self, context: Context, node: Node) -> None: - density_socket = node.inputs["Density"] # Grab the density - node_name = util.nameGeneralize(node.name).rstrip() # Get the name (who knew this could be used on nodes?) - # Sometimes there may be something linked to the density but it's fine to treat it as a homogeneous volume - # This allows the user to control the addon at the node level - if not density_socket.is_linked: - if node_name == MCPREP_NOT_HOMOGENOUS_volume: - self.not_homogenous_volumes -= 1 - else: - self.homogenous_volumes += 1 - else: - if node_name == MCPREP_HOMOGENOUS_volume: - self.homogenous_volumes += 1 - else: - self.not_homogenous_volumes += 1 - - self.scrambling_multiplier += SCRAMBLING_MULTIPLIER_ADD - if self.scrambling_multiplier >= CMP_SCRAMBLING_MULTIPLIER: # at this point, it's worthless to keep it enabled - self.uses_scrambling = False - self.preview_scrambling = False - self.scrambling_multiplier = 1.0 - - print(self.homogenous_volumes, " ", self.not_homogenous_volumes) - - def is_pricipled(self, context: Context, mat_type: str, node: Node) -> None: - if mat_type == "reflective": - roughness_socket = node.inputs["Roughness"] - if not roughness_socket.is_linked and roughness_socket.default_value >= 0.2: - self.glossy = self.glossy - 1 if self.glossy > 2 else 2 - elif mat_type == "glass": - if "Transmission" in node.inputs: # pre 4.0 way - transmission_socket = node.inputs["Transmission"] - else: # Blender 4.0+ - transmission_socket = node.inputs["Transmission Weight"] - if not transmission_socket.is_linked and transmission_socket.default_value >= 0: - self.transmissive = self.transmissive - 2 if self.transmissive > 1 else 2 - - def execute(self, context): - # ! Calling this twice seems to remove all unused materials - # TODO: find a better way of doing this - try: - bpy.ops.outliner.orphans_purge() - bpy.ops.outliner.orphans_purge() - except Exception as e: - print("Failed to purge orphans:") - print(e) - prefs = util.get_preferences(context) - cprefs = prefs.addons.get("cycles") - scn_props = context.scene.optimizer_props - - # Get the compute device type. - cycles_compute_device_type = None - has_active_device = cprefs.preferences.has_active_device() - if cprefs is not None and has_active_device: - cycles_compute_device_type = cprefs.preferences.compute_device_type - - # Sampling Settings. - self.samples = bpy.context.scene.cycles.samples # We will be doing some minor adjustments to the sample count - self.minimum_samples = None - self.noise_threshold = 0.2 - - - # Light Bounces. - self.diffuse = 2 # This is default because diffuse bounces don't need to be high - self.glossy = 1 - self.transmissive = 1 - self.volume = 2 - - # volumetric Settings. - self.max_steps = 100 - self.stepping_rate = 5 - self.homogenous_volumes = 0 - self.not_homogenous_volumes = 0 - - # Filter glossy and clamping settings. - self.filter_glossy = 1 - self.clamping_indirect = 1 - - # Motion blur, caustics, etc. - self.motion_blur = scn_props.motion_blur_bool - self.reflective_caustics = scn_props.caustics_bool - self.refractive_caustics = scn_props.caustics_bool - - # Optimizer Settings. - self.quality = scn_props.quality_vs_speed - self.uses_scrambling = scn_props.scrambling_unsafe - self.preview_scrambling = scn_props.preview_scrambling - self.scrambling_multiplier = MIN_SCRAMBLING_MULTIPLIER - - # Time of day. - if scn_props.scene_brightness == "BRIGHT": - self.noise_threshold = 0.2 - else: - self.samples += 20 - self.noise_threshold = 0.05 - - if self.quality: - self.minimum_samples = self.samples // 4 - self.filter_glossy = MAX_FILTER_glossy // 2 - self.max_steps = MAX_STEPS # TODO: Add better volumetric optimizations - - else: - self.minimum_samples = self.samples // 8 - self.filter_glossy = MAX_FILTER_glossy - self.max_steps = MAX_STEPS // 2 # TODO: Add better volumetric optimizations - - # Compute device. - if cycles_compute_device_type == "NONE": - if util.bv30() is False: - try: - addon_utils.enable("render_auto_tile_size", default_set=True) - val_1, val_2 = addon_utils.check("render_auto_tile_size") - if val_1 is not True: - bpy.context.scene.render.tile_x = 32 - bpy.context.scene.render.tile_y = 32 - except Exception: - bpy.context.scene.render.tile_x = 32 - bpy.context.scene.render.tile_y = 32 - - elif cycles_compute_device_type in ("CUDA", "HIP"): - if util.bv30() is False: - try: - addon_utils.enable("render_auto_tile_size", default_set=True) - val_1, val_2 = addon_utils.check("render_auto_tile_size") - if val_1 is not True: - bpy.context.scene.render.tile_x = 256 - bpy.context.scene.render.tile_y = 256 - except Exception: - bpy.context.scene.render.tile_x = 256 - bpy.context.scene.render.tile_y = 256 - - elif cycles_compute_device_type == "OPTIX": - if util.bv30() is False: - try: - addon_utils.enable("render_auto_tile_size", default_set=True) - val_1, val_2 = addon_utils.check("render_auto_tile_size") - if val_1 is not True: - bpy.context.scene.render.tile_x = 512 - bpy.context.scene.render.tile_y = 512 - except Exception: - bpy.context.scene.render.tile_x = 512 - bpy.context.scene.render.tile_y = 512 - - elif cycles_compute_device_type == "OPENCL": # Always in any version of Blender pre-3.0 - try: - addon_utils.enable("render_auto_tile_size", default_set=True) - val_1, val_2 = addon_utils.check("render_auto_tile_size") - if val_1 is not True: - bpy.context.scene.render.tile_x = 256 - bpy.context.scene.render.tile_y = 256 - except Exception: - bpy.context.scene.render.tile_x = 256 - bpy.context.scene.render.tile_y = 256 - - # Cycles Render Settings Optimizations. - for mat in bpy.data.materials: - matGen = util.nameGeneralize(mat.name) - canon, form = generate.get_mc_canonical_name(matGen) - mat_type = None - if generate.checklist(canon, "reflective"): - self.glossy += 1 - mat_type = "reflective" - if generate.checklist(canon, "glass"): - self.transmissive += 1 - mat_type = "glass" - - if mat.use_nodes: - nodes = mat.node_tree.nodes - for node in nodes: - print(node.bl_idname) - if node.bl_idname in VOLUMETRIC_NODES: - self.is_vol(context, node) - - # Not the best check, but better then nothing - if node.bl_idname == "ShaderNodeBsdfPrincipled": - self.is_pricipled(context, mat_type, node) - - if self.homogenous_volumes > 0 or self.not_homogenous_volumes > 0: - volumes_rate = self.homogenous_volumes - self.not_homogenous_volumes - if volumes_rate > 0: - self.stepping_rate += 2 * volumes_rate - mat.cycles.homogenous_volume = True - elif volumes_rate < 0: - self.stepping_rate -= 2 * abs(volumes_rate) # get the absolute value of volumes_rate since it's negative - if self.stepping_rate < 2: - self.stepping_rate = 2 # 2 is the lowest stepping rate - - - """ - The reason we divide by 2 only if the bounces are greater then or equal to CMP_BOUNCES (MIN_BOUNCES * 2) is to - prevent the division operation from setting the bounces below MIN_BOUNCES. - - For instance, if the glossy bounces are 3, and we divide by 2 anyway, the glossy bounces would be set to 1 (we're using // for integer division), which - may cause issues when dealing with multiple glossy objects. - - However, this is not an issue in this case since 3 is less then CMP_BOUNCES (by default anyway) - """ - if self.glossy >= CMP_BOUNCES: - self.glossy = self.glossy // 2 - if self.transmissive >= CMP_BOUNCES: - self.transmissive = self.transmissive // 2 - - local_max_bounce = MAX_BOUNCES - if self.glossy > local_max_bounce: - local_max_bounce = self.glossy - - if self.transmissive > local_max_bounce: - local_max_bounce = self.transmissive - - # Sampling settings - # Adaptive sampling is something from Blender 2.9+ - if util.min_bv((2, 90)): - bpy.context.scene.cycles.adaptive_threshold = self.noise_threshold - bpy.context.scene.cycles.adaptive_min_samples = self.minimum_samples - # Scrambling distance is a 3.0 feature - if util.bv30() and self.uses_scrambling: - bpy.context.scene.cycles.auto_scrambling_distance = self.uses_scrambling - bpy.context.scene.cycles.preview_scrambling_distance = self.preview_scrambling - bpy.context.scene.cycles.scrambling_distance = self.scrambling_multiplier - - if self.uses_scrambling is not False: - bpy.context.scene.cycles.min_light_bounces = 1 - bpy.context.scene.cycles.min_transparent_bounces = 2 - - bpy.context.scene.cycles.samples = self.samples - - # volumetric settings - bpy.context.scene.cycles.volume_max_steps = self.max_steps - bpy.context.scene.cycles.volume_step_rate = self.stepping_rate - bpy.context.scene.cycles.volume_preview_step_rate = self.stepping_rate - - # Bounce settings - bpy.context.scene.cycles.max_bounces = local_max_bounce - bpy.context.scene.cycles.diffuse_bounces = self.diffuse - bpy.context.scene.cycles.volume_bounces = self.volume - bpy.context.scene.cycles.glossy_bounces = self.glossy - bpy.context.scene.cycles.transmission_bounces = self.transmissive - - # Settings related to glossy and transmissive materials - bpy.context.scene.cycles.blur_glossy = self.filter_glossy - bpy.context.scene.cycles.caustics_reflective = self.reflective_caustics - bpy.context.scene.cycles.caustics_refractive = self.refractive_caustics - bpy.context.scene.cycles.sample_clamp_indirect = self.clamping_indirect - - # Sometimes people don't want to use simplify because it messes with rigs - bpy.context.scene.render.use_simplify = scn_props.simplify - bpy.context.scene.render.simplify_subdivision = 0 - bpy.context.scene.render.use_motion_blur = self.motion_blur - return {'FINISHED'} - - -classes = ( - MCprepOptimizerProperties, - MCPrep_OT_optimize_scene -) - - -def register(): - for cls in classes: - bpy.utils.register_class(cls) - - bpy.types.Scene.optimizer_props = bpy.props.PointerProperty( - type=MCprepOptimizerProperties) - - -def unregister(): - for cls in reversed(classes): - bpy.utils.unregister_class(cls) - del bpy.types.Scene.optimizer_props diff --git a/MCprep_addon/spawner/item.py b/MCprep_addon/spawner/item.py index f66bca1e..f30add36 100644 --- a/MCprep_addon/spawner/item.py +++ b/MCprep_addon/spawner/item.py @@ -238,7 +238,7 @@ def spawn_item_from_filepath( mat.use_transparency = 1 mat.alpha = 0 mat.texture_slots[0].use_map_alpha = 1 - elif engine == 'CYCLES' or engine == 'BLENDER_EEVEE': + elif engine == 'CYCLES' or engine == 'BLENDER_EEVEE' or engine == 'BLENDER_EEVEE_NEXT': mat.use_nodes = True nodes = mat.node_tree.nodes links = mat.node_tree.links diff --git a/MCprep_addon/spawner/mcmodel.py b/MCprep_addon/spawner/mcmodel.py index aafd84ab..dc71e3aa 100644 --- a/MCprep_addon/spawner/mcmodel.py +++ b/MCprep_addon/spawner/mcmodel.py @@ -136,7 +136,7 @@ def add_material( passes[pass_name] = None # Prep material # Halt if no diffuse image found - if engine == 'CYCLES' or engine == 'BLENDER_EEVEE': + if engine == 'CYCLES' or engine == 'BLENDER_EEVEE' or engine == 'BLENDER_EEVEE_NEXT': options = generate.PrepOptions( passes=passes, use_reflections=False, diff --git a/MCprep_addon/spawner/meshswap.py b/MCprep_addon/spawner/meshswap.py index ea865051..8a245fb5 100644 --- a/MCprep_addon/spawner/meshswap.py +++ b/MCprep_addon/spawner/meshswap.py @@ -20,6 +20,7 @@ from dataclasses import dataclass from typing import Dict, List, Union, Tuple import math +from MCprep_addon import world_tools import mathutils import os import random @@ -465,8 +466,7 @@ class MCPREP_OT_meshswap(bpy.types.Operator): @classmethod def poll(cls, context): - addon_prefs = util.get_user_preferences(context) - return addon_prefs.MCprep_exporter_type != "(choose)" and context.mode == 'OBJECT' + return world_tools.get_exporter(context) != world_tools.WorldExporter.Unknown and context.mode == 'OBJECT' def invoke(self, context, event): return context.window_manager.invoke_props_dialog( @@ -517,6 +517,8 @@ def draw(self, context): @tracking.report_error def execute(self, context): tprep = time.time() + + # NOTE: This is temporary addon_prefs = util.get_user_preferences(context) self.track_exporter = addon_prefs.MCprep_exporter_type diff --git a/MCprep_addon/spawner/mobs.py b/MCprep_addon/spawner/mobs.py index 91508d4a..1460e3f4 100644 --- a/MCprep_addon/spawner/mobs.py +++ b/MCprep_addon/spawner/mobs.py @@ -256,7 +256,7 @@ def draw(self, context): row.prop(self, "clearPose") row = self.layout.row(align=True) engine = context.scene.render.engine - if engine == 'CYCLES' or engine == 'BLENDER_EEVEE': + if engine == 'CYCLES' or engine == 'BLENDER_EEVEE' or engine == 'BLENDER_EEVEE_NEXT': row.prop(self, "prep_materials") else: row.prop(self, "prep_materials", text="Prep materials") diff --git a/MCprep_addon/spawner/spawn_util.py b/MCprep_addon/spawner/spawn_util.py index a8031347..ae1ce8d0 100644 --- a/MCprep_addon/spawner/spawn_util.py +++ b/MCprep_addon/spawner/spawn_util.py @@ -24,7 +24,7 @@ import bpy from bpy.types import Context, Collection, BlendDataLibraries -from ..conf import env +from ..conf import MCprepError, env from .. import util from .. import tracking from . import mobs @@ -372,6 +372,7 @@ def load_linked(self, context: Context, path: str, name: str) -> None: path = bpy.path.abspath(path) act = None + res = None if hasattr(bpy.data, "groups"): res = util.bAppendLink(f"{path}/Group", name, True) act = context.object # assumption of object after linking, 2.7 only @@ -382,10 +383,10 @@ def load_linked(self, context: Context, path: str, name: str) -> None: else: print("Error: Should have had at least one object selected.") - if res is False: + if isinstance(res, MCprepError): # Most likely scenario, path was wrong and raised "not a library". # end and automatically reload assets. - self.report({'WARNING'}, "Failed to load asset file") + self.report({'WARNING'}, res.msg) bpy.ops.mcprep.prompt_reset_spawners('INVOKE_DEFAULT') return diff --git a/MCprep_addon/util.py b/MCprep_addon/util.py index 09045ff8..207bbf00 100644 --- a/MCprep_addon/util.py +++ b/MCprep_addon/util.py @@ -26,6 +26,7 @@ import random import re import subprocess +from MCprep_addon.commonmcobj_parser import CommonMCOBJTextureType import bpy from bpy.types import ( @@ -39,7 +40,7 @@ ) from mathutils import Vector, Matrix -from .conf import env +from .conf import MCprepError, env # Commonly used name for an excluded collection in Blender 2.8+ SPAWNER_EXCLUDE = "Spawner Exclude" @@ -48,31 +49,55 @@ # GENERAL SUPPORTING FUNCTIONS (no registration required) # ----------------------------------------------------------------------------- +def update_matrices(obj): + """Update mattrices of object so that we can accurately parent, + because for some stupid reason, Blender doesn't do this by default""" + if obj.parent is None: + obj.matrix_world = obj.matrix_basis + + else: + obj.matrix_world = obj.parent.matrix_world * \ + obj.matrix_parent_inverse * \ + obj.matrix_basis -def apply_colorspace(node: Node, color_enum: Tuple) -> None: - """Apply color space in a cross compatible way, for version and language. - Use enum nomeclature matching Blender 2.8x Default, not 2.7 or other lang +def apply_noncolor_data(node: Node) -> Optional[MCprepError]: """ - global noncolor_override - noncolor_override = None + Apply the Non-Color/Generic Data option to the passed + node in a way that is cross version compatible, as well as OCIO + config compatible in theory. + + Returns: + If success: None + If fail: + All cases - MCprepError(TypeError) + """ + options: List[str] = [] + if env.json_data: + options = env.json_data["non_color_options"] if not node.image: env.log("Node has no image applied yet, cannot change colorspace") - - # For later 2.8, fix images color space user - if hasattr(node, "color_space"): # 2.7 and earlier 2.8 versions - node.color_space = 'NONE' # for better interpretation of specmaps - elif hasattr(node.image, "colorspace_settings"): # later 2.8 versions - # try default 'Non-color', fall back to best guess 'Non-Colour Data' - if color_enum == 'Non-color' and noncolor_override is not None: - node.image.colorspace_settings.name = noncolor_override - else: + + # Blender 2.8+ + if hasattr(node.image, "colorspace_settings"): + # Avoid hard-coding values into the + # code so that users with non-standard + # setups can add whatever additional + # options are needed for their OCIO + # setup + for opt in options: try: - node.image.colorspace_settings.name = 'Non-Color' - except TypeError: - node.image.colorspace_settings.name = 'Non-Colour Data' - noncolor_override = 'Non-Colour Data' + node.image.colorspace_settings.name = opt + return None + except TypeError: + continue + (lineno, file) = env.current_line_and_file() + return MCprepError( + TypeError("None of the non-color options work, add a new option to mcprep_data.json"), + lineno, + file + ) def nameGeneralize(name: str) -> str: @@ -122,19 +147,27 @@ def materialsFromObj(obj_list: List[bpy.types.Object]) -> List[Material]: return mat_list -def bAppendLink(directory: str, name: str, toLink: bool, active_layer: bool=True) -> bool: - """For multiple version compatibility, this function generalized - appending/linking blender post 2.71 changed to new append/link methods +def bAppendLink(directory: str, name: str, toLink: bool, active_layer: bool=True) -> Optional[MCprepError]: + """ + This function calls the append and link methods in an + easy and safe manner. Note that for 2.8 compatibility, the directory passed in should already be correctly identified (eg Group or Collection) Arguments: - directory: xyz.blend/Type, where Type is: Collection, Group, Material... - name: asset name + directory: str + xyz.blend/Type, where Type is: Collection, Group, Material... + name: str + Asset name toLink: bool + If true, link instead of append + active_layer: bool=True + Deprecated in MCprep 3.6 as it relates to pre-2.8 layers - Returns: true if successful, false if not. + Returns: + - None if successful + - MCprepError with message if the asset could not be appended or linked """ env.log(f"Appending {directory} : {name}", vv_only=True) @@ -143,17 +176,7 @@ def bAppendLink(directory: str, name: str, toLink: bool, active_layer: bool=True if directory[-1] != "/" and directory[-1] != os.path.sep: directory += os.path.sep - if "link_append" in dir(bpy.ops.wm): - # OLD method of importing, e.g. in blender 2.70 - env.log("Using old method of append/link, 2.72 <=", vv_only=True) - try: - bpy.ops.wm.link_append(directory=directory, filename=name, link=toLink) - return True - except RuntimeError as e: - print("bAppendLink", e) - return False - elif "link" in dir(bpy.ops.wm) and "append" in dir(bpy.ops.wm): - env.log("Using post-2.72 method of append/link", vv_only=True) + if "link" in dir(bpy.ops.wm) and "append" in dir(bpy.ops.wm): if toLink: bpy.ops.wm.link(directory=directory, filename=name) else: @@ -161,10 +184,11 @@ def bAppendLink(directory: str, name: str, toLink: bool, active_layer: bool=True bpy.ops.wm.append( directory=directory, filename=name) - return True + return None except RuntimeError as e: print("bAppendLink", e) - return False + line, file = env.current_line_and_file() + return MCprepError(e, line, file, f"Could not append {name}!") def obj_copy( @@ -215,12 +239,6 @@ def min_bv(version: Tuple, *, inclusive: bool = True) -> bool: return bpy.app.version >= version -def bv28() -> bool: - """Check if blender 2.8, for layouts, UI, and properties. """ - env.deprecation_warning() - return min_bv((2, 80)) - - def bv30() -> bool: """Check if we're dealing with Blender 3.0""" return min_bv((3, 00)) @@ -250,6 +268,12 @@ def is_atlas_export(context: Context) -> bool: file_types["ATLAS"] += 1 else: file_types["INDIVIDUAL"] += 1 + elif "COMMONMCOBJ_HEADER" in obj and obj["PARENTED_EMPTY"] is not None: + tex = CommonMCOBJTextureType[obj["PARENTED_EMPTY"]["texture_type"]] + if tex is CommonMCOBJTextureType.ATLAS: + file_types["ATLAS"] += 1 + elif tex is CommonMCOBJTextureType.INDIVIDUAL_TILES: + file_types["INDIVIDUAL"] += 1 else: continue @@ -327,19 +351,33 @@ def link_selected_objects_to_scene() -> None: if ob not in list(bpy.context.scene.objects): obj_link_scene(ob) - -def open_program(executable: str) -> Union[int, str]: +def open_program(executable: str) -> Optional[MCprepError]: + """ + Runs an executable such as Mineways or jmc2OBJ, taking into account the + user's operating system (using Wine if Mineways is to be launched on + a non-Windows OS such as macOS or Linux) and automatically checks if + the program exists or has the right permissions to be executed. + + Returns: + - None if the program is found and ran successfully + - MCprepError in all error cases (may have error message) + """ # Open an external program from filepath/executbale executable = bpy.path.abspath(executable) env.log(f"Open program request: {executable}") + # Doesn't matter where the exact error occurs + # in this function, since they're all going to + # be crazy hard to decipher + line, file = env.current_line_and_file() + # input could be .app file, which appears as if a folder if not os.path.isfile(executable): env.log("File not executable") if not os.path.isdir(executable): - return -1 + return MCprepError(FileNotFoundError(), line, file) elif not executable.lower().endswith(".app"): - return -1 + return MCprepError(FileNotFoundError(), line, file) # try to open with wine, if available osx_or_linux = platform.system() == "Darwin" @@ -361,13 +399,13 @@ def open_program(executable: str) -> Union[int, str]: # for line in iter(p.stdout.readline, ''): # # will print lines as they come, instead of just at end # print(stdout) - return 0 + return None try: # attempt to use blender's built-in method res = bpy.ops.wm.path_open(filepath=executable) if res == {"FINISHED"}: env.log("Opened using built in path opener") - return 0 + return None else: env.log("Did not get finished response: ", str(res)) except: @@ -381,8 +419,8 @@ def open_program(executable: str) -> Union[int, str]: p = Popen(['open', executable], stdin=PIPE, stdout=PIPE, stderr=PIPE) stdout, err = p.communicate(b"") if err != b"": - return f"Error occured while trying to open executable: {err}" - return "Failed to open executable" + return MCprepError(RuntimeError(), line, file, f"Error occured while trying to open executable: {err!r}") + return MCprepError(RuntimeError(), line, file, "Failed to open executable") def open_folder_crossplatform(folder: str) -> bool: @@ -453,7 +491,8 @@ def load_mcprep_json() -> bool: "canon_mapping_block": {} }, "mob_skip_prep": [], - "make_real": [] + "make_real": [], + "non_color_options" : [], } if not os.path.isfile(path): env.log(f"Error, json file does not exist: {path}") diff --git a/MCprep_addon/world_tools.py b/MCprep_addon/world_tools.py index fd598020..83c871f3 100644 --- a/MCprep_addon/world_tools.py +++ b/MCprep_addon/world_tools.py @@ -16,17 +16,21 @@ # # ##### END GPL LICENSE BLOCK ##### +import enum +from dataclasses import fields +from enum import Enum, auto import os import math from pathlib import Path -from typing import List, Optional +from typing import List, Optional, Union import shutil +from MCprep_addon.commonmcobj_parser import CommonMCOBJ, CommonMCOBJTextureType, parse_header import bpy from bpy.types import Context, Camera from bpy_extras.io_utils import ExportHelper, ImportHelper -from .conf import env, VectorType +from .conf import MCprepError, env, VectorType from . import util from . import tracking from .materials import generate @@ -36,18 +40,10 @@ # supporting functions # ----------------------------------------------------------------------------- -BUILTIN_SPACES = ( - "Standard", - "Filmic", - "Filmic Log", - "Raw", - "False Color" -) - +BUILTIN_SPACES = ('Standard', 'Khronos PBR Neutral', 'AgX', 'Filmic', 'Filmic Log', 'False Color', 'Raw') time_obj_cache = None - def get_time_object() -> None: """Returns the time object if present in the file""" global time_obj_cache # to avoid re parsing every time @@ -79,9 +75,12 @@ class ObjHeaderOptions: """Wrapper functions to avoid typos causing issues.""" def __init__(self): - self._exporter: Optional[str] = None - self._file_type: Optional[str] = None - + # This assumes all OBJs that aren't from Mineways + # and don't have a CommonMCOBJ header are from + # jmc2obj, and use individual tiles for textures + self._exporter: Optional[str] = "jmc2obj" + self._file_type: Optional[str] = "INDIVIDUAL_TILES" + """ Wrapper functions to avoid typos causing issues """ @@ -110,16 +109,104 @@ def texture_type(self): return self._file_type if self._file_type is not None else "NONE" -obj_header = ObjHeaderOptions() +class WorldExporter(Enum): + """ + Defines all supported exporters + with a fallback + """ + + # Mineways with CommonMCOBJ + Mineways = auto() + + # Jmc2OBJ with CommonMCOBJ + Jmc2OBJ = auto() + + # Cmc2OBJ, the reference + # implementation of CommonMCOBJ + # + # For the most part, this + # will be treated as + # Unknown as it's not meant + # for regular use. The distinct + # option exists for testing purposes + Cmc2OBJ = auto() + + # Any untested exporter + Unknown = auto() + # Mineways before the CommonMCOBJ standard + ClassicMW = auto() -def detect_world_exporter(filepath: Path) -> None: + # jmc2OBJ before the CommonMCOBJ standard + ClassicJmc = auto() + + +EXPORTER_MAPPING = { + "mineways" : WorldExporter.Mineways, + "jmc2obj" : WorldExporter.Jmc2OBJ, + "cmc2obj" : WorldExporter.Cmc2OBJ, + "mineways-c" : WorldExporter.ClassicMW, + "jmc2obj-c" : WorldExporter.ClassicJmc +} + +UNSUPPORTED_OR_NONE = (WorldExporter.Unknown, None) + +def get_exporter(context: Context) -> Optional[WorldExporter]: + """ + Return the exporter on the active object if it has + an exporter attribute. + + For maximum backwards compatibility, it'll convert the + explicit options we have in MCprep for world exporters to + WorldExporter enum objects, if the object does not have either + the CommonMCOBJ exporter attribute, or if it does not have the + MCPREP_OBJ_EXPORTER attribute added in MCprep 3.6. This backwards + compatibility will be removed by default in MCprep 4.0 + + Returns: + - WorldExporter if the world exporter can be detected + - None otherwise + """ + obj = context.active_object + if not obj: + return None + + if "COMMONMCOBJ_HEADER" in obj: + if obj["PARENTED_EMPTY"] is not None and obj["PARENTED_EMPTY"]["exporter"] in EXPORTER_MAPPING: + return EXPORTER_MAPPING[obj["PARENTED_EMPTY"]["exporter"]] + else: + return WorldExporter.Unknown + elif "MCPREP_OBJ_HEADER" in obj: + if "MCPREP_OBJ_EXPORTER" in obj: + return EXPORTER_MAPPING[obj["MCPREP_OBJ_EXPORTER"]] + + # This section will be placed behind a legacy + # option in MCprep 4.0, once CommonMCOBJ becomes + # more adopted in exporters + prefs = util.get_user_preferences(context) + if prefs.MCprep_exporter_type == "Mineways": + return WorldExporter.ClassicMW + elif prefs.MCprep_exporter_type == "jmc2obj": + return WorldExporter.ClassicJmc + return None + + +def detect_world_exporter(filepath: Path) -> Union[CommonMCOBJ, ObjHeaderOptions]: """Detect whether Mineways or jmc2obj was used, based on prefix info. Primary heruistic: if detect Mineways header, assert Mineways, else assume jmc2obj. All Mineways exports for a long time have prefix info set in the obj file as comments. """ + obj_header = ObjHeaderOptions() + + # First parse header for commonmcobj + with open(filepath, 'r') as obj_fd: + cmc_header = parse_header(obj_fd) + if cmc_header is not None: + return cmc_header + + # If not found, fall back to recognizing the mineway legacy convention with open(filepath, 'r') as obj_fd: try: header = obj_fd.readline() @@ -142,22 +229,17 @@ def detect_world_exporter(filepath: Path) -> None: "# File type: Export tiles for textures to directory textures", "# File type: Export individual textures to directory tex" ) - print('"{}"'.format(header)) if header in atlas: # If a texture atlas is used obj_header.set_atlas() elif header in tiles: # If the OBJ uses individual textures obj_header.set_seperated() - return + return obj_header except UnicodeDecodeError: print(f"Failed to read first line of obj: {filepath}") - return - obj_header.set_jmc2obj() - # Since this is the default for Jmc2Obj, - # we'll assume this is what the OBJ is using - obj_header.set_seperated() + return obj_header -def convert_mtl(filepath): +def convert_mtl(filepath) -> Union[bool, MCprepError]: """Convert the MTL file if we're not using one of Blender's built in colorspaces @@ -170,8 +252,15 @@ def convert_mtl(filepath): - Add a header at the end Returns: - True if success or skipped, False if failed, or None if skipped + - True if the file was converted + - False if conversion was skipped or it was already converted before + - MCprepError if failed (may return with message) """ + + # Perform this early to get it out of the way + if bpy.context.scene.view_settings.view_transform in BUILTIN_SPACES: + return False + # Check if the MTL exists. If not, then check if it # uses underscores. If still not, then return False mtl = Path(filepath.rsplit(".", 1)[0] + '.mtl') @@ -180,7 +269,8 @@ def convert_mtl(filepath): if mtl_underscores.exists(): mtl = mtl_underscores else: - return False + line, file = env.current_line_and_file() + return MCprepError(FileNotFoundError(), line, file) lines = None copied_file = None @@ -190,12 +280,12 @@ def convert_mtl(filepath): lines = mtl_file.readlines() except Exception as e: print(e) + line, file = env.current_line_and_file() + return MCprepError(e, line, file, "Could not read file!") + + # This checks to see if none of the lines have map_d. If so then skip + if not any("map_d" in s for s in lines): return False - - # This checks to see if the user is using a built-in colorspace or if none of the lines have map_d. If so - # then ignore this file and return None - if bpy.context.scene.view_settings.view_transform in BUILTIN_SPACES or not any("map_d" in s for s in lines): - return None # This represents a new folder that'll backup the MTL filepath original_mtl_path = Path(filepath).parent.absolute() / "ORIGINAL_MTLS" @@ -215,10 +305,11 @@ def convert_mtl(filepath): print("Header " + str(header)) copied_file = shutil.copy2(mtl, original_mtl_path.absolute()) else: - return True + return False except Exception as e: print(e) - return False + line, file = env.current_line_and_file() + return MCprepError(e, line, file) # In this section, we go over each line # and check to see if it begins with map_d. If @@ -231,7 +322,8 @@ def convert_mtl(filepath): lines[index] = "# " + line except Exception as e: print(e) - return False + line, file = env.current_line_and_file() + return MCprepError(e, line, file, "Could not read file!") # This needs to be seperate since it involves writing try: @@ -243,20 +335,35 @@ def convert_mtl(filepath): except Exception as e: print(e) shutil.copy2(copied_file, mtl) - return False + line, file = env.current_line_and_file() + return MCprepError(e, line, file) return True +class OBJImportCode(enum.Enum): + """ + This represents the state of the + OBJ import addon in pre-4.0 versions + of Blender + """ + ALREADY_ENABLED = 0 + DISABLED = 1 -def enble_obj_importer() -> Optional[bool]: - """Checks if obj import is avail and tries to activate if not. - If we fail to enable obj importing, return false. True if enabled, and Non - if nothing changed. +def enable_obj_importer() -> Union[OBJImportCode, MCprepError]: + """ + Checks if the obj import addon (pre-Blender 4.0) is enabled, + and enable it if it isn't enabled. + + Returns: + - OBJImportCode.ALREADY_ENABLED if either enabled already or + the user is using Blender 4.0. + - OBJImportCode.DISABLED if the addon had to be enabled. + - MCprepError with a message if the addon could not be enabled. """ enable_addon = None if util.min_bv((4, 0)): - return None # No longer an addon, native built in. + return OBJImportCode.ALREADY_ENABLED # No longer an addon, native built in. else: in_import_scn = "obj_import" not in dir(bpy.ops.wm) in_wm = "" @@ -264,13 +371,14 @@ def enble_obj_importer() -> Optional[bool]: enable_addon = "io_scene_obj" if enable_addon is None: - return None + return OBJImportCode.ALREADY_ENABLED try: bpy.ops.preferences.addon_enable(module=enable_addon) - return True + return OBJImportCode.DISABLED except RuntimeError: - return False + line, file = env.current_line_and_file() + return MCprepError(Exception(), line, file, "Could not enable the Built-in OBJ importer!") # ----------------------------------------------------------------------------- @@ -295,13 +403,14 @@ class MCPREP_OT_open_jmc2obj(bpy.types.Operator): def execute(self, context): addon_prefs = util.get_user_preferences(context) res = util.open_program(addon_prefs.open_jmc2obj_path) - - if res == -1: - bpy.ops.mcprep.install_jmc2obj('INVOKE_DEFAULT') - return {'CANCELLED'} - elif res != 0: - self.report({'ERROR'}, str(res)) - return {'CANCELLED'} + + if isinstance(res, MCprepError): + if isinstance(res.err_type, FileNotFoundError): + bpy.ops.mcprep.install_jmc2obj('INVOKE_DEFAULT') + return {'CANCELLED'} + else: + self.report({'ERROR'}, res.msg) + return {'CANCELLED'} else: self.report({'INFO'}, "jmc2obj should open soon") return {'FINISHED'} @@ -375,14 +484,16 @@ def execute(self, context): if os.path.isfile(addon_prefs.open_mineways_path): res = util.open_program(addon_prefs.open_mineways_path) else: - res = -1 - - if res == -1: - bpy.ops.mcprep.install_mineways('INVOKE_DEFAULT') - return {'CANCELLED'} - elif res != 0: - self.report({'ERROR'}, str(res)) - return {'CANCELLED'} + # Doesn't matter here, it's a dummy value + res = MCprepError(FileNotFoundError(), -1, "") + + if isinstance(res, MCprepError): + if isinstance(res.err_type, FileNotFoundError): + bpy.ops.mcprep.install_mineways('INVOKE_DEFAULT') + return {'CANCELLED'} + else: + self.report({'ERROR'}, res.msg) + return {'CANCELLED'} else: self.report({'INFO'}, "Mineways should open soon") return {'FINISHED'} @@ -465,24 +576,24 @@ def execute(self, context): # Auto change from MTL to OBJ, latet if's will check if existing. self.filepath = str(new_filename) if not self.filepath: - self.report({"ERROR"}, "File not found, could not import obj") + self.report({"ERROR"}, f"File not found, could not import obj \'{self.filepath}\'") return {'CANCELLED'} if not os.path.isfile(self.filepath): - self.report({"ERROR"}, "File not found, could not import obj") + self.report({"ERROR"}, f"File not found, could not import obj \'{self.filepath}\'") return {'CANCELLED'} if not self.filepath.lower().endswith(".obj"): self.report({"ERROR"}, "You must select a .obj file to import") return {'CANCELLED'} - res = enble_obj_importer() - if res is None: + res = enable_obj_importer() + if res is OBJImportCode.ALREADY_ENABLED: pass - elif res is True: + elif res is OBJImportCode.DISABLED: self.report( {"INFO"}, "FYI: had to enable OBJ imports in user preferences") - elif res is False: - self.report({"ERROR"}, "Built-in OBJ importer could not be enabled") + elif isinstance(res, MCprepError): + self.report({"ERROR"}, res.msg) return {'CANCELLED'} # There are a number of bug reports that come from the generic call @@ -503,10 +614,13 @@ def execute(self, context): # First let's convert the MTL if needed conv_res = convert_mtl(self.filepath) try: - if conv_res is None: - pass # skipped, no issue anyways. - elif conv_res is False: - self.report({"WARNING"}, "MTL conversion failed!") + if isinstance(conv_res, MCprepError): + if isinstance(conv_res.err_type, FileNotFoundError): + self.report({"WARNING"}, "MTL not found!") + elif conv_res.msg is not None: + self.report({"WARNING"}, conv_res.msg) + else: + self.report({"WARNING"}, conv_res.err_type) res = None if util.min_bv((3, 5)): @@ -591,17 +705,93 @@ def execute(self, context): return {'CANCELLED'} prefs = util.get_user_preferences(context) - detect_world_exporter(self.filepath) - prefs.MCprep_exporter_type = obj_header.exporter() - - for obj in context.selected_objects: - obj["MCPREP_OBJ_HEADER"] = True - obj["MCPREP_OBJ_FILE_TYPE"] = obj_header.texture_type() + header = detect_world_exporter(Path(self.filepath)) + + if isinstance(header, ObjHeaderOptions): + prefs.MCprep_exporter_type = header.exporter() + + # Create empty at the center of the OBJ + empty = None + if isinstance(header, CommonMCOBJ): + # Get actual 3D space coordinates of the full bounding box + # + # These are in Minecraft coordinates, so they translate + # from (X, Y, Z) to (X, -Z, Y) + max_pair = (header.export_bounds_max[0] + header.export_offset[0], + (-header.export_bounds_max[2]) + (-header.export_offset[2]), + header.export_bounds_max[1] + header.export_offset[1]) + + min_pair = (header.export_bounds_min[0] + header.export_offset[0], + (-header.export_bounds_min[2]) + (-header.export_offset[2]), + header.export_bounds_min[1] + header.export_offset[1]) + + # Calculate the center of the bounding box + # + # We do this by taking the average of the given + # points, so: + # (x1 + x2) / 2 + # (y1 + y2) / 2 + # (z1 + z2) / 2 + # + # This will give us the midpoints of these + # coordinates, which in turn will correspond + # to the center of the bounding box + location = ( + (max_pair[0] + min_pair[0]) / 2, + (max_pair[1] + min_pair[1]) / 2, + (max_pair[2] + min_pair[2]) / 2) + empty = bpy.data.objects.new( + name=header.world_name + "_mcprep_empty", object_data=None) + empty.empty_display_size = 2 + empty.empty_display_type = 'PLAIN_AXES' + empty.location = location + empty.hide_viewport = True # Hide empty globally + util.update_matrices(empty) + for field in fields(header): + if getattr(header, field.name) is None: + continue + if field.type == CommonMCOBJTextureType: + empty[field.name] = getattr(header, field.name).value + else: + empty[field.name] = getattr(header, field.name) - self.split_world_by_material(context) + else: + empty = bpy.data.objects.new("mcprep_obj_empty", object_data=None) + empty.empty_display_size = 2 + empty.empty_display_type = 'PLAIN_AXES' + empty.hide_viewport = True # Hide empty globally addon_prefs = util.get_user_preferences(context) - self.track_exporter = addon_prefs.MCprep_exporter_type # Soft detect. + + for obj in context.selected_objects: + if isinstance(header, CommonMCOBJ): + obj["COMMONMCOBJ_HEADER"] = True + obj["PARENTED_EMPTY"] = empty + obj.parent = empty + obj.matrix_parent_inverse = empty.matrix_world.inverted() # don't transform object + self.track_exporter = header.exporter + + elif isinstance(header, ObjHeaderOptions): + obj["MCPREP_OBJ_HEADER"] = True + obj["MCPREP_OBJ_FILE_TYPE"] = header.texture_type() + + obj.parent = empty + obj.matrix_parent_inverse = empty.matrix_world.inverted() # don't transform object + + # Future-proofing for MCprep 4.0 when we + # put global exporter options behind a legacy + # option and by default use the object for + # getting the exporter + obj["MCPREP_OBJ_EXPORTER"] = "mineways-c" if header.exporter() == "Mineways" else "jmc2obj-c" + self.track_exporter = addon_prefs.MCprep_exporter_type # Soft detect. + + # One final assignment of the preferences, to avoid doing each loop + val = header.exporter if isinstance(header, CommonMCOBJ) else header.exporter() + addon_prefs.MCprep_exporter_type = "Mineways" if val.lower().startswith("mineways") else "jmc2obj" + + new_col = self.split_world_by_material(context) + new_col.objects.link(empty) # parent empty + return {'FINISHED'} def obj_name_to_material(self, obj): @@ -617,7 +807,7 @@ def obj_name_to_material(self, obj): return obj.name = util.nameGeneralize(mat.name) - def split_world_by_material(self, context: Context) -> None: + def split_world_by_material(self, context: Context) -> bpy.types.Collection: """2.8-only function, split combined object into parts by material""" world_name = os.path.basename(self.filepath) world_name = os.path.splitext(world_name)[0] @@ -637,6 +827,7 @@ def split_world_by_material(self, context: Context) -> None: # Force renames based on material, as default names are not useful. for obj in worldg.objects: self.obj_name_to_material(obj) + return worldg class MCPREP_OT_prep_world(bpy.types.Operator): @@ -660,12 +851,10 @@ def execute(self, context): context.scene.world = bpy.data.worlds.new("MCprep world") if engine == 'CYCLES': self.prep_world_cycles(context) - elif engine == 'BLENDER_EEVEE': + elif engine == 'BLENDER_EEVEE' or engine == 'BLENDER_EEVEE_NEXT': self.prep_world_eevee(context) - elif engine == 'BLENDER_RENDER' or engine == 'BLENDER_GAME': - self.prep_world_internal(context) else: - self.report({'ERROR'}, "Must be cycles, eevee, or blender internal") + self.report({'ERROR'}, "Must be Cycles or EEVEE") return {'FINISHED'} def prep_world_cycles(self, context: Context) -> None: @@ -756,41 +945,7 @@ def prep_world_eevee(self, context: Context) -> None: # Renders faster at a (minor?) cost of the image output # TODO: given the output change, consider make a bool toggle for this - bpy.context.scene.render.use_simplify = True - - def prep_world_internal(self, context): - # check for any suns with the sky setting on; - if not context.scene.world: - return - context.scene.world.use_nodes = False - context.scene.world.horizon_color = (0.00938029, 0.0125943, 0.0140572) - context.scene.world.light_settings.use_ambient_occlusion = True - context.scene.world.light_settings.ao_blend_type = 'MULTIPLY' - context.scene.world.light_settings.ao_factor = 0.1 - context.scene.world.light_settings.use_environment_light = True - context.scene.world.light_settings.environment_energy = 0.05 - context.scene.render.use_shadows = True - context.scene.render.use_raytrace = True - context.scene.render.use_textures = True - - # check for any sunlamps with sky setting - sky_used = False - for lamp in context.scene.objects: - if lamp.type not in ("LAMP", "LIGHT") or lamp.data.type != "SUN": - continue - if lamp.data.sky.use_sky: - sky_used = True - break - if sky_used: - env.log("MCprep sky being used with atmosphere") - context.scene.world.use_sky_blend = False - context.scene.world.horizon_color = (0.00938029, 0.0125943, 0.0140572) - else: - env.log("No MCprep sky with atmosphere") - context.scene.world.use_sky_blend = True - context.scene.world.horizon_color = (0.647705, 0.859927, 0.940392) - context.scene.world.zenith_color = (0.0954261, 0.546859, 1) - + bpy.context.scene.render.use_simplify = True class MCPREP_OT_add_mc_sky(bpy.types.Operator): """Add sun lamp and time of day (dynamic) driver, setup sky with sun and moon""" @@ -802,7 +957,7 @@ def enum_options(self, context: Context) -> List[tuple]: """Dynamic set of enums to show based on engine""" engine = bpy.context.scene.render.engine enums = [] - if bpy.app.version >= (2, 77) and engine in ("CYCLES", "BLENDER_EEVEE"): + if engine in ("CYCLES", "BLENDER_EEVEE", "BLENDER_EEVEE_NEXT"): enums.append(( "world_shader", "Dynamic sky + shader sun/moon", @@ -882,7 +1037,7 @@ def execute(self, context): engine = context.scene.render.engine wname = None - if engine == "BLENDER_EEVEE": + if engine == "BLENDER_EEVEE" or engine == "BLENDER_EEVEE_NEXT": blend = "clouds_moon_sun_eevee.blend" wname = "MCprepWorldEevee" else: @@ -896,20 +1051,10 @@ def execute(self, context): if self.world_type in ("world_static_mesh", "world_static_only"): # Create world dynamically (previous, simpler implementation) new_sun = self.create_sunlamp(context) - new_objs.append(new_sun) - - if engine in ('BLENDER_RENDER', 'BLENDER_GAME'): - world = context.scene.world - if not world: - world = bpy.data.worlds.new("MCprep World") - context.scene.world = world - new_sun.data.shadow_method = 'RAY_SHADOW' - new_sun.data.shadow_soft_size = 0.5 - world.use_sky_blend = False - world.horizon_color = (0.00938029, 0.0125943, 0.0140572) + new_objs.append(new_sun) bpy.ops.mcprep.world(skipUsage=True) # do rest of sky setup - elif engine == 'CYCLES' or engine == 'BLENDER_EEVEE': + elif engine == 'CYCLES' or engine == 'BLENDER_EEVEE' or engine == 'BLENDER_EEVEE_NEXT': if not os.path.isfile(blendfile): self.report( {'ERROR'}, @@ -920,35 +1065,7 @@ def execute(self, context): if wname in bpy.data.worlds: prev_world = bpy.data.worlds[wname] prev_world.name = "-old" - new_objs += self.create_dynamic_world(context, blendfile, wname) - - elif engine == 'BLENDER_RENDER' or engine == 'BLENDER_GAME': - # dynamic world using built-in sun sky and atmosphere - new_sun = self.create_sunlamp(context) - new_objs.append(new_sun) - new_sun.data.shadow_method = 'RAY_SHADOW' - new_sun.data.shadow_soft_size = 0.5 - - world = context.scene.world - if not world: - world = bpy.data.worlds.new("MCprep World") - context.scene.world = world - world.use_sky_blend = False - world.horizon_color = (0.00938029, 0.0125943, 0.0140572) - - # be sure to turn off all other sun lamps with atmosphere set - new_sun.data.sky.use_sky = True # use sun orientation settings if BI - for lamp in context.scene.objects: - if lamp.type not in ("LAMP", "LIGHT") or lamp.data.type != "SUN": - continue - if lamp == new_sun: - continue - lamp.data.sky.use_sky = False - - time_obj = get_time_object() - if not time_obj: - env.log( - "TODO: implement create time_obj, parent sun to it & driver setup") + new_objs += self.create_dynamic_world(context, blendfile, wname) if self.world_type in ("world_static_mesh", "world_mesh"): if not os.path.isfile(blendfile): @@ -1017,10 +1134,7 @@ def execute(self, context): def create_sunlamp(self, context: Context) -> bpy.types.Object: """Create new sun lamp from primitives""" - if hasattr(bpy.data, "lamps"): # 2.7 - newlamp = bpy.data.lamps.new("Sun", "SUN") - else: # 2.8 - newlamp = bpy.data.lights.new("Sun", "SUN") + newlamp = bpy.data.lights.new("Sun", "SUN") obj = bpy.data.objects.new("Sunlamp", newlamp) obj.location = (0, 0, 20) obj.rotation_euler[0] = 0.481711 diff --git a/README.md b/README.md index 02cb3399..f3d88e79 100644 --- a/README.md +++ b/README.md @@ -98,7 +98,7 @@ Feature list CREDIT ====== -While this addon is released as open source software, the assets are being released as [Creative Commons Attributions, CC-BY](https://creativecommons.org/licenses/by/3.0/us/). If you use MeshSwap, **please credit the creators** by linking to this page wherever your project may appear: [http://github.com/TheDuckCow/MCprep](https://github.com/TheDuckCow/MCprep) +While this addon is released as open source software under the GNU GPL license, the assets are being released as [Creative Commons Attributions, CC-BY](https://creativecommons.org/licenses/by/3.0/us/). If you use MeshSwap, **please credit the creators** by linking to this page wherever your project may appear: [http://github.com/TheDuckCow/MCprep](https://github.com/TheDuckCow/MCprep). In addition, different parts of MCprep are under different GPL compatible licenses (see `LICENSE-3RD-PARTY.txt`). Meshswap Block models developed by [Patrick W. Crawford](https://twitter.com/TheDuckCow), [SilverC16](http://youtube.com/user/silverC16), and [Nils Söderman (rymdnisse)](http://youtube.com/rymdnisse). diff --git a/action-scripts/default.py b/action-scripts/default.py new file mode 100644 index 00000000..39c54763 --- /dev/null +++ b/action-scripts/default.py @@ -0,0 +1,7 @@ +from default_scripts import ignore_filters + +def main(): + ignore_filters.main() + +if __name__ == "__main__": + main() diff --git a/action-scripts/default_scripts/__init__.py b/action-scripts/default_scripts/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/action-scripts/default_scripts/ignore_filters.py b/action-scripts/default_scripts/ignore_filters.py new file mode 100644 index 00000000..30fd9abe --- /dev/null +++ b/action-scripts/default_scripts/ignore_filters.py @@ -0,0 +1,8 @@ +IGNORE_FILTERS = ["**/mcprep_addon_tracker.json"] + +def main(): + from pathlib import Path + for i in IGNORE_FILTERS: + for p in Path(".").glob(i): + p.unlink() + print(f"Ignore Filter: Deleted {p}! Please remove it from the addon directory") diff --git a/action-scripts/dev.py b/action-scripts/dev.py new file mode 100644 index 00000000..460be80c --- /dev/null +++ b/action-scripts/dev.py @@ -0,0 +1,7 @@ +def main() -> None: + print("Dev build, adding mcprep_dev.txt...") + with open("mcprep_dev.txt", "w") as f: + f.write("This file is used to enable debugging mode in MCprep, you can ignore this") + +if __name__ == "__main__": + main() diff --git a/action-scripts/translate.py b/action-scripts/translate.py new file mode 100644 index 00000000..097365f8 --- /dev/null +++ b/action-scripts/translate.py @@ -0,0 +1,26 @@ +from typing import Optional +from translate_scripts import build_pot, build_trans_dict, compile_po_to_mo +from bpy_addon_build.api import BabContext, BpyError +import sys +import io + +def pre_build(ctx: BabContext) -> Optional[BpyError]: + if not isinstance(sys.stdout, io.StringIO): + sys.stdout.reconfigure(encoding='utf-8') + return build_pot.pre_build(ctx) + +def main(): + if not isinstance(sys.stdout, io.StringIO): + sys.stdout.reconfigure(encoding='utf-8') + + # Go through each module, and + # run its main function, while checking + # for a return value + modules = [compile_po_to_mo, build_trans_dict] + for mod in modules: + error = mod.main() + if error: + return error + +if __name__ == "__main__": + main() diff --git a/action-scripts/translate_scripts/__init__.py b/action-scripts/translate_scripts/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/action-scripts/translate_scripts/build_pot.py b/action-scripts/translate_scripts/build_pot.py new file mode 100644 index 00000000..3e3b7bf9 --- /dev/null +++ b/action-scripts/translate_scripts/build_pot.py @@ -0,0 +1,136 @@ +import ast +import re +from datetime import datetime, tzinfo, timedelta +import time +from pathlib import Path +from typing import Optional +import polib + +from bpy_addon_build.api import BabContext, BpyError + +class TranslateCallVisitor(ast.NodeVisitor): + def __init__(self): + self.keys = {} + + def visit_Call(self, node: ast.Call): + attr = node.func + if not isinstance(attr, ast.Attribute): + self.generic_visit(node) + return + + # Check if the function we're calling + # is an attribute of env + attr_value = attr.value + if not isinstance(attr_value, ast.Name): + self.generic_visit(node) + return + if attr_value.id != 'env': + self.generic_visit(node) + return + del attr_value # delete as no longer needed + + # Check if we're calling env._ + attr_name = attr.attr + if attr_name != '_': + self.generic_visit(node) + return + del attr_name # delete as no longer needed + + if len(node.args): + # env._ only accepts one argument + msgid = node.args[0] + if not isinstance(msgid, ast.Constant): + self.generic_visit(node) + return + if msgid.value not in self.keys: + self.keys[msgid.value] = [msgid.lineno] + else: + self.keys[msgid.value].append(msgid.lineno) + self.generic_visit(node) + +VERSION_REGEX = r'"version"\s*:\s*(\(.*?\))' +MCPREP_ISSUE_TRACKER = "https://github.com/Moo-Ack-Productions/MCprep/issues" + +# Copied from here: +# https://github.com/shibukawa/sphinx/blob/master/sphinx/builders/gettext.py +timestamp = time.time() +tzdelta = datetime.fromtimestamp(timestamp) - datetime.utcfromtimestamp(timestamp) +class LocalTimeZone(tzinfo): + + def __init__(self, *args, **kw): + super(LocalTimeZone, self).__init__(*args, **kw) + self.tzdelta = tzdelta + + def utcoffset(self, dt): + return self.tzdelta + + def dst(self, dt): + return timedelta(0) + +ltz = LocalTimeZone() + +def pre_build(ctx: BabContext) -> Optional[BpyError]: + print("Building POT...") + path = Path(ctx.current_path) + extracted_strings = {} + + version_str: Optional[str] = None + with open(Path(ctx.current_path, "__init__.py"), 'r') as f: + for line in f: + if re.search(VERSION_REGEX, line): + version_str = line + break + + if not version_str: + return BpyError("Can't extract version from __init__.py!") + + try: + # Step by step breakdown: + # 1. Split the string '"version": (...)' to + # "version" and (...) + # 2. Take the second element, remove extra whitespace, + # and remove the first character and last 2 characters + # (parenthesis and trailing comma) + # 3. Split that final string by the comma + # 4. Do a small generator that strips each element + # of extra whitespace + # 5. Join them up with a '.' in between + processed_version_string = '.'.join(tuple( + v.strip() for v in version_str.strip() + .split(':')[1] + .strip()[1:][:-2] + .split(','))) + except Exception: + return BpyError(f"Couldn't convert {version_str}!") + + for p in path.rglob("*.py"): + with open(p, 'r') as f: + root = ast.parse(f.read()) + visitor = TranslateCallVisitor() + visitor.visit(root) + if len(visitor.keys): + extracted_strings[f"{str(p)}"] = visitor.keys + + po = polib.POFile() + po.metadata = { + "Project-Id-Version": processed_version_string, + "Report-Msgid-Bugs-To": MCPREP_ISSUE_TRACKER, + "POT-Creation-Date": datetime.fromtimestamp(timestamp, ltz).strftime('%Y-%m-%d %H:%M%z'), + "PO-Revision-Date": "YEAR-MO-DA HO:MI+ZONE", + "Last-Translator": "FULL NAME ", + "Language-Team": "LANGUAGE ", + "Language": "", + "MIME-Version": "1.0", + "Content-Type": "text/plain; charset=UTF-8", + "Content-Transfer-Encoding": "8bit", + } + for file, keys in extracted_strings.items(): + for msgid, lineno in keys.items(): + entry = polib.POEntry( + msgid=msgid, + msgstr=u'', + occurrences=[(file, n) for n in lineno] + ) + po.append(entry) + po.save(str(ctx.current_path) + "/MCprep_resources/Languages/mcprep.pot") + return None diff --git a/action-scripts/translate_scripts/build_trans_dict.py b/action-scripts/translate_scripts/build_trans_dict.py new file mode 100644 index 00000000..137d8063 --- /dev/null +++ b/action-scripts/translate_scripts/build_trans_dict.py @@ -0,0 +1,59 @@ +# Copyright (c) 2024 Mahid Sheikh. All Rights Reserved. +# +# This is a build script to automatically +# generate a bpy.apps.translations compatible +# dictionary from PO files. +# +# Apparently Blender's UI Translate addon has +# this feature, but it's so confusing to use that +# it makes more sense for us to create a build script +# that automatically generates a Python file with +# the necessary translation dictionary. + +from pathlib import Path +from typing import Dict, Tuple +import polib +import pprint + +translation_dict: Dict[str, Dict[Tuple[str, str], str]] = {} + +def main() -> None: + print("Building Translations...") + languages = Path("MCprep_resources/Languages") + + if not languages.exists() or not languages.is_dir(): + print("Invalid directory for translations! Exiting...") + + for locale in languages.iterdir(): + if not locale.is_dir(): + continue + file = Path(locale, "LC_MESSAGES", "mcprep.po") + if not file.exists(): + print(file, "does not exist!") + + po = polib.pofile(str(file)) + + locale_dict: Dict[Tuple[str, str], str] = {} + for entry in po: + # Let's keep the file as small + # as possible, and not include + # untranslated parts + if entry.msgstr == '': + continue + locale_dict[("*", entry.msgid)] = entry.msgstr + + # This is to handle cases like English + # where the dictionary is empty + if len(locale_dict): + translation_dict[str(locale.name)] = locale_dict + + with open("translations.py", 'w') as f: + f.write("# This file is generated by BpyBuild.\n") + f.write("# Do not modify this file directly, modify the PO files\n") + f.write("# under MCprep_addon/MCprep_resources/Languages\n") + f.write("MCPREP_TRANSLATIONS = ") + pprint.pprint(translation_dict, f) + + +if __name__ == "__main__": + main() diff --git a/action-scripts/translate_scripts/compile_po_to_mo.py b/action-scripts/translate_scripts/compile_po_to_mo.py new file mode 100644 index 00000000..add296e9 --- /dev/null +++ b/action-scripts/translate_scripts/compile_po_to_mo.py @@ -0,0 +1,27 @@ +# Copyright (c) 2024 Mahid Sheikh. All Rights Reserved. +# +# This compiles all PO files in MCprep_resources/Languages +# to MO files that are used as a fallback for Python gettext. + +from pathlib import Path +import polib + +def main() -> None: + print("Building MO files...") + languages = Path("MCprep_resources/Languages") + + if not languages.exists() or not languages.is_dir(): + print("Invalid directory for translations! Exiting...") + + for locale in languages.iterdir(): + if not locale.is_dir(): + continue + file = Path(locale, "LC_MESSAGES", "mcprep.po") + if not file.exists(): + print(file, "does not exist!") + + po = polib.pofile(str(file)) + po.save_as_mofile(str(file.parent.joinpath("mcprep.mo"))) + +if __name__ == "__main__": + main() diff --git a/bpy-build.yaml b/bpy-build.yaml index 8ef9a0e1..b5b0ccb3 100644 --- a/bpy-build.yaml +++ b/bpy-build.yaml @@ -2,22 +2,25 @@ addon_folder: MCprep_addon build_name: MCprep_addon install_versions: - - '4.0' - - '3.6' - - '3.5' - - '3.4' - - '3.3' - - '3.2' - - '3.1' - - '3.0' - - '2.93' - - '2.90' - - '2.80' + - 4.2 + - 4.1 + - 4.0 + - 3.6 + - 3.5 + - 3.4 + - 3.3 + - 3.2 + - 3.1 + - 3.0 + - 2.93 + - 2.9 + - 2.83 + - 2.8 -# Run using a command like: bpy-addon-build --during-build dev -during_build: - default: - - [] # Re-enable later: [flake8 --extend-ignore W191 .] - dev: - - create_file("mcprep_dev.txt") - +build_actions: + default: + script: "action-scripts/default.py" + dev: + script: "action-scripts/dev.py" + translate: + script: "action-scripts/translate.py" diff --git a/docs/i18n/developer_notes.md b/docs/i18n/developer_notes.md new file mode 100644 index 00000000..4642f66b --- /dev/null +++ b/docs/i18n/developer_notes.md @@ -0,0 +1,23 @@ +# Notes for Developers +- All user facing strings in MCprep that can be translated should be translated. This can be done with `env._`: +```py +# DON'T: This string is user +# facing and thus should be +# translatable +layout.label(text="Hello World!") + +# DO: This makes the string +# translatable and also allows +# us to update mcprep.pot +layout.label(text=env._("Hello World!")) +``` + +# Notes for Maintainers +- Update the POT file before merging a PR. On Linux, the POT file can be updated with the following: +```sh +find ./MCprep_addon -iname "*.py" | xargs xgettext --keyword="env._" --from-code utf-8 -o MCprep_addon/MCprep_resources/Languages/mcprep.pot +``` + +- BpyBuild will automatically generate `translate.py` and all `mcprep.mo` files at build time, provided the `translate` option is passed at build time. + - Production builds: `bab -b translate` + - Dev builds: `bab -b dev translate` diff --git a/docs/i18n/translating.md b/docs/i18n/translating.md new file mode 100644 index 00000000..35f55fca --- /dev/null +++ b/docs/i18n/translating.md @@ -0,0 +1,70 @@ +# Translation Guidelines +To make sure translations are up to par with the standards we hold on the MCprep repo, all translators must follow these guidelines. +1. No AI for translations, such as Google Translate, ChatGPT, etc. AI translations are unreliable, differ massively in terms of quality between different languages, and don't take context properly into account. +2. All translations must be confirmed by a regular user who knows the language in question. This is so we can verify that the translation is up to par. + +In addition, the following should be kept in mind: +- Not everything needs to be translated at once. If you're unsure of how to translate something, skip it. +- Translations should be close to the original strings in length. However, if this is not possible, then bring it up in a GitHub Issue. + +# Understanding MCprep's Language format +MCprep uses PO files to store translations for files, which are compiled to MO files and stored in `MCprep_resources/Languages`. The layout of the `Languages`folder is the following: +``` +Languages/ +├── mcprep.pot +├── en_US/ +│ └── LC_MESSAGES/ +│ ├── mcprep.po +│ └── mcprep.mo +└── ... +``` + +Each language is stored in `i18n_code/LC_MESSAGES` (where `i18n_code` is the language code). All PO files are made from copying `mcprep.pot`, but `mcprep.pot` **should never be edited**. `mcprep.pot` is the template that all translations use. + +# Making a Translation +This guide assumes you know the basics of `git` and forking in general. +1. Clone the MCprep repository +2. Create a folder with the language code of what you're translating for as the name, and in that folder, create a new folder called `LC_MESSAGES`. Using American English (`en_US`) as an example, it should look like this: +``` +en_US/ +└── LC_MESSAGES/ +``` + +3. Next, copy `mcprep.pot` to `LC_MESSAGES` as `mcprep.po`. `mcprep.pot` itself **should never be modified**, only copied. +4. Start editing `mcprep.po`. If you're using a text editor, you'll see a bunch of strings like these: +```po +#: MCprep_addon/mcprep_ui.py:98 +msgid "Restart blender" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:100 +msgid "to complete update" +msgstr "" + +#: MCprep_addon/mcprep_ui.py:121 +msgid "Load mobs" +msgstr "" +``` + +Lines that begin with `msgid` are the original strings, those remain untouched. Lines that begin with `msgstr` contain the translations. + +In general, we recommend using a PO editor such as [PoEdit](https://poedit.net). These editors make it easier to translate. + +5. Compile the PO file to an MO file. There's several ways to do this: + - **Recommended**: Build MCprep using BpyBuild. Instructions can be found on [CONTRIBUTING.md](https://github.com/Moo-Ack-Productions/MCprep/blob/dev/CONTRIBUTING.md) + - Websites such as `https://po2mo.net` + - Command line tools such as `msgfmt` + - PO editors like PoEdit + +Save the MO file as `mcprep.mo` in `LC_MESSAGES`, and your file structure should now look like this (using `en_US` as an example): +``` +en_US/ +└── LC_MESSAGES/ + ├── mcprep.po + └── mcprep.mo +``` + +This conversion needs to be done each time the PO file is modified. + +6. Build MCprep (see [CONTRIBUTING.md](https://github.com/Moo-Ack-Productions/MCprep/blob/dev/CONTRIBUTING.md) for steps on building MCprep) and test out the translation. *Note: MCprep follows the language set in Blender. If the language you're translating to is not supported in Blender, open a GitHub Issue* +7. Push the translations to your local fork of MCprep, open a PR, etc. Check [CONTRIBUTING.md](https://github.com/Moo-Ack-Productions/MCprep/blob/dev/CONTRIBUTING.md) for more details. diff --git a/poetry.lock b/poetry.lock index c2fc808a..4f108b72 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,31 +1,81 @@ -# This file is automatically @generated by Poetry 1.5.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand. + +[[package]] +name = "attrs" +version = "23.2.0" +description = "Classes Without Boilerplate" +optional = false +python-versions = ">=3.7" +files = [ + {file = "attrs-23.2.0-py3-none-any.whl", hash = "sha256:99b87a485a5820b23b879f04c2305b44b951b502fd64be915879d77a7e8fc6f1"}, + {file = "attrs-23.2.0.tar.gz", hash = "sha256:935dc3b529c262f6cf76e50877d35a4bd3c1de194fd41f47a2b7ae8f19971f30"}, +] + +[package.extras] +cov = ["attrs[tests]", "coverage[toml] (>=5.3)"] +dev = ["attrs[tests]", "pre-commit"] +docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier", "zope-interface"] +tests = ["attrs[tests-no-zope]", "zope-interface"] +tests-mypy = ["mypy (>=1.6)", "pytest-mypy-plugins"] +tests-no-zope = ["attrs[tests-mypy]", "cloudpickle", "hypothesis", "pympler", "pytest (>=4.3.0)", "pytest-xdist[psutil]"] [[package]] name = "bpy-addon-build" -version = "0.2.1" +version = "0.4.0" description = "A build system to make building and testing Blender addons 10 times easier" optional = false -python-versions = ">=3.7,<4.0" +python-versions = ">=3.8" files = [ - {file = "bpy_addon_build-0.2.1-py3-none-any.whl", hash = "sha256:443a0df1e940070a956278040ab04774bcbf42090aba8a05240d7b51928e443f"}, - {file = "bpy_addon_build-0.2.1.tar.gz", hash = "sha256:175e2afad291d1def1612b5e55ce70fe686f9a108f9f78f5d823e4c3cd6745ae"}, + {file = "bpy_addon_build-0.4.0-py3-none-any.whl", hash = "sha256:8de89cbdec1f763df83be7306d2e2bfe659087c4122bdbba9f4e095571c572b1"}, + {file = "bpy_addon_build-0.4.0.tar.gz", hash = "sha256:8ac7d4d82ef759d1b461e0063e2af40215ff410fca421655f8cdc4b914ecfd77"}, ] [package.dependencies] -docopt = ">=0.6.2,<0.7.0" -pyyaml = ">=6.0,<7.0" -rich = ">=13.3.5,<14.0.0" +attrs = ">=23.1.0,<24.0.0" +cattrs = ">=23.2.3,<24.0.0" +pyyaml = ">=6.0.1,<7.0.0" +rich = ">=13.7.0,<14.0.0" +typeguard = ">=4.1.5,<5.0.0" [[package]] -name = "docopt" -version = "0.6.2" -description = "Pythonic argument parser, that will make you smile" +name = "cattrs" +version = "23.2.3" +description = "Composable complex class support for attrs and dataclasses." optional = false -python-versions = "*" +python-versions = ">=3.8" +files = [ + {file = "cattrs-23.2.3-py3-none-any.whl", hash = "sha256:0341994d94971052e9ee70662542699a3162ea1e0c62f7ce1b4a57f563685108"}, + {file = "cattrs-23.2.3.tar.gz", hash = "sha256:a934090d95abaa9e911dac357e3a8699e0b4b14f8529bcc7d2b1ad9d51672b9f"}, +] + +[package.dependencies] +attrs = ">=23.1.0" +exceptiongroup = {version = ">=1.1.1", markers = "python_version < \"3.11\""} +typing-extensions = {version = ">=4.1.0,<4.6.3 || >4.6.3", markers = "python_version < \"3.11\""} + +[package.extras] +bson = ["pymongo (>=4.4.0)"] +cbor2 = ["cbor2 (>=5.4.6)"] +msgpack = ["msgpack (>=1.0.5)"] +orjson = ["orjson (>=3.9.2)"] +pyyaml = ["pyyaml (>=6.0)"] +tomlkit = ["tomlkit (>=0.11.8)"] +ujson = ["ujson (>=5.7.0)"] + +[[package]] +name = "exceptiongroup" +version = "1.2.1" +description = "Backport of PEP 654 (exception groups)" +optional = false +python-versions = ">=3.7" files = [ - {file = "docopt-0.6.2.tar.gz", hash = "sha256:49b3a825280bd66b3aa83585ef59c4a8c82f2c8a522dbe754a8bc8d08c85c491"}, + {file = "exceptiongroup-1.2.1-py3-none-any.whl", hash = "sha256:5258b9ed329c5bbdd31a309f53cbfb0b155341807f6ff7606a1e801a891b29ad"}, + {file = "exceptiongroup-1.2.1.tar.gz", hash = "sha256:a4785e48b045528f5bfe627b6ad554ff32def154f42372786903b7abcfe1aa16"}, ] +[package.extras] +test = ["pytest (>=6)"] + [[package]] name = "fake-bpy-module-2-80" version = "20230117" @@ -49,44 +99,42 @@ files = [ ] [package.dependencies] -importlib-metadata = {version = ">=1.1.0,<4.3", markers = "python_version < \"3.8\""} mccabe = ">=0.7.0,<0.8.0" pycodestyle = ">=2.9.0,<2.10.0" pyflakes = ">=2.5.0,<2.6.0" [[package]] name = "importlib-metadata" -version = "4.2.0" +version = "7.1.0" description = "Read metadata from Python packages" optional = false -python-versions = ">=3.6" +python-versions = ">=3.8" files = [ - {file = "importlib_metadata-4.2.0-py3-none-any.whl", hash = "sha256:057e92c15bc8d9e8109738a48db0ccb31b4d9d5cfbee5a8670879a30be66304b"}, - {file = "importlib_metadata-4.2.0.tar.gz", hash = "sha256:b7e52a1f8dec14a75ea73e0891f3060099ca1d8e6a462a4dff11c3e119ea1b31"}, + {file = "importlib_metadata-7.1.0-py3-none-any.whl", hash = "sha256:30962b96c0c223483ed6cc7280e7f0199feb01a0e40cfae4d4450fc6fab1f570"}, + {file = "importlib_metadata-7.1.0.tar.gz", hash = "sha256:b78938b926ee8d5f020fc4772d487045805a55ddbad2ecf21c6d60938dc7fcd2"}, ] [package.dependencies] -typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""} zipp = ">=0.5" [package.extras] -docs = ["jaraco.packaging (>=8.2)", "rst.linker (>=1.9)", "sphinx"] -testing = ["flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pep517", "pyfakefs", "pytest (>=4.6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.0.1)", "pytest-flake8", "pytest-mypy"] +docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +perf = ["ipython"] +testing = ["flufl.flake8", "importlib-resources (>=1.3)", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-perf (>=0.9.2)", "pytest-ruff (>=0.2.1)"] [[package]] name = "markdown-it-py" -version = "2.2.0" +version = "3.0.0" description = "Python port of markdown-it. Markdown parsing, done right!" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "markdown-it-py-2.2.0.tar.gz", hash = "sha256:7c9a5e412688bc771c67432cbfebcdd686c93ce6484913dccf06cb5a0bea35a1"}, - {file = "markdown_it_py-2.2.0-py3-none-any.whl", hash = "sha256:5a35f8d1870171d9acc47b99612dc146129b631baf04970128b568f190d0cc30"}, + {file = "markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb"}, + {file = "markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1"}, ] [package.dependencies] mdurl = ">=0.1,<1.0" -typing_extensions = {version = ">=3.7.4", markers = "python_version < \"3.8\""} [package.extras] benchmarking = ["psutil", "pytest", "pytest-benchmark"] @@ -95,7 +143,7 @@ compare = ["commonmark (>=0.9,<1.0)", "markdown (>=3.4,<4.0)", "mistletoe (>=1.0 linkify = ["linkify-it-py (>=1,<3)"] plugins = ["mdit-py-plugins"] profiling = ["gprof2dot"] -rtd = ["attrs", "myst-parser", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinx_book_theme"] +rtd = ["jupyter_sphinx", "mdit-py-plugins", "myst-parser", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinx_book_theme"] testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] [[package]] @@ -120,6 +168,17 @@ files = [ {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, ] +[[package]] +name = "polib" +version = "1.2.0" +description = "A library to manipulate gettext files (po and mo files)." +optional = false +python-versions = "*" +files = [ + {file = "polib-1.2.0-py2.py3-none-any.whl", hash = "sha256:1c77ee1b81feb31df9bca258cbc58db1bbb32d10214b173882452c73af06d62d"}, + {file = "polib-1.2.0.tar.gz", hash = "sha256:f3ef94aefed6e183e342a8a269ae1fc4742ba193186ad76f175938621dbfc26b"}, +] + [[package]] name = "pycodestyle" version = "2.9.1" @@ -144,17 +203,18 @@ files = [ [[package]] name = "pygments" -version = "2.16.1" +version = "2.17.2" description = "Pygments is a syntax highlighting package written in Python." optional = false python-versions = ">=3.7" files = [ - {file = "Pygments-2.16.1-py3-none-any.whl", hash = "sha256:13fc09fa63bc8d8671a6d247e1eb303c4b343eaee81d861f3404db2935653692"}, - {file = "Pygments-2.16.1.tar.gz", hash = "sha256:1daff0494820c69bc8941e407aa20f577374ee88364ee10a98fdbe0aece96e29"}, + {file = "pygments-2.17.2-py3-none-any.whl", hash = "sha256:b27c2826c47d0f3219f29554824c30c5e8945175d888647acd804ddd04af846c"}, + {file = "pygments-2.17.2.tar.gz", hash = "sha256:da46cec9fd2de5be3a8a784f434e4c4ab670b4ff54d605c4c2717e9d49c4c367"}, ] [package.extras] plugins = ["importlib-metadata"] +windows-terminal = ["colorama (>=0.4.6)"] [[package]] name = "pyyaml" @@ -207,13 +267,13 @@ files = [ [[package]] name = "rich" -version = "13.5.2" +version = "13.7.1" description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" optional = false python-versions = ">=3.7.0" files = [ - {file = "rich-13.5.2-py3-none-any.whl", hash = "sha256:146a90b3b6b47cac4a73c12866a499e9817426423f57c5a66949c086191a8808"}, - {file = "rich-13.5.2.tar.gz", hash = "sha256:fb9d6c0a0f643c99eed3875b5377a184132ba9be4d61516a55273d3554d75a39"}, + {file = "rich-13.7.1-py3-none-any.whl", hash = "sha256:4edbae314f59eb482f54e9e30bf00d33350aaa94f4bfcd4e9e3110e64d0d7222"}, + {file = "rich-13.7.1.tar.gz", hash = "sha256:9be308cb1fe2f1f57d67ce99e95af38a1e2bc71ad9813b0e247cf7ffbcc3a432"}, ] [package.dependencies] @@ -224,33 +284,52 @@ typing-extensions = {version = ">=4.0.0,<5.0", markers = "python_version < \"3.9 [package.extras] jupyter = ["ipywidgets (>=7.5.1,<9)"] +[[package]] +name = "typeguard" +version = "4.2.1" +description = "Run-time type checker for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "typeguard-4.2.1-py3-none-any.whl", hash = "sha256:7da3bd46e61f03e0852f8d251dcbdc2a336aa495d7daff01e092b55327796eb8"}, + {file = "typeguard-4.2.1.tar.gz", hash = "sha256:c556a1b95948230510070ca53fa0341fb0964611bd05d598d87fb52115d65fee"}, +] + +[package.dependencies] +importlib-metadata = {version = ">=3.6", markers = "python_version < \"3.10\""} +typing-extensions = {version = ">=4.10.0", markers = "python_version < \"3.13\""} + +[package.extras] +doc = ["Sphinx (>=7)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)"] +test = ["coverage[toml] (>=7)", "mypy (>=1.2.0)", "pytest (>=7)"] + [[package]] name = "typing-extensions" -version = "4.7.1" -description = "Backported and Experimental Type Hints for Python 3.7+" +version = "4.11.0" +description = "Backported and Experimental Type Hints for Python 3.8+" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "typing_extensions-4.7.1-py3-none-any.whl", hash = "sha256:440d5dd3af93b060174bf433bccd69b0babc3b15b1a8dca43789fd7f61514b36"}, - {file = "typing_extensions-4.7.1.tar.gz", hash = "sha256:b75ddc264f0ba5615db7ba217daeb99701ad295353c45f9e95963337ceeeffb2"}, + {file = "typing_extensions-4.11.0-py3-none-any.whl", hash = "sha256:c1f94d72897edaf4ce775bb7558d5b79d8126906a14ea5ed1635921406c0387a"}, + {file = "typing_extensions-4.11.0.tar.gz", hash = "sha256:83f085bd5ca59c80295fc2a82ab5dac679cbe02b9f33f7d83af68e241bea51b0"}, ] [[package]] name = "zipp" -version = "3.15.0" +version = "3.18.1" description = "Backport of pathlib-compatible object wrapper for zip files" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "zipp-3.15.0-py3-none-any.whl", hash = "sha256:48904fc76a60e542af151aded95726c1a5c34ed43ab4134b597665c86d7ad556"}, - {file = "zipp-3.15.0.tar.gz", hash = "sha256:112929ad649da941c23de50f356a2b5570c954b65150642bccdd66bf194d224b"}, + {file = "zipp-3.18.1-py3-none-any.whl", hash = "sha256:206f5a15f2af3dbaee80769fb7dc6f249695e940acca08dfb2a4769fe61e538b"}, + {file = "zipp-3.18.1.tar.gz", hash = "sha256:2884ed22e7d8961de1c9a05142eb69a247f120291bc0206a00a7642f09b5b715"}, ] [package.extras] -docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] -testing = ["big-O", "flake8 (<5)", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"] +docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-ignore-flaky", "pytest-mypy", "pytest-ruff (>=0.2.1)"] [metadata] lock-version = "2.0" -python-versions = "^3.7" -content-hash = "47abfe0a2df11a2ed20dc95d08450e596a5c0691f20601813536697f4b153d3c" +python-versions = "^3.8" +content-hash = "d725fe40cac4ba2a28c60418d4f17201aa68417a734a717e674747eaf9202124" diff --git a/push_latest.sh b/push_latest.sh index e83e5f02..885618e4 100755 --- a/push_latest.sh +++ b/push_latest.sh @@ -46,10 +46,21 @@ git checkout test_files/test_data/mineways_test_separated_1_15_2.mtl python mcprep_data_refresh.py -auto -if [[ `git status --porcelain` ]]; then - echo "There are uncommited changes, ending" - # exit # TODO: Enforce in future directly. -fi +# ----------------------------------------------------------------------------- +# Build releasE with translation updates +# ----------------------------------------------------------------------------- + +echo "Force remove trcaker files, in case they are left over" +rm MCprep_addon/mcprep_addon_tracker.json +rm mcprep_addon_trackerid.json + +echo "Building prod addon..." +bpy-addon-build -b translate # No --during-build dev to make it prod. +ls build/MCprep_addon.zip + +# ----------------------------------------------------------------------------- +# Cross check no local changes, such as updated translations +# ----------------------------------------------------------------------------- ANY_DIFF=$(git diff MCprep_addon/MCprep_resources/mcprep_data_update.json) if [ -z "$ANY_DIFF" ] @@ -61,19 +72,15 @@ else exit fi +if [[ `git status --porcelain` ]]; then + echo "There are uncommited changes/untracked local files" + # exit # TODO: Enforce in future directly. +fi # ----------------------------------------------------------------------------- -# Validate and build the release. +# Commence release draft # ----------------------------------------------------------------------------- -echo "Force remove trcaker files, in case they are left over" -rm MCprep_addon/mcprep_addon_tracker.json -rm mcprep_addon_trackerid.json - -echo "Building prod addon..." -bpy-addon-build # No --during-build dev to make it prod. -ls build/MCprep_addon.zip - echo "" echo "Last 5 live tags online:" git tag -l | tail -5 @@ -83,6 +90,12 @@ echo "" BASE_VER=$(grep "\"version\":" MCprep_addon/__init__.py | awk -F"[()]" '{print $2}' | tr ',' '.' | tr -d ' ') NEW_TAG="${BASE_VER}" +ALL_TAGS=$(git tag -l) +if [[ $ALL_TAGS == *$NEW_TAG* ]]; then + echo "Version $NEW_TAG already exists, need to update __init__.py" + exit 1 +fi + echo -e "Current __init__ version: ${GREEN}${NEW_TAG}${NC}" read -p "Continue (Y/N): " confirm && [[ $confirm == [yY] || $confirm == [yY][eE][sS] ]] || exit 1 diff --git a/pyproject.toml b/pyproject.toml index 98ba6724..b3716c16 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,10 +7,11 @@ license = "GPL-3.0-only" readme = "README.md" [tool.poetry.dependencies] -python = "^3.7" +python = "^3.8" fake-bpy-module-2-80 = "^20230117" flake8 = "^5.0.4" -bpy-addon-build = ">=0.1.8,<0.3.0" +bpy-addon-build = ">=0.4.0" +polib = "^1.2.0" [build-system] requires = ["poetry-core"] diff --git a/rc-files/HOW_TO_BUILD.md b/rc-files/HOW_TO_BUILD.md new file mode 100644 index 00000000..5e8b2dcb --- /dev/null +++ b/rc-files/HOW_TO_BUILD.md @@ -0,0 +1,8 @@ +Step 1. Run `download_resources.sh` to add the necesary asset files + +Step 2. Apply all patches in the `patches` folder. Here's an example for the `rc-bl_info` patch +```sh +git apply rc-files/patches/rc-bl_info.patch +``` + +Step 3. Use Bpy-Build to build MCprep like normal diff --git a/rc-files/download_resources.sh b/rc-files/download_resources.sh new file mode 100644 index 00000000..385d1c3d --- /dev/null +++ b/rc-files/download_resources.sh @@ -0,0 +1,14 @@ +# Get the latest stable release and unzip +# +# TODO: Automatically get the latest tag and download link. Perhaps +# through the GitHub API? +wget "https://github.com/Moo-Ack-Productions/MCprep/releases/download/3.5.3/MCprep_addon_3.5.3.zip" +mkdir MCprep_stable_release +unzip ./MCprep_addon_*.zip -d MCprep_stable_release + +# Remove the files in resources +rm -rf ./MCprep_addon/MCprep_resources/** +# Move the special files +mv -f ./MCprep_stable_release/MCprep_addon/MCprep_resources/** ./MCprep_addon/MCprep_resources +# Remove the stable release +rm -rf ./MCprep_stable_release MCprep_addon_*.zip diff --git a/rc-files/patches/rc-bl_info.patch b/rc-files/patches/rc-bl_info.patch new file mode 100644 index 00000000..7ff9c318 --- /dev/null +++ b/rc-files/patches/rc-bl_info.patch @@ -0,0 +1,21 @@ +diff --git a/MCprep_addon/__init__.py b/MCprep_addon/__init__.py +index e727f3f..39c9fa7 100755 +--- a/MCprep_addon/__init__.py ++++ b/MCprep_addon/__init__.py +@@ -39,13 +39,13 @@ Disclaimer: This is not an official Google product + # error = 51 + + bl_info = { +- "name": "MCprep", ++ "name": "MCprep 3.6 RC-3", + "category": "Object", +- "version": (3, 5, 3), ++ "version": (3, 5, 3, 3), + "blender": (2, 80, 0), + "location": "3D window toolshelf > MCprep tab", + "description": "Minecraft workflow addon for rendering and animation", +- "warning": "", ++ "warning": "Release Candidate, expect bugs!", + "wiki_url": "https://TheDuckCow.com/MCprep", + "author": "Patrick W. Crawford ", + "tracker_url": "https://github.com/TheDuckCow/MCprep/issues" diff --git a/rc-files/patches/rc-tracker-id.patch b/rc-files/patches/rc-tracker-id.patch new file mode 100644 index 00000000..83f09984 --- /dev/null +++ b/rc-files/patches/rc-tracker-id.patch @@ -0,0 +1,13 @@ +diff --git a/MCprep_addon/tracking.py b/MCprep_addon/tracking.py +index d3020d4..120185d 100644 +--- a/MCprep_addon/tracking.py ++++ b/MCprep_addon/tracking.py +@@ -54,7 +54,7 @@ except Exception as err: + # global vars + # ----------------------------------------------------------------------------- + +-IDNAME = "mcprep" ++IDNAME = "mcprep_rc" + + # max data/string lengths to match server-side validation, + # if exceeded, request will return denied (no data written) diff --git a/requirements.txt b/requirements.txt index 06be29e2..e92789b7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ -bpy-addon-build>=0.1.8,<0.3.0 +bpy-addon-build>=0.4.0 fake-bpy-module-2.80==20230117 flake8==5.0.4 +polib==1.2.0 diff --git a/run_tests.py b/run_tests.py index f9356bbb..2ca36091 100644 --- a/run_tests.py +++ b/run_tests.py @@ -41,7 +41,7 @@ import time -COMPILE_CMD = ["bpy-addon-build", "--during-build", "dev"] +COMPILE_CMD = ["bab", "-b", "dev"] DATA_CMD = ["python", "mcprep_data_refresh.py", "-auto"] # TODO, include in build DCC_EXES = "blender_execs.txt" TEST_RUNNER = os.path.join("test_files", "test_runner.py") @@ -70,8 +70,7 @@ def main(): # Compile the addon res = subprocess.check_output(COMPILE_CMD) - print(res.decode("utf-8")) - + print("Compile output:", res.decode("utf-8")) reset_test_file() # Loop over all binaries and run tests. diff --git a/test_files/materials_test.py b/test_files/materials_test.py index de154cea..815fddad 100644 --- a/test_files/materials_test.py +++ b/test_files/materials_test.py @@ -98,6 +98,17 @@ def _set_test_mcprep_texturepack_path(self, reset: bool = False): raise Exception("Failed to set test texturepack path") bpy.context.scene.mcprep_texturepack_path = path + def _count_missing_files(self) -> int: + missing = 0 + for img in bpy.data.images: + if not os.path.isfile(img.filepath): + missing += 1 + elif img.channels == 0: + # If a file is valid but still not loaded by blender, the num + # of chanels will appear to be zero + missing += 1 + return missing + def test_prep_materials_no_selection(self): """Ensures prepping with no selection fails.""" with self.assertRaises(RuntimeError) as rte: @@ -695,17 +706,17 @@ def cleanup(): # the test cases; input is diffuse, output is the whole dict cases = [ { - "diffuse": os.path.join(tmp_dir, "oak_log_top.png"), - "specular": os.path.join(tmp_dir, "oak_log_top-s.png"), - "normal": os.path.join(tmp_dir, "oak_log_top_n.png"), + "diffuse": Path(tmp_dir) / "oak_log_top.png", + "specular": Path(tmp_dir) / "oak_log_top-s.png", + "normal": Path(tmp_dir) / "oak_log_top_n.png", }, { - "diffuse": os.path.join(tmp_dir, "oak_log.jpg"), - "specular": os.path.join(tmp_dir, "oak_log_s.jpg"), - "normal": os.path.join(tmp_dir, "oak_log_n.jpeg"), - "displace": os.path.join(tmp_dir, "oak_log_disp.jpeg"), + "diffuse": Path(tmp_dir) / "oak_log.jpg", + "specular": Path(tmp_dir) / "oak_log_s.jpg", + "normal": Path(tmp_dir) / "oak_log_n.jpeg", + "displace": Path(tmp_dir) / "oak_log_disp.jpeg", }, { - "diffuse": os.path.join(tmp_dir, "stonecutter_saw.tiff"), - "normal": os.path.join(tmp_dir, "stonecutter_saw n.tiff"), + "diffuse": Path(tmp_dir) / "stonecutter_saw.tiff", + "normal": Path(tmp_dir) / "stonecutter_saw n.tiff", } ] @@ -783,6 +794,31 @@ def test_replace_missing_images_fixed(self): os.remove(tmp_image) self.fail("New path file does not exist") + def test_replace_missing_textures_integration(self): + """A more exhaustive test to ensure find missing works in jmc2obj exports.""" + + testdir = os.path.dirname(__file__) + demo_world = os.path.join(testdir, "test_data", "jmc2obj_test_1_21.obj") + + # Do built-in world import (as opposed to mcprep.import_world_split) + # to keep it simple here + if util.min_bv((3, 5)): + res = bpy.ops.wm.obj_import( + filepath=demo_world, use_split_groups=True) + else: + res = bpy.ops.import_scene.obj( + filepath=demo_world, use_split_groups=True) + self.assertEqual(res, {"FINISHED"}) + + pre_missing_count = self._count_missing_files() + bpy.ops.mcprep.replace_missing_textures(animateTextures=False) + post_missing_count = self._count_missing_files() + + self.assertGreater( + pre_missing_count, 0, "Ensure some initial missing imgs") + self.assertGreater( + pre_missing_count, post_missing_count, "Ensure some imgs replaced") + def test_replace_missing_images_moved_blend(self): """Scenario where we save, close, then move the blend file.""" tmp_dir = tempfile.gettempdir() @@ -886,7 +922,7 @@ def test_swap_texture_pack(self): obj.active_material = new_mat self.assertIsNotNone(obj.active_material, "Material should be applied") - # Ensure if no texture pack selected, it fails. + # Ensure if no exporter type selected, it fails. addon_prefs = util.get_user_preferences(bpy.context) addon_prefs.MCprep_exporter_type = "(choose)" with self.assertRaises(RuntimeError): diff --git a/test_files/test_data/jmc2obj_test_1_21.mtl b/test_files/test_data/jmc2obj_test_1_21.mtl new file mode 100644 index 00000000..8c6ebc7d --- /dev/null +++ b/test_files/test_data/jmc2obj_test_1_21.mtl @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7155824abda1552da03233e2320dc89b3f1776a62470ab2a515f489269f9ff53 +size 82370 diff --git a/test_files/test_data/jmc2obj_test_1_21.obj b/test_files/test_data/jmc2obj_test_1_21.obj new file mode 100644 index 00000000..d7961ecf --- /dev/null +++ b/test_files/test_data/jmc2obj_test_1_21.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5032b39a22db60ecd3346a40c93b2b1f5871566e49496f04cdd846a934adc681 +size 409323 diff --git a/test_files/test_data/mineways_test_combined_1_21.mtl b/test_files/test_data/mineways_test_combined_1_21.mtl new file mode 100644 index 00000000..c81ef376 --- /dev/null +++ b/test_files/test_data/mineways_test_combined_1_21.mtl @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:06ef918eb7f9a84e3f33e2fe5970d58ea19e3376462161e62c0c1a05f6c1ab42 +size 133438 diff --git a/test_files/test_data/mineways_test_combined_1_21.obj b/test_files/test_data/mineways_test_combined_1_21.obj new file mode 100644 index 00000000..33504499 --- /dev/null +++ b/test_files/test_data/mineways_test_combined_1_21.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c3f0b89937501e34913009f4e9c656674cb33fadded977589253f813b1c910d8 +size 583757 diff --git a/test_files/test_data/mineways_test_separated_1_21.mtl b/test_files/test_data/mineways_test_separated_1_21.mtl new file mode 100644 index 00000000..44029d93 --- /dev/null +++ b/test_files/test_data/mineways_test_separated_1_21.mtl @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bbe8a7093f0c7799b7919509316eb18146ad423c252852fcb0687dbfe56c08ac +size 147361 diff --git a/test_files/test_data/mineways_test_separated_1_21.obj b/test_files/test_data/mineways_test_separated_1_21.obj new file mode 100644 index 00000000..4c2e3b1e --- /dev/null +++ b/test_files/test_data/mineways_test_separated_1_21.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e0f4b8e1742ef87a28b105330990628c6a6bc30d095a0cb87dcf0d3bdc90a250 +size 445809 diff --git a/test_files/world_saves/Test MCprep 1.14.4.zip b/test_files/world_saves/Test MCprep 1.14.4.zip new file mode 100644 index 00000000..4ab192fa --- /dev/null +++ b/test_files/world_saves/Test MCprep 1.14.4.zip @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c7aaa259299eefd6f97640f842a40a22c7655cb2207b49cae16686fcb15f90a6 +size 933854 diff --git a/test_files/world_saves/Test MCprep 1.15.2.zip b/test_files/world_saves/Test MCprep 1.15.2.zip new file mode 100644 index 00000000..6ef09189 --- /dev/null +++ b/test_files/world_saves/Test MCprep 1.15.2.zip @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:09a96a83293e40d581d6b3fb28496b5f3f1d59487e7fdfe0128b285355bec1cb +size 1057610 diff --git a/test_files/world_saves/Test MCprep 1.21.zip b/test_files/world_saves/Test MCprep 1.21.zip new file mode 100644 index 00000000..44c71bd8 --- /dev/null +++ b/test_files/world_saves/Test MCprep 1.21.zip @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:117c5856616b825e2f5b6c34d4f6cf79efc1014cc2e2e4395ca243f3132449f0 +size 2647829 diff --git a/test_files/world_tools_test.py b/test_files/world_tools_test.py index ef71b316..10145af1 100644 --- a/test_files/world_tools_test.py +++ b/test_files/world_tools_test.py @@ -85,14 +85,16 @@ def _import_materials_util(self, mapping_set): # can't import conf separately. mcprep_data = util.env.json_data["blocks"][mapping_set] + generalized = [get_mc_canonical_name(mat.name)[0] for mat in bpy.data.materials] + # first detect alignment to the raw underlining mappings, nothing to # do with canonical yet mapped = [ - mat.name for mat in bpy.data.materials - if mat.name in mcprep_data] # ok! + name for name in generalized + if name in mcprep_data] # ok! unmapped = [ - mat.name for mat in bpy.data.materials - if mat.name not in mcprep_data] # not ok + name for name in generalized + if name not in mcprep_data] # not ok fullset = mapped + unmapped # ie all materials unleveraged = [ mat for mat in mcprep_data @@ -132,8 +134,9 @@ def _import_materials_util(self, mapping_set): if mats_not_canon and mapping_set != "block_mapping_mineways": # print("Non-canon material names found: ({})".format(len(mats_not_canon))) # print(mats_not_canon) - if len(mats_not_canon) > 30: # arbitrary threshold - self.fail("Too many materials found without canonical name") + if len(mats_not_canon) > 40: # arbitrary threshold + self.fail(("Too many materials found without canonical name: " + f"{len(mats_not_canon)}")) # affirm the correct mappings mats_no_packimage = [ @@ -161,9 +164,14 @@ def _import_materials_util(self, mapping_set): def test_enable_obj_importer(self): """Ensure module name is correct, since error won't be reported.""" - bpy.ops.preferences.addon_enable(module="io_scene_obj") - - def test_world_import_jmc_full(self): + if bpy.app.version < (4, 0): + res = bpy.ops.preferences.addon_enable(module="io_scene_obj") + self.assertEqual(res, {'FINISHED'}) + else: + in_import_scn = "obj_import" in dir(bpy.ops.wm) + self.assertTrue(in_import_scn, "obj_import operator not found") + + def test_world_import_legacy_jmc_full(self): test_subpath = os.path.join( "test_data", "jmc2obj_test_1_15_2.obj") self._import_world_with_settings(file=test_subpath) @@ -184,7 +192,29 @@ def test_world_import_jmc_full(self): with self.subTest("test_mappings"): self._import_materials_util("block_mapping_jmc") - def test_world_import_mineways_separated(self): + def test_world_import_cmcobj_jmc_full(self): + test_subpath = os.path.join( + "test_data", "jmc2obj_test_1_21.obj") + self._import_world_with_settings(file=test_subpath) + # TODO: Check that affirms it picks up the mcobj format. + self.assertEqual(self.addon_prefs.MCprep_exporter_type, "jmc2obj") + + # UV tool test. Would be in its own test, but then we would be doing + # multiple unnecessary imports of the same world. So make it a subtest. + with self.subTest("test_uv_transform_no_alert_jmc2obj"): + invalid, invalid_objs = detect_invalid_uvs_from_objs( + bpy.context.selected_objects) + prt = ",".join([obj.name.split("_")[-1] for obj in invalid_objs]) + self.assertFalse( + invalid, f"jmc2obj export should not alert: {prt}") + + with self.subTest("canon_name_validation"): + self._canonical_name_no_none() + + with self.subTest("test_mappings"): + self._import_materials_util("block_mapping_jmc") + + def test_world_import_legacy_mineways_separated(self): test_subpath = os.path.join( "test_data", "mineways_test_separated_1_15_2.obj") self._import_world_with_settings(file=test_subpath) @@ -206,7 +236,29 @@ def test_world_import_mineways_separated(self): with self.subTest("test_mappings"): self._import_materials_util("block_mapping_mineways") - def test_world_import_mineways_combined(self): + def test_world_import_cmcobj_mineways_separated(self): + test_subpath = os.path.join( + "test_data", "mineways_test_separated_1_21.obj") + self._import_world_with_settings(file=test_subpath) + self.assertEqual(self.addon_prefs.MCprep_exporter_type, "Mineways") + + # UV tool test. Would be in its own test, but then we would be doing + # multiple unnecessary imports of the same world. So make it a subtest. + with self.subTest("test_uv_transform_no_alert_mineways"): + invalid, invalid_objs = detect_invalid_uvs_from_objs( + bpy.context.selected_objects) + prt = ",".join([obj.name for obj in invalid_objs]) + self.assertFalse( + invalid, + f"Mineways separated tiles export should not alert: {prt}") + + with self.subTest("canon_name_validation"): + self._canonical_name_no_none() + + with self.subTest("test_mappings"): + self._import_materials_util("block_mapping_mineways") + + def test_world_import_legacy_mineways_combined(self): test_subpath = os.path.join( "test_data", "mineways_test_combined_1_15_2.obj") self._import_world_with_settings(file=test_subpath) @@ -241,6 +293,41 @@ def test_world_import_mineways_combined(self): with self.subTest("test_mappings"): self._import_materials_util("block_mapping_mineways") + def test_world_import_cmcobj_mineways_combined(self): + test_subpath = os.path.join( + "test_data", "mineways_test_combined_1_21.obj") + self._import_world_with_settings(file=test_subpath) + self.assertEqual(self.addon_prefs.MCprep_exporter_type, "Mineways") + + with self.subTest("test_uv_transform_combined_alert"): + invalid, invalid_objs = detect_invalid_uvs_from_objs( + bpy.context.selected_objects) + self.assertTrue(invalid, "Combined image export should alert") + if not invalid_objs: + self.fail( + "Correctly alerted combined image, but no obj's returned") + + # Do specific checks for water and lava, could be combined and + # cover more than one uv position (and falsely pass the test) in + # combined, water is called "Stationary_Wat" and "Stationary_Lav" + # (yes, appears cutoff; and yes includes the flowing too) + # NOTE! in 2.7x, will be named "Stationary_Water", but in 2.9 it is + # "Test_MCprep_1.16.4__-145_4_1271_to_-118_255_1311_Stationary_Wat" + water_obj = [obj for obj in bpy.data.objects + if "Stationary_Wat" in obj.name][0] + lava_obj = [obj for obj in bpy.data.objects + if "Stationary_Lav" in obj.name][0] + + invalid, invalid_objs = detect_invalid_uvs_from_objs( + [lava_obj, water_obj]) + self.assertTrue(invalid, "Combined lava/water should still alert") + + with self.subTest("canon_name_validation"): + self._canonical_name_no_none() + + with self.subTest("test_mappings"): + self._import_materials_util("block_mapping_mineways") + def test_world_import_fails_expected(self): testdir = os.path.dirname(__file__) obj_path = os.path.join(testdir, "fake_world.obj") @@ -304,27 +391,16 @@ def test_convert_mtl_simple(self): # framework, hence we'll just clear the world_tool's vars. save_init = list(world_tools.BUILTIN_SPACES) world_tools.BUILTIN_SPACES = ["NotRealSpace"] - print("TEST: pre", world_tools.BUILTIN_SPACES) - - # Resultant file res = world_tools.convert_mtl(tmp_mtl) - - # Restore the property we unset. world_tools.BUILTIN_SPACES = save_init - print("TEST: post", world_tools.BUILTIN_SPACES) - self.assertIsNotNone( + self.assertTrue( res, - "Failed to mock color space and thus could not test convert_mtl") - - self.assertTrue(res, "Convert mtl failed with false response") - - # Now check that the data is the same. + "Should return false ie skipped conversion") res = filecmp.cmp(tmp_mtl, modified_mtl, shallow=False) + os.remove(tmp_mtl) # Cleanup first, in case assert fails self.assertTrue( res, f"Generated MTL is different: {tmp_mtl} vs {modified_mtl}") - # Not removing file, since we likely want to inspect it. - os.remove(tmp_mtl) def test_convert_mtl_skip(self): """Ensures that we properly skip if a built in space active.""" @@ -356,11 +432,10 @@ def test_convert_mtl_skip(self): # Restore the property we unset. world_tools.BUILTIN_SPACES = save_init - # print("TEST: post", world_tools.BUILTIN_SPACES) if res is not None: os.remove(tmp_mtl) - self.assertIsNone(res, "Should not have converter MTL for valid space") + self.assertFalse(res, "Should not have converter MTL for valid space") if __name__ == '__main__': diff --git a/visuals/MCprep_mobs_included.png b/visuals/MCprep_mobs_included.png index d731c302..a677c1e1 100644 --- a/visuals/MCprep_mobs_included.png +++ b/visuals/MCprep_mobs_included.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:99db969086df0a009a136dfb6a2995421a3ed7f19808322a00dc2961045d59a6 -size 265919 +oid sha256:701befa5a30f4f40c074cfa2713dee634adbb86ce0228cbbba93f6a348dd0303 +size 252718