Compare commits

...

12 Commits

Author SHA1 Message Date
Colin Basnett
c51a8f21df Fixed a bug with materials in PSK export 2024-04-21 00:55:58 -07:00
Colin Basnett
06b1dd10a8 Merged main 2024-04-12 11:39:48 -07:00
Colin Basnett
ff090baffb Updated localizations 2024-03-24 23:30:36 -07:00
Colin Basnett
7e1693dbb3 Translated the README the Japanese 2024-03-22 23:58:51 -07:00
Colin Basnett
56c847e791 Initial commit of scripts for importing & exporting translations from weblate 2024-03-19 01:08:21 -07:00
Colin Basnett
d48534f1a5 Loads of work on localization
Also updated some of the operator UI to use the new panels
2024-03-15 03:02:15 -07:00
Colin Basnett
ea539d542f Merge branch 'blender-4.1' into i18n-jp
# Conflicts:
#	io_scene_psk_psa/__init__.py
#	io_scene_psk_psa/psk/export/operators.py
#	io_scene_psk_psa/psk/export/ui.py
#	io_scene_psk_psa/psk/import_/operators.py
2024-03-14 19:19:19 -07:00
Colin Basnett
7adf817673 Some more 日本語 translations 2023-10-19 12:59:12 -07:00
Colin Basnett
f92231091c Merge branch 'blender-4.0' into i18n-jp
# Conflicts:
#	io_scene_psk_psa/psa/export/properties.py
#	io_scene_psk_psa/types.py
2023-10-04 23:29:31 -07:00
Colin Basnett
66fa656b12 More localization work 2023-08-04 16:28:31 -07:00
Colin Basnett
a73655fc98 Fixed a bug where the action list would not be populated for PSA export if the armature had not had it's animation data initialized 2023-08-04 16:27:50 -07:00
Colin Basnett
38a8dab100 Initial commit for Japanese translations 2023-08-01 21:49:29 -07:00
27 changed files with 743 additions and 183 deletions

3
.gitmodules vendored Normal file
View File

@@ -0,0 +1,3 @@
[submodule "extern/io_scene_psk_psa-translations"]
path = extern/io_scene_psk_psa-translations
url = https://github.com/DarklightGames/io_scene_psk_psa-translations.git

84
README-JP.md Normal file
View File

@@ -0,0 +1,84 @@
[![Blender](https://img.shields.io/badge/Blender->=2.9-blue?logo=blender&logoColor=white)](https://www.blender.org/download/ "Download Blender")
[![GitHub release](https://img.shields.io/github/release/DarklightGames/io_scene_psk_psa?include_prereleases=&sort=semver&color=blue)](https://github.com/DarklightGames/io_scene_psk_psa/releases/)
[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/L4L3853VR)
この Blender アドオンを使用すると、Unreal Engine の多くのバージョンで使用されている [PSK および PSA ファイル形式](https://wiki.beyondunreal.com/PSK_%26_PSA_file_formats) との間でメッシュとアニメーションをインポートおよびエクスポートできます。
# 互換性
| Blenderのバージョン | アドオンのバージョン | Long Term Support |
|--------------------------------------------------------------|--------------------------------------------------------------------------------|-------------------|
| 4.0+ | [最新](https://github.com/DarklightGames/io_scene_psk_psa/releases/latest) | 未定 |
| [3.4 - 3.6](https://www.blender.org/download/lts/3-6/) | [5.0.5](https://github.com/DarklightGames/io_scene_psk_psa/releases/tag/5.0.5) | ✅️ 2025年6月 |
| [2.93 - 3.3](https://www.blender.org/download/releases/3-3/) | [4.3.0](https://github.com/DarklightGames/io_scene_psk_psa/releases/tag/4.3.0) | ✅️ 202年9月 |
バグ修正は、[Blender の LTS メンテナンス期間](https://www.blender.org/download/lts/)中のレガシー アドオン バージョンに対して発行されます。 LTS 期間が終了すると、レガシー アドオン バージョンはこのリポジトリのメンテナによってサポートされなくなりますが、バグ修正のためのプル リクエストは受け付けます。
# 機能
* PSK および PSA ファイルをインポートする。
* 非標準のファイル セクション データ (頂点法線、追加の UV チャンネル、頂点カラー、シェイプ キーなど) をインポートします。
* 多数のシーケンスを含む PSA ファイルを操作する場合の効率的なワークフローのためのきめ細かい PSA シーケンス インポート。
* PSA シーケンスのメタデータ (フレーム レートなど) はインポート時に保存されるため、このデータをエクスポート時に再利用できます。
* ボーン コレクションは PSK/PSA エクスポートから除外できます。 これは、IK コントローラなどの寄与しないボーンを除外する場合に便利です。
* PSA シーケンスは、アクションから直接エクスポートすることも、シーンの [タイムライン マーカー](https://docs.blender.org/manual/ja/latest/animation/markers.html) または NLA トラック ストリップを使用して描写することもでき、[NLA](https://docs.blender.org/manual/ja/latest/editors/nla/index.html) シーケンスを作成するとき。
* 複数のメッシュ オブジェクトをエクスポートするときにマテリアル スロットを手動で並べ替えます。
# 取り付け
1. 最新バージョンの zip ファイルを [Releases](https://github.com/DarklightGames/io_export_psk_psa/releases) ページからダウンロードします。
2. Blender 4.0.0 以降を開きます。
3. Blender の設定に移動します (`編集` > `プリファレンス`)。
4. `アドオン`タブを選択します。
5. `インストール...`ボタンをクリックします。
6. 先ほどダウンロードした `.zip` ファイルを選択し、`アドオンのインストール`をクリックします。
7. 新しく追加された`インポート-エクスポート: PSK/PSA Importer/Exporter`アドオンを有効にします。
# 使用方法
## PSKのエクスポート
1. エクスポートするアーマチュア オブジェクトを選択します。
2. `ファイル` > `エクスポート` > `Unreal PSK (.psk)`に移動します.
3. ファイル名を入力して`エクスポート`をクリックします.
## PSK/PSKXのインポート
1. `ファイル` > `インポート` > `Unreal PSK (.psk/.pskx)`に移動します.
2. インポートしたい PSK ファイルを選択し、`インポート`をクリックします。
## PSAのエクスポート
1. エクスポートするアーマチュア オブジェクトを選択します。
2. `ファイル` > `エクスポート` > `Unreal PSA (.psa)`に移動します.
3. ファイル名を入力し、`エクスポート`をクリックします。
## PSAのインポート
1. アニメーションを読み込みたいアーマチュアを選択します。
2. `ファイル` > `インポート` > `Unreal PSA (.psa)`に移動します。
3. アニメーションのインポート元となる PSA ファイルを選択します.
4. インポートしたいアニメーションを選択し、`インポート`をクリックします。
> アーマチュアに適用されたインポートされたアクションを確認するには、[ドープ シート](https://docs.blender.org/manual/ja/latest/editors/dope_sheet/introduction.html) エディタまたは [ノンリニアアニメーション](https://docs.blender.org/manual/ja/latest/editors/nla/introduction.html) エディタを使用する必要があります。
## ローカライゼーション
現在、次の言語がサポートされています。
| 言語 | 地位 | 方法 |
|-|-|-|
| 英語 (English) | ✅️ | 🧑 |
| 日本語 | ✅️ | 🤖 |
翻訳に問題がある場合は、お気軽に [Weblate](https://weblate.darklightgames.com/projects/io_scene_psk_psa/) に修正を送信してください。
自分の母国語に対するサポートの追加をご希望の場合は、[問題を報告して](http://github.com/DarklightGames/io_scene_psk_psa/issues)ください。
# FAQ
## PSA をインポートした後にアニメーションが表示されないのはなぜですか?
PSA アニメーションをインポートしても、アクションはアーマチュアに自動的に適用されません。これは、PSA には複数のシーケンスをインポートできるため、また、インポーターが必要のないときにシーンを変更するのは悪い形式であるためです。
PSA インポーターは、PSA 内の選択されたシーケンスごとに[アクション](https://docs.blender.org/manual/ja/latest/animation/actions.html)を作成します。これらのアクションは、[アクション エディター](https://docs.blender.org/manual/ja/latest/editors/dope_sheet/action.html)または [NLA エディター](https://docs.blender.org/manual/ja/latest/editors/nla/index.html)を介してアーマチュアに適用できます。
## [UE Viewer](https://www.gildor.org/en/projects/umodel) から抽出した PSK をインポートすると、メッシュ面の法線が不正確になるのはなぜですか?
モデルのメッシュ法線を保持することがワークフローにとって重要な場合、UE Viewer から PSK ファイルをエクスポートすることはお勧めできません。これは、UE Viewer が元の[スムージング グループ](https://en.wikipedia.org/wiki/Smoothing_group)を再構築しようとしないためです。その結果、インポートされた PSK ファイルの法線は、Blender にインポートされたときに不正確になるため、手動で修正する必要があります。
回避策として、[glTF](https://en.wikipedia.org/wiki/GlTF) 形式は明示的な法線をサポートしており、UE Viewer はエクスポート時にメッシュ法線を正しく保存できるため、代わりに UE Viewer から glTF メッシュをエクスポートすることをお勧めします。ただし、インポートされた glTF アーマチュアは、Blender にインポートされたときにボーンの方向が間違っている可能性があります。これを軽減するには、PSK のアーマチュアと glTF のメッシュを組み合わせて最良の結果を得ることができます。

View File

@@ -5,7 +5,7 @@
This Blender addon allows you to import and export meshes and animations to and from the [PSK and PSA file formats](https://wiki.beyondunreal.com/PSK_%26_PSA_file_formats) used in many versions of the Unreal Engine.
## Compatibility
# Compatibility
| Blender Version | Addon Version | Long Term Support |
|--------------------------------------------------------------|--------------------------------------------------------------------------------|-------------------|
@@ -56,6 +56,19 @@ Bug fixes will be issued for legacy addon versions that are under [Blender's LTS
> Note that in order to see the imported actions applied to your armature, you must use the [Dope Sheet](https://docs.blender.org/manual/en/latest/editors/dope_sheet/introduction.html) or [Nonlinear Animation](https://docs.blender.org/manual/en/latest/editors/nla/introduction.html) editors.
## Localization
The following languages are currently supported:
| Language | Status | Method |
|----------------|--------|--------|
| English | ✅️ | 🧑 |
| Japanese (日本語) | ✅️ | 🤖 |
If you notice any issues with the translations, please feel free to submit a correction on [Weblate](https://weblate.darklightgames.com/projects/io_scene_psk_psa/).
If you would like to see support add for your own native language, please [open an issue](http://github.com/DarklightGames/io_scene_psk_psa/issues) and we will consider adding it.
# FAQ
## Why can't I see the animations imported from my PSA?

View File

@@ -18,6 +18,7 @@ if 'bpy' in locals():
importlib.reload(psx_data)
importlib.reload(psx_helpers)
importlib.reload(psx_types)
importlib.reload(psx_i18n)
importlib.reload(psk_data)
importlib.reload(psk_reader)
@@ -48,6 +49,7 @@ else:
from . import data as psx_data
from . import helpers as psx_helpers
from . import types as psx_types
from . import i18n as psx_i18n
from .psk import data as psk_data
from .psk import reader as psk_reader
from .psk import writer as psk_writer
@@ -110,6 +112,7 @@ def psa_import_menu_func(self, context):
def register():
for cls in classes:
bpy.utils.register_class(cls)
bpy.types.TOPBAR_MT_file_export.append(psk_export_menu_func)
bpy.types.TOPBAR_MT_file_import.append(psk_import_menu_func)
bpy.types.TOPBAR_MT_file_export.append(psa_export_menu_func)
@@ -120,8 +123,11 @@ def register():
bpy.types.Scene.psk_export = PointerProperty(type=psk_export_properties.PSK_PG_export)
bpy.types.Action.psa_export = PointerProperty(type=psx_types.PSX_PG_action_export)
bpy.app.translations.register(__name__, psx_i18n.langs)
def unregister():
bpy.app.translations.unregister(__name__)
del bpy.types.Material.psk
del bpy.types.Scene.psa_import
del bpy.types.Scene.psa_export

View File

@@ -60,7 +60,7 @@ def populate_bone_collection_list(armature_object: Object, bone_collection_list:
return
item = bone_collection_list.add()
item.name = 'Unassigned'
item.name = bpy.app.translations.pgettext_iface('Unassigned')
item.index = -1
# Count the number of bones without an assigned bone collection
item.count = sum(map(lambda bone: 1 if len(bone.collections) == 0 else 0, armature.bones))
@@ -78,9 +78,12 @@ def check_bone_names(bone_names: Iterable[str]):
pattern = re.compile(r'^[a-zA-Z\d_\- ]+$')
invalid_bone_names = [x for x in bone_names if pattern.match(x) is None]
if len(invalid_bone_names) > 0:
raise RuntimeError(f'The following bone names are invalid: {invalid_bone_names}.\n'
f'Bone names must only contain letters, numbers, spaces, hyphens and underscores.\n'
f'You can bypass this by disabling "Enforce Bone Name Restrictions" in the export settings.')
message = bpy.app.translations.pgettext_iface(
'The following bone names are invalid: {invalid_bone_names}.\n'
'Bone names must only contain letters, numbers, spaces, hyphens and underscores.\n'
'You can bypass this by disabling "Enforce Bone Name Restrictions" in the export settings'
)
raise RuntimeError(message.format(invalid_bone_names=str(invalid_bone_names)))
def get_export_bone_names(armature_object: Object, bone_filter_mode: str, bone_collection_indices: List[int]) -> List[str]:
@@ -153,13 +156,18 @@ def get_export_bone_names(armature_object: Object, bone_filter_mode: str, bone_c
# TODO: in future, it would be preferential to have a readout of *all* instigator bones.
instigator_bone_name = instigator_bone_names[bone_names.index(bone_name)]
if instigator_bone_name is None:
print(f'Root bone "{root_bone_name}" was included because {bone_name} was marked for export')
message = bpy.app.translations.pgettext_iface('Root bone "{root_bone_name}" was included because {bone_name} was marked for export')
message = message.format(root_bone_name=root_bone_name, bone_name=bone_name)
print(message)
break
bone_name = instigator_bone_name
raise RuntimeError('Exported bone hierarchy must have a single root bone.\n'
f'The bone hierarchy marked for export has {len(root_bones)} root bones: {root_bone_names}.\n'
f'Additional debugging information has been written to the console.')
message = bpy.app.translations.pgettext_iface(
'Exported bone hierarchy must have a single root bone.\n'
'The bone hierarchy marked for export has {root_bone_count} root bones: {root_bone_names}.\n'
'Additional debugging information has been written to the console.'
)
raise RuntimeError(message.format(root_bone_count=len(root_bones), root_bone_names=str(root_bone_names)))
return bone_names

216
io_scene_psk_psa/i18n.py Normal file
View File

@@ -0,0 +1,216 @@
langs = {'ja': {('*', 'Action Metadata'): 'アクションメタデータ',
('*', 'Additional data will be written to the properties of the Action (e.g., frame rate)'): 'アクションのプロパティに追加データが書き込まれます '
'(フレームレートなど)',
('*', 'All bones will be exported'): 'すべてのボーンがエクスポートされます',
('*', 'All selected meshes must have the same armature modifier, encountered {count} ({names})'): '選択したすべてのメッシュには同じアーマチュア修飾子が必要です。検出されたアーマチュア修飾子は '
'{count} '
'({names})',
('*', 'An armature must be selected'): 'アーマチュアを選択する必要があります',
('*', 'An armature object must be supplied'): 'アーマチュアオブジェクトを指定する必要があります',
('*', 'Assign each imported action a fake user so that the data block is always saved'): 'インポートされた各アクションにフェイクユーザーを割り当てて、データブロックが常に保存されるようにします',
('*', 'At least one bone must be marked for export'): '少なくとも 1 '
'つのボーンをエクスポート対象としてマークする必要があります',
('*', 'At least one mesh must be selected'): '少なくとも 1 '
'つのメッシュを選択する必要があります',
('*', 'Bone Collections'): 'ボーンコレクション',
('*', 'Bone Filter'): 'ボーンフィルター',
('*', 'Bone Length'): 'ボーンの長さ',
('*', 'Bone Name Mapping'): 'ボーン名マッピング',
('*', 'Bone name "{name}" contains characters that cannot be encoded in the Windows-1252 codepage'): 'ボーン名「{name}」には、Windows-1252 '
'コードページではエンコードできない文字が含まれています',
('*', 'Bone names must match exactly'): 'ボーン名は正確に一致する必要があります',
('*', 'Bone names restrictions will be enforced. Note that bone names without properly formatted names may not be able to be referenced by some versions of the Unreal Engine'): 'ボーン名の制限が適用されます。ボーン名が正しくフォーマットされていないと、Unreal '
'Engine '
'のバージョンによっては参照できない場合があることに注意してください',
('*', 'Bones'): 'ボーンズ',
('*', "Bones names must match, ignoring case (e.g., the PSA bone 'aBcDeF' can be mapped to the armature bone 'ABCDEF')"): 'ボーンの名前は大文字と小文字を区別しないで一致する必要があります '
'(例:PSAボーン '
"'abcDef' "
'はアーマチュアボーン '
"'ABCDEF' "
'にマッピングできます)',
('*', 'Case Insensitive'): '大文字と小文字を区別しない',
('*', 'Compression Ratio'): '圧縮率',
('*', 'Context'): 'コンテキスト',
('*', 'Convert keyframes to read-only samples. Recommended if you do not plan on editing the actions directly'): 'キーフレームを読み取り専用サンプルに変換します。アクションを直接編集する予定がない場合におすすめです',
('*', 'Convert to Samples'): 'サンプルに変換',
('*', 'Custom FPS'): 'カスタムFPS',
('*', 'Deselect All'): 'すべて選択解除',
('*', 'Deselect all bone collections'): 'すべてのボーンコレクションを選択解除',
('*', 'Deselect all visible sequences'): '表示されているシーケンスをすべて選択解除',
('*', 'Discarded {count} invalid face(s)'): '{count}個の無効なフェースが廃棄されました',
('*', 'Duplicate action: {action_name}'): 'アクションが重複しています:{action_name}',
('*', 'Each sequence name should be on a new line'): '各シーケンス名は新しい行にする必要があります',
('*', 'Enforce Bone Name Restrictions'): 'ボーン名制限を強制',
('*', 'Enforce that bone names must only contain letters, numbers, spaces, hyphens and underscores.\n\nDepending on the engine, improper bone names might not be referenced correctly by scripts'): 'ボーン名には文字、数字、スペース、ハイフン、アンダースコアのみを含めるようにしてください。\n'
'\n'
'エンジンによっては、不適切なボーン名がスクリプトで正しく参照されない場合があります',
('*', 'Exact'): '正確',
('*', 'Export'): 'エクスポート',
('*', 'Export actions to PSA'): 'アクションをPSAにエクスポート',
('*', 'Export mesh and armature to PSK'): 'メッシュとアーマチュアを PSK にエクスポート',
('*', 'Exported bone hierarchy must have a single root bone.\nThe bone hierarchy marked for export has {root_bone_count} root bones: {root_bone_names}.\nAdditional debugging information has been written to the console.'): 'エクスポートされたボーン階層にはルートボーンが '
'1 '
'つ必要です。\n'
'エクスポート対象としてマークされたボーン階層には '
'{root_bone_count} '
'個のルートボーン '
'({root_bone_names}) '
'があります。\n'
'追加のデバッグ情報がコンソールに書き込まれました。',
('*', 'Extra UVs'): '追加のUV',
('*', 'FPS Source'): 'FPSソース',
('*', 'Failed to read PSA config file: {error}'): 'PSA構成ファイルを読み込めませんでした:{error}',
('*', 'Fake User'): 'フェイクユーザー',
('*', 'File Path'): 'ファイルパス',
('*', 'File path used for exporting the PSA file'): 'PSAファイルのエクスポートに使用するファイルパス',
('*', 'File path used for exporting the PSK file'): 'PSKファイルのエクスポートに使用するファイルパス',
('*', 'File path used for importing the PSA file'): 'PSAファイルのインポートに使用するファイルパス',
('*', 'Filter by Name'): '名前で絞り込む',
('*', 'Filter using regular expressions'): '正規表現を使用してフィルタリングする',
('*', 'Flags'): '国旗',
('*', "If an action with a matching name already exists, the existing action will have it's data overwritten instead of a new action being created"): '一致する名前のアクションが既に存在する場合、新しいアクションが作成されるのではなく、既存のアクションのデータが上書きされます',
('*', 'Import'): 'インポート',
('*', 'Import Extra UVs'): '追加のUVをインポート',
('*', 'Import Shape Keys'): 'シェイプキーをインポート',
('*', 'Import Vertex Colors'): '頂点カラーをインポート',
('*', 'Import Vertex Normals'): '頂点法線をインポート',
('*', 'Import extra UVs, if available'): '追加の UV (可能な場合) をインポートする',
('*', 'Import shape keys, if available'): 'シェイプキーをインポートする (可能な場合)',
('*', 'Import the selected animations into the scene as actions'): '選択したアニメーションをアクションとしてシーンにインポートします',
('*', 'Import vertex colors, if available'): '頂点カラーをインポート (可能な場合)',
('*', 'Import vertex normals, if available'): '頂点法線 (可能な場合) をインポートする',
('*', 'Imported {action_count} action(s)'): 'インポートされた{action_count}アクション',
('*', 'Imported {action_count} action(s) with {warning_count} warning(s)'): '{action_count}個のアクションが{warning_count}個の警告付きでインポートされました',
('*', 'Invalid FPS source: {fps_source}'): 'FPSソースが無効です:{fps_source}',
('*', 'Invert filtering (show hidden items, and vice versa)'): '反転フィルタリング '
'(隠しアイテムを表示、その逆)',
('*', 'Keyframe Quota'): 'キーフレームクォータ',
('*', 'Load a PSK file'): 'PSKファイルを読み込む',
('*', 'Material name "{name}" contains characters that cannot be encoded in the Windows-1252 codepage'): 'マテリアル名「{name}」には、Windows-1252 '
'コードページではエンコードできない文字が含まれています',
('*', 'Material slot cannot be empty (index {index})'): 'マテリアルスロットを空にすることはできません '
'(インデックス{index})',
('*', 'Materials'): 'マテリアル',
('*', 'Mesh "{object_name}" must have at least one material'): 'メッシュ「{object_name}」には少なくとも1つのマテリアルが必要です',
('*', 'Mesh "{object_name}" must have only one armature modifier'): 'メッシュ「{object_name}」にはアーマチュアモディファイヤが '
'1 '
'つだけ必要です',
('*', 'Metadata'): 'メタデータ',
('*', 'Modulate'): '変調する',
('*', 'Move the selected material down one slot'): '選択したマテリアルを1スロット下に移動',
('*', 'Move the selected material up one slot'): '選択したマテリアルを1つ上のスロットに移動',
('*', 'NLA Track'): 'NLAトラック',
('*', 'NLA Track Index'): 'NLAトラックインデックス',
('*', 'NLA Track Strips'): 'NLAトラックストリップ',
('*', 'No NLA track strips were selected for export'): 'エクスポート対象のNLAトラックストリップが選択されていません',
('*', 'No Smooth'): 'スムーズなし',
('*', 'No actions were selected for export'): 'エクスポートするアクションが選択されていません',
('*', 'No animation data for object "{name}"'): 'オブジェクト「{name}」のアニメーションデータはありません',
('*', 'No bones available for export'): 'エクスポートできるボーンはありません',
('*', 'No modifiers will be evaluated as part of the exported mesh'): 'エクスポートされたメッシュの一部として評価されるモディファイヤはありません',
('*', 'No sequences selected'): 'シーケンスが選択されていません',
('*', 'No text block selected'): 'テキストブロックが選択されていません',
('*', 'No timeline markers were selected for export'): 'エクスポートするタイムラインマーカーが選択されていません',
('*', 'Normal Two-Sided'): 'ノーマル両面',
('*', 'Nothing to import'): 'インポートするものはありません',
('*', 'Number of bones ({bone_count}) exceeds limit of {MAX_BONE_COUNT}'): 'ボーンの数({bone_count})が{MAX_BONE_COUNT}の制限を超えています',
('*', 'Number of materials ({material_count}) exceeds limit of {MAX_MATERIAL_COUNT}'): 'マテリアルの数({material_count})が{MAX_MATERIAL_COUNT}の制限を超えています',
('*', 'Number of wedges ({wedge_count}) exceeds limit of {MAX_WEDGE_COUNT}'): 'ウェッジの数({wedge_count})が{MAX_WEDGE_COUNT}の制限を超えています',
('*', 'Numbers of vertices ({point_count}) exceeds limit of {MAX_POINT_COUNT}'): '頂点の数({point_count})が{MAX_POINT_COUNT}の制限を超えています',
('*', 'Only Show Selected'): '選択したものだけを表示',
('*', 'Only bones belonging to the selected bone collections and their ancestors will be exported'): '選択したボーンコレクションに属するボーンとその祖先だけがエクスポートされます',
('*', "Only show items matching this name (use '*' as wildcard)"): 'この名前に一致するアイテムのみを表示 '
'(ワイルドカードとして「*」を使用)',
('*', 'Only show selected sequences'): '選択したシーケンスのみを表示',
('*', 'Override Animation Data'): 'アニメーションデータをオーバーライドする',
('*', 'PSA Export'): 'PSAエクスポート',
('*', 'PSA bone {bone_index} ({bone_name}) could not be mapped to armature bone {armature_bone_index} ({armature_bone_name}) because the armature bone is already mapped to PSA bone {mapped_psa_bone_index} ({mapped_psa_bone_name})'): 'アーマチュアボーンは既に '
'PSA '
'ボーン{mapped_psa_bone_index}({mapped_psa_bone_name})にマップされているため、PSA '
'ボーン{bone_index}({bone_name})をアーマチュアボーン{armature_bone_index}({mapped_psa_bone_name})にマッピングできませんでした',
('*', 'PSA export successful'): 'PSAのエクスポートが成功しました',
('*', 'PSK Material'): 'PSKマテリアル',
('*', 'PSK export successful'): 'PSKのエクスポートが成功しました',
('*', 'PSK export successful with {warning_count} warnings'): 'PSKエクスポートは成功しましたが、警告が{warning_count}件ありました',
('*', 'PSK imported ({name})'): 'PSKがインポートされました ({name})',
('*', 'PSK imported with {count} warning(s)'): 'PSKをインポートすると{count}つの警告が発生しました',
('*', 'PSK/PSA Import/Export (.psk/.psa)'): 'PSK/PSA インポート/エクスポート '
'(.psk/.psa)',
('*', 'PSK/PSA Importer/Exporter'): 'PSK/PSA インポーター/エクスポーター',
('*', 'Prefix Action Name'): 'プレフィックスアクション名',
('*', 'Raw Mesh Data'): '未加工メッシュデータ',
('*', 'Regular Expression'): '正規表現',
('*', 'RemoveTracks'): 'RemoveTracks',
('*', 'Reversed'): '逆転しました',
('*', 'Root Motion'): 'ルートモーション',
('*', 'Root bone "{root_bone_name}" was included because {bone_name} was marked for export'): 'ルートボーン「{root_bone_name}」が含まれたのは、「{bone_name}」がエクスポート対象としてマークされているためです',
('*', 'Select All'): 'すべて選択',
('*', 'Select By Text List'): 'テキストリストで選択',
('*', 'Select a PSA file'): 'PSAファイルを選択',
('*', 'Select all bone collections'): 'すべてのボーンコレクションを選択',
('*', 'Select all visible sequences'): '表示されているシーケンスをすべて選択',
('*', 'Select sequences by name from text list'): 'テキストリストから名前でシーケンスを選択',
('*', 'Selected object "{object_name}" is not a mesh'): '選択したオブジェクト「{object_name}」はメッシュではありません',
('*', 'Selected object must be an Armature'): '選択したオブジェクトはアーマチュアでなければなりません',
('*', 'Selected {count} sequence(s)'): '選択した{count}シーケンス',
('*', 'Sequence'): 'シーケンス',
('*', 'Sequence name "{name}" contains characters that cannot be encoded in the Windows-1252 codepage'): 'シーケンス名「{name}」には、Windows-1252 '
'コードページではエンコードできない文字が含まれています',
('*', 'Sequences'): 'シーケンス',
('*', 'Sequences are delineated by scene timeline markers'): 'シーケンスはシーンのタイムラインマーカーによって描かれます',
('*', 'Sequences are delineated by the start & end times of strips on the selected NLA track'): 'シーケンスは、選択した '
'NLA '
'トラックのストリップの開始時間と終了時間によって示されます',
('*', 'Sequences will be exported using actions'): 'シーケンスはアクションを使用してエクスポートされます',
('*', 'Shape Keys'): 'シェイプキー',
('*', 'Show actions that belong to an asset library'): 'アセットライブラリに属するアクションを表示',
('*', 'Show reversed sequences'): '逆のシーケンスを表示',
('*', 'Source'): 'ソース',
('*', 'Stash'): '保留',
('*', 'Stash each imported action as a strip on a new non-contributing NLA track'): 'インポートした各アクションを、コントリビューションされていない新しい '
'NLA '
'トラックにストリップとして保存する',
('*', 'The active object must be an armature'): 'アクティブオブジェクトはアーマチュアでなければなりません',
('*', "The armature '{armature_name}' is missing {count} bones that exist in the PSA:\n{missing_bone_names}"): 'アーマチュア「{armature_name}」にはPSAに存在する{count}個のボーンがありません:\n'
'{missing_bone_names}',
('*', 'The following bone names are invalid: {invalid_bone_names}.\nBone names must only contain letters, numbers, spaces, hyphens and underscores.\nYou can bypass this by disabling "Enforce Bone Name Restrictions" in the export settings.'): '次のボーン名は無効です:{invalid_bone_names}\n'
'ボーン名に使用できるのは、文字、数字、スペース、ハイフン、アンダースコアのみです。\n'
'エクスポート設定で「ボーン名制限を強制」を無効にすることで、これを回避できます',
('*', 'The frame rate of the exported sequence'): 'エクスポートされたシーケンスのフレームレート',
('*', 'The frame rate to which the imported sequences will be resampled to'): 'インポートしたシーケンスをリサンプリングするときのフレームレート',
('*', "The frame rate will be determined by action's FPS property found in the PSA Export panel.\n\nIf the Sequence Source is Timeline Markers, the lowest value of all contributing actions will be used"): 'フレームレートは、PSAエクスポートのパネルにあるアクションのFPSプロパティによって決まります。\n'
'\n'
'シーケンスソースがタイムラインマーカーの場合、関連するすべてのアクションのうち最も低い値が使用されます',
('*', 'The keyframe sampling ratio of the exported sequence.\n\nA compression ratio of 1.0 will export all frames, while a compression ratio of 0.5 will export half of the frames'): 'エクスポートされたシーケンスのキーフレームサンプリング比。\n'
'\n'
'圧縮率が1.0の場合はすべてのフレームがエクスポートされ、圧縮率が0.5の場合はフレームの半分がエクスポートされます',
('*', 'The method by which bones from the PSA file are mapped to the bones of the armature'): 'PSAファイルのボーンをアーマチュアのボーンにマッピングする方法',
('*', 'The minimum number of keyframes to be exported'): 'エクスポートするキーフレームの最小数',
('*', 'The selected object must be an armature'): '選択したオブジェクトはアーマチュアでなければなりません',
('*', 'The sequence frame rate matches the original frame rate'): 'シーケンスのフレームレートは元のフレームレートと一致します',
('*', 'The sequence is resampled to a custom frame rate'): 'シーケンスはカスタムフレームレートにリサンプリングされます',
('*', 'The sequence is resampled to the frame rate of the scene'): 'シーケンスはシーンのフレームレートに合わせてリサンプリングされます',
('*', 'The source vertex color space'): 'ソース頂点カラースペース',
('*', 'Translucent'): '半透明',
('*', 'Triangle Bit Flags'): 'トライアングルビットフラグ',
('*', 'Triangle Type'): 'トライアングルタイプ',
('*', 'Unhandled section "{section_name}" at position {position}'): '{position}の位置にある未処理のセクション「{section_name}',
('*', 'Unhandled sequence source: {sequence_source}'): '未処理のシーケンスソース:{sequence_source}',
('*', 'Unlit'): '明かりなし',
('*', 'Unreal PSA (.psa)'): 'アンリアルPSA (.psa)',
('*', 'Unreal PSK (.psk)'): 'アンリアルPSK (.psk)',
('*', 'Unreal PSK (.psk/.pskx)'): 'アンリアルPSK (.psk/.pskx)',
('*', 'Unrecognized wedge format'): '認識されないウェッジフォーマット',
('*', 'Use Config File'): '設定ファイルを使用',
('*', 'Use the .config file that is sometimes generated when the PSA file is exported from UEViewer. This file contains options that can be used to filter out certain bones tracks from the imported actions'): 'PSA '
'ファイルをUEViewerからエクスポートしたときに生成されることがある.configファイルを使用してください。このファイルには、インポートしたアクションから特定のボーントラックを除外するためのオプションが含まれています',
('*', 'Use the animation data from a different object instead of the selected object'): '選択したオブジェクトの代わりに別のオブジェクトのアニメーションデータを使用する',
('*', 'Vertex Color Space'): '頂点カラースペース',
('*', 'Vertex Colors'): '頂点カラー',
('*', 'Vertex Normals'): '頂点法線',
('*', 'When enabled, the root bone will be transformed as it appears in the scene.\n\nYou might want to disable this if you are exporting an animation for an armature that is attached to another object, such as a weapon or a shield'): '有効にすると、ルートボーンはシーンに表示されているとおりにトランスフォームされます。\n'
'\n'
'武器や盾など、別のオブジェクトにアタッチされているアーマチュアのアニメーションをエクスポートする場合は、これを無効にすると良いかもしれません',
('*', 'Write'): '書き込み',
('*', 'sRGBA'): 'sRGBA'}}

View File

@@ -1,5 +1,6 @@
from typing import Optional
import bpy.app.translations
from bpy.types import Armature, Bone, Action, PoseBone
from .data import *
@@ -95,7 +96,8 @@ def build_psa(context: bpy.types.Context, options: PsaBuildOptions) -> Psa:
try:
psa_bone.name = bytes(bone.name, encoding='windows-1252')
except UnicodeEncodeError:
raise RuntimeError(f'Bone name "{bone.name}" contains characters that cannot be encoded in the Windows-1252 codepage')
message = bpy.app.translations.pgettext_iface('Bone name "{name}" contains characters that cannot be encoded in the Windows-1252 codepage')
raise RuntimeError(message.format(name=bone.name))
try:
parent_index = bones.index(bone.parent)
@@ -172,7 +174,9 @@ def build_psa(context: bpy.types.Context, options: PsaBuildOptions) -> Psa:
try:
psa_sequence.name = bytes(export_sequence.name, encoding='windows-1252')
except UnicodeEncodeError:
raise RuntimeError(f'Sequence name "{export_sequence.name}" contains characters that cannot be encoded in the Windows-1252 codepage')
message = bpy.app.translations.pgettext_iface('Sequence name "{name}" contains characters that cannot be encoded in the Windows-1252 codepage')
message = message.format(name=export_sequence.name)
raise RuntimeError(message)
psa_sequence.frame_count = frame_count
psa_sequence.frame_start_index = frame_start_index
psa_sequence.fps = frame_count / sequence_duration

View File

@@ -100,7 +100,9 @@ def get_sequence_fps(context: Context, fps_source: str, fps_custom: float, actio
# Get the minimum value of action metadata FPS values.
return min([action.psa_export.fps for action in actions])
case _:
raise RuntimeError(f'Invalid FPS source "{fps_source}"')
message = bpy.app.translations.pgettext_iface('Invalid FPS source: {fps_source}')
message = message.format(fps_source=fps_source)
raise RuntimeError(message)
def get_animation_data_object(context: Context) -> Object:
@@ -234,79 +236,94 @@ class PSA_OT_export(Operator, ExportHelper):
layout = self.layout
pg = getattr(context.scene, 'psa_export')
# FPS
layout.prop(pg, 'fps_source', text='FPS')
if pg.fps_source == 'CUSTOM':
layout.prop(pg, 'fps_custom', text='Custom')
sequences_header, sequences_panel = layout.panel('sequences_panel_id', default_closed=False)
sequences_header.label(text='Sequences', icon='ACTION')
# SOURCE
layout.prop(pg, 'sequence_source', text='Source')
if pg.sequence_source in {'TIMELINE_MARKERS', 'NLA_TRACK_STRIPS'}:
# ANIMDATA SOURCE
layout.prop(pg, 'should_override_animation_data')
if pg.should_override_animation_data:
layout.prop(pg, 'animation_data_override', text='')
if pg.sequence_source == 'NLA_TRACK_STRIPS':
flow = layout.grid_flow()
if sequences_panel is not None:
flow = sequences_panel.grid_flow()
flow.use_property_split = True
flow.use_property_decorate = False
flow.prop(pg, 'nla_track')
# SELECT ALL/NONE
row = layout.row(align=True)
row.label(text='Select')
row.operator(PSA_OT_export_actions_select_all.bl_idname, text='All', icon='CHECKBOX_HLT')
row.operator(PSA_OT_export_actions_deselect_all.bl_idname, text='None', icon='CHECKBOX_DEHLT')
# FPS
fps_row = flow.row(align=True)
fps_row.prop(pg, 'fps_source', text='FPS')
if pg.fps_source == 'CUSTOM':
fps_row.prop(pg, 'fps_custom', text='')
# ACTIONS
if pg.sequence_source == 'ACTIONS':
rows = max(3, min(len(pg.action_list), 10))
layout.template_list('PSA_UL_export_sequences', '', pg, 'action_list', pg, 'action_list_index', rows=rows)
elif pg.sequence_source == 'TIMELINE_MARKERS':
rows = max(3, min(len(pg.marker_list), 10))
layout.template_list('PSA_UL_export_sequences', '', pg, 'marker_list', pg, 'marker_list_index', rows=rows)
elif pg.sequence_source == 'NLA_TRACK_STRIPS':
rows = max(3, min(len(pg.nla_strip_list), 10))
layout.template_list('PSA_UL_export_sequences', '', pg, 'nla_strip_list', pg, 'nla_strip_list_index', rows=rows)
# SOURCE
flow.prop(pg, 'sequence_source', text='Source')
col = layout.column()
col.use_property_split = True
col.use_property_decorate = False
col.prop(pg, 'sequence_name_prefix')
col.prop(pg, 'sequence_name_suffix')
if pg.sequence_source in {'TIMELINE_MARKERS', 'NLA_TRACK_STRIPS'}:
# ANIMDATA SOURCE
flow.prop(pg, 'should_override_animation_data')
if pg.should_override_animation_data:
flow.prop(pg, 'animation_data_override', text='')
# Determine if there is going to be a naming conflict and display an error, if so.
selected_items = [x for x in pg.action_list if x.is_selected]
action_names = [x.name for x in selected_items]
action_name_counts = Counter(action_names)
for action_name, count in action_name_counts.items():
if count > 1:
layout.label(text=f'Duplicate action: {action_name}', icon='ERROR')
break
if pg.sequence_source == 'NLA_TRACK_STRIPS':
flow = sequences_panel.grid_flow()
flow.use_property_split = True
flow.use_property_decorate = False
flow.prop(pg, 'nla_track')
layout.separator()
# SELECT ALL/NONE
row = sequences_panel.row(align=True)
row.label(text='Select')
row.operator(PSA_OT_export_actions_select_all.bl_idname, text='All', icon='CHECKBOX_HLT')
row.operator(PSA_OT_export_actions_deselect_all.bl_idname, text='None', icon='CHECKBOX_DEHLT')
# ACTIONS
if pg.sequence_source == 'ACTIONS':
rows = max(3, min(len(pg.action_list), 10))
sequences_panel.template_list('PSA_UL_export_sequences', '', pg, 'action_list', pg, 'action_list_index', rows=rows)
elif pg.sequence_source == 'TIMELINE_MARKERS':
rows = max(3, min(len(pg.marker_list), 10))
sequences_panel.template_list('PSA_UL_export_sequences', '', pg, 'marker_list', pg, 'marker_list_index', rows=rows)
elif pg.sequence_source == 'NLA_TRACK_STRIPS':
rows = max(3, min(len(pg.nla_strip_list), 10))
sequences_panel.template_list('PSA_UL_export_sequences', '', pg, 'nla_strip_list', pg, 'nla_strip_list_index', rows=rows)
col = sequences_panel.column()
col.use_property_split = True
col.use_property_decorate = False
col.prop(pg, 'sequence_name_prefix')
col.prop(pg, 'sequence_name_suffix')
# Determine if there is going to be a naming conflict and display an error, if so.
selected_items = [x for x in pg.action_list if x.is_selected]
action_names = [x.name for x in selected_items]
action_name_counts = Counter(action_names)
for action_name, count in action_name_counts.items():
if count > 1:
text = bpy.app.translations.pgettext_iface('Duplicate action: {action_name}')
text = text.format(action_name=action_name)
sequences_panel.label(text, icon='ERROR')
break
# BONES
row = layout.row(align=True)
row.prop(pg, 'bone_filter_mode', text='Bones')
bones_header, bones_panel = layout.panel('bones_panel_id', default_closed=False)
bones_header.label(text='Bones', icon='BONE_DATA')
if pg.bone_filter_mode == 'BONE_COLLECTIONS':
row = layout.row(align=True)
row.label(text='Select')
row.operator(PSA_OT_export_bone_collections_select_all.bl_idname, text='All', icon='CHECKBOX_HLT')
row.operator(PSA_OT_export_bone_collections_deselect_all.bl_idname, text='None', icon='CHECKBOX_DEHLT')
rows = max(3, min(len(pg.bone_collection_list), 10))
layout.template_list('PSX_UL_bone_collection_list', '', pg, 'bone_collection_list', pg, 'bone_collection_list_index',
rows=rows)
if bones_panel is not None:
row = bones_panel.row(align=True)
row.prop(pg, 'bone_filter_mode', text='Bones')
layout.prop(pg, 'should_enforce_bone_name_restrictions')
if pg.bone_filter_mode == 'BONE_COLLECTIONS':
row = bones_panel.row(align=True)
row.label(text='Select')
row.operator(PSA_OT_export_bone_collections_select_all.bl_idname, text='All', icon='CHECKBOX_HLT')
row.operator(PSA_OT_export_bone_collections_deselect_all.bl_idname, text='None', icon='CHECKBOX_DEHLT')
rows = max(3, min(len(pg.bone_collection_list), 10))
bones_panel.template_list('PSX_UL_bone_collection_list', '', pg, 'bone_collection_list', pg, 'bone_collection_list_index',
rows=rows)
layout.separator()
bones_panel.prop(pg, 'should_enforce_bone_name_restrictions')
# ROOT MOTION
layout.prop(pg, 'root_motion', text='Root Motion')
advanced_header, advanced_panel = layout.panel('advanced_panel_id', default_closed=False)
advanced_header.label(text='Advanced')
if advanced_panel is not None:
advanced_panel.prop(pg, 'root_motion', text='Root Motion')
@classmethod
def _check_context(cls, context):
@@ -355,7 +372,9 @@ class PSA_OT_export(Operator, ExportHelper):
animation_data = animation_data_object.animation_data
if animation_data is None:
raise RuntimeError(f'No animation data for object \'{animation_data_object.name}\'')
message = bpy.app.translations.pgettext_iface('No animation data for object "{name}"')
message = message.format(name=animation_data_object.name)
raise RuntimeError(message)
export_sequences: List[PsaBuildSequence] = []
@@ -393,7 +412,8 @@ class PSA_OT_export(Operator, ExportHelper):
export_sequence.fps = get_sequence_fps(context, pg.fps_source, pg.fps_custom, [nla_strip_item.action])
export_sequences.append(export_sequence)
else:
raise ValueError(f'Unhandled sequence source: {pg.sequence_source}')
message = bpy.app.translations.pgettext_iface('Unhandled sequence source: {sequence_source}')
raise ValueError(message.format(sequence_source=pg.sequence_source))
options = PsaBuildOptions()
options.animation_data = animation_data

View File

@@ -141,9 +141,9 @@ class PSA_PG_export(PropertyGroup):
options=empty_set,
description='',
items=(
('ALL', 'All', 'All bones will be exported.'),
('ALL', 'All', 'All bones will be exported'),
('BONE_COLLECTIONS', 'Bone Collections', 'Only bones belonging to the selected bone collections and their '
'ancestors will be exported.'),
'ancestors will be exported'),
)
)
bone_collection_list: CollectionProperty(type=PSX_PG_bone_collection_list_item)
@@ -152,7 +152,7 @@ class PSA_PG_export(PropertyGroup):
default=False,
name='Enforce Bone Name Restrictions',
description='Bone names restrictions will be enforced. Note that bone names without properly formatted names '
'may not be able to be referenced in-engine'
'may not be able to be referenced by some versions of the Unreal Engine'
)
sequence_name_prefix: StringProperty(name='Prefix', options=empty_set)
sequence_name_suffix: StringProperty(name='Suffix', options=empty_set)
@@ -168,18 +168,18 @@ class PSA_PG_export(PropertyGroup):
description='Invert filtering (show hidden items, and vice versa)')
sequence_filter_asset: BoolProperty(
default=False,
name='Show assets',
name='Assets',
options=empty_set,
description='Show actions that belong to an asset library')
sequence_filter_pose_marker: BoolProperty(
default=True,
name='Show pose markers',
name='Pose Markers',
options=empty_set)
sequence_use_filter_sort_reverse: BoolProperty(default=True, options=empty_set)
sequence_filter_reversed: BoolProperty(
default=True,
options=empty_set,
name='Show Reversed',
name='Reversed',
description='Show reversed sequences'
)

View File

@@ -15,7 +15,7 @@ class PSA_UL_export_sequences(UIList):
def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index):
item = cast(PSA_PG_export_action_list_item, item)
is_pose_marker = hasattr(item, 'is_pose_marker') and item.is_pose_marker
layout.prop(item, 'is_selected', icon_only=True, text=item.name)
layout.prop(item, 'is_selected', icon_only=True, text=item.name, translate=False)
if hasattr(item, 'action') and item.action is not None and item.action.asset_data is not None:
layout.label(text='', icon='ASSET_MANAGER')
@@ -24,7 +24,7 @@ class PSA_UL_export_sequences(UIList):
if item.frame_end < item.frame_start:
row.label(text='', icon='FRAME_PREV')
if is_pose_marker:
row.label(text=item.action.name, icon='PMARKER')
row.label(text=item.action.name, icon='PMARKER', translate=False)
def draw_filter(self, context, layout):
pg = getattr(context.scene, 'psa_export')

View File

@@ -1,6 +1,7 @@
import os
from pathlib import Path
import bpy
from bpy.props import StringProperty
from bpy.types import Operator, Event, Context, FileHandler
from bpy_extras.io_utils import ImportHelper
@@ -28,7 +29,7 @@ class PSA_OT_import_sequences_from_text(Operator):
def draw(self, context):
layout = self.layout
pg = getattr(context.scene, 'psa_import')
layout.label(icon='INFO', text='Each sequence name should be on a new line.')
layout.label(icon='INFO', text='Each sequence name should be on a new line')
layout.prop(pg, 'select_text', text='')
def execute(self, context):
@@ -43,14 +44,16 @@ class PSA_OT_import_sequences_from_text(Operator):
if sequence.action_name == line:
sequence.is_selected = True
count += 1
self.report({'INFO'}, f'Selected {count} sequence(s)')
message = bpy.app.translations.pgettext('Selected {count} sequence(s)')
message = message.format(count=count)
self.report({'INFO'}, message)
return {'FINISHED'}
class PSA_OT_import_sequences_select_all(Operator):
bl_idname = 'psa_import.sequences_select_all'
bl_label = 'All'
bl_description = 'Select all sequences'
bl_description = 'Select all visible sequences'
bl_options = {'INTERNAL'}
@classmethod
@@ -165,17 +168,22 @@ class PSA_OT_import(Operator, ImportHelper):
try:
options.psa_config = read_psa_config(psa_reader, str(config_path))
except Exception as e:
self.report({'WARNING'}, f'Failed to read PSA config file: {e}')
message = bpy.app.translations.pgettext_iface('Failed to read PSA config file: {error}')
message = message.format(error=str(e))
self.report({'WARNING'}, message)
result = import_psa(context, psa_reader, context.view_layer.objects.active, options)
if len(result.warnings) > 0:
message = f'Imported {len(sequence_names)} action(s) with {len(result.warnings)} warning(s)\n'
message = bpy.app.translations.pgettext_iface('Imported {action_count} action(s) with {warning_count} warning(s)')
message = message.format(action_count=len(sequence_names), warning_count=len(result.warnings))
self.report({'WARNING'}, message)
for warning in result.warnings:
self.report({'WARNING'}, warning)
else:
self.report({'INFO'}, f'Imported {len(sequence_names)} action(s)')
message = bpy.app.translations.pgettext_iface('Imported {action_count} action(s)')
message = message.format(action_count=len(sequence_names))
self.report({'INFO'}, message)
return {'FINISHED'}
@@ -259,7 +267,7 @@ class PSA_OT_import(Operator, ImportHelper):
class PSA_FH_import(FileHandler):
bl_idname = 'PSA_FH_import'
bl_label = 'File handler for Unreal PSA import'
bl_label = ''
bl_import_operator = 'psa_import.import'
bl_file_extensions = '.psa'

View File

@@ -30,7 +30,7 @@ class PSA_PG_import(PropertyGroup):
sequence_list_index: IntProperty(name='', default=0)
should_use_fake_user: BoolProperty(default=True, name='Fake User',
description='Assign each imported action a fake user so that the data block is '
'saved even it has no users',
'always saved',
options=empty_set)
should_use_config_file: BoolProperty(default=True, name='Use Config File',
description='Use the .config file that is sometimes generated when the PSA '
@@ -48,8 +48,8 @@ class PSA_PG_import(PropertyGroup):
'will have it\'s data overwritten instead of a new action being created')
should_write_keyframes: BoolProperty(default=True, name='Keyframes', options=empty_set)
should_write_metadata: BoolProperty(default=True, name='Metadata', options=empty_set,
description='Additional data will be written to the custom properties of the '
'Action (e.g., frame rate)')
description='Additional data will be written to the properties of the Action '
'(e.g., frame rate)')
sequence_filter_name: StringProperty(default='', options={'TEXTEDIT_UPDATE'})
sequence_filter_is_selected: BoolProperty(default=False, options=empty_set, name='Only Show Selected',
description='Only show selected sequences')
@@ -64,13 +64,13 @@ class PSA_PG_import(PropertyGroup):
'Recommended if you do not plan on editing the actions directly'
)
bone_mapping_mode: EnumProperty(
name='Bone Mapping',
name='Bone Name Mapping',
options=empty_set,
description='The method by which bones from the incoming PSA file are mapped to the armature',
description='The method by which bones from the PSA file are mapped to the bones of the armature',
items=(
('EXACT', 'Exact', 'Bone names must match exactly.', 'EXACT', 0),
('CASE_INSENSITIVE', 'Case Insensitive', 'Bones names must match, ignoring case (e.g., the bone PSA bone '
'\'root\' can be mapped to the armature bone \'Root\')', 'CASE_INSENSITIVE', 1),
('EXACT', 'Exact', 'Bone names must match exactly', 'EXACT', 0),
('CASE_INSENSITIVE', 'Case Insensitive', 'Bones names must match, ignoring case (e.g., the PSA bone '
'\'aBcDeF\' can be mapped to the armature bone \'ABCDEF\')', 'CASE_INSENSITIVE', 1),
)
)
fps_source: EnumProperty(name='FPS Source', items=(

View File

@@ -10,8 +10,7 @@ class PSA_UL_sequences(UIList):
split = row.split(align=True, factor=0.75)
column = split.row(align=True)
column.alignment = 'LEFT'
column.prop(item, 'is_selected', icon_only=True)
column.label(text=getattr(item, 'action_name'))
column.prop(item, 'is_selected', text=getattr(item, 'action_name'), translate=False)
def draw_filter(self, context, layout):
pg = getattr(context.scene, 'psa_import')

View File

@@ -158,16 +158,16 @@ def import_psa(context: Context, psa_reader: PsaReader, armature_object: Object,
psa_bone_name = psa_bone_names[psa_bone_index]
armature_bone_name = armature_bone_names[armature_bone_index]
mapped_psa_bone_name = psa_bone_names[mapped_psa_bone_index]
result.warnings.append(f'PSA bone {psa_bone_index} ({psa_bone_name}) could not be mapped to armature bone {armature_bone_index} ({armature_bone_name}) because the armature bone is already mapped to PSA bone {mapped_psa_bone_index} ({mapped_psa_bone_name})')
message = bpy.app.translations.pgettext_iface('PSA bone {bone_index} ({bone_name}) could not be mapped to armature bone {armature_bone_index} ({armature_bone_name}) because the armature bone is already mapped to PSA bone {mapped_psa_bone_index} ({mapped_psa_bone_name})')
message = message.format(bone_index=psa_bone_index, bone_name=psa_bone_name, armature_bone_index=armature_bone_index, armature_bone_name=armature_bone_name, mapped_psa_bone_index=mapped_psa_bone_index, mapped_psa_bone_name=mapped_psa_bone_name)
result.warnings.append(message)
# Report if there are missing bones in the target armature.
missing_bone_names = set(psa_bone_names).difference(set(armature_bone_names))
if len(missing_bone_names) > 0:
result.warnings.append(
f'The armature \'{armature_object.name}\' is missing {len(missing_bone_names)} bones that exist in '
'the PSA:\n' +
str(list(sorted(missing_bone_names)))
)
message = bpy.app.translations.pgettext_iface('The armature \'{armature_name}\' is missing {count} bones that exist in the PSA:\n{missing_bone_names}')
message = message.format(armature_name=armature_object.name, count=len(missing_bone_names), missing_bone_names=str(list(sorted(missing_bone_names))))
result.warnings.append(message)
del armature_bone_names
# Create intermediate bone data for import operations.
@@ -232,7 +232,9 @@ def import_psa(context: Context, psa_reader: PsaReader, armature_object: Object,
case 'SEQUENCE':
target_fps = sequence.fps
case _:
raise ValueError(f'Unknown FPS source: {options.fps_source}')
message = bpy.app.translations.pgettext_iface('Invalid FPS source: {fps_source}')
message = message.format(fps_source=options.fps_source)
raise ValueError(message)
if options.should_write_keyframes:
# Remove existing f-curves.

View File

@@ -1,5 +1,7 @@
import ctypes
import warnings
import bpy.app.translations
import numpy as np
from .data import *
@@ -113,5 +115,7 @@ class PsaReader(object):
fp.seek(section.data_size * section.data_count, 1)
else:
fp.seek(section.data_size * section.data_count, 1)
print(f'Unrecognized section in PSA: "{section.name}"')
message = bpy.app.translations.pgettext_iface('Unhandled section "{section_name}" at position {position}')
message = message.format(section_name=section.name.decode(), position=fp.tell())
warnings.warn(message)
return psa

View File

@@ -12,7 +12,7 @@ from ..helpers import *
class PskInputObjects(object):
def __init__(self):
self.mesh_objects = []
self.mesh_objects = set()
self.armature_object: Optional[Object] = None
@@ -27,18 +27,29 @@ class PskBuildOptions(object):
def get_psk_input_objects(context) -> PskInputObjects:
input_objects = PskInputObjects()
for selected_object in context.view_layer.objects.selected:
if selected_object.type != 'MESH':
raise RuntimeError(f'Selected object "{selected_object.name}" is not a mesh')
input_objects.mesh_objects = context.view_layer.objects.selected
# Check if the active object is an armature.
if context.active_object is not None and context.active_object.type == 'ARMATURE':
# Collect all the mesh objects that have an armature modifier referencing the active armature.
input_objects.armature_object = context.active_object
for obj in filter(lambda x: x.type == 'MESH' and x.visible_get(), context.view_layer.objects):
for modifier in obj.modifiers:
if modifier.type == 'ARMATURE' and modifier.object == input_objects.armature_object:
input_objects.mesh_objects.add(obj)
for selected_object in context.view_layer.objects.selected:
if selected_object is input_objects.armature_object:
continue
if selected_object.type == 'MESH':
input_objects.mesh_objects.add(selected_object)
if len(input_objects.mesh_objects) == 0:
raise RuntimeError('At least one mesh must be selected')
for mesh_object in input_objects.mesh_objects:
if len(mesh_object.data.materials) == 0:
raise RuntimeError(f'Mesh "{mesh_object.name}" must have at least one material')
message = bpy.app.translations.pgettext_iface('Mesh "{object_name}" must have at least one material')
raise RuntimeError(message.format(object_name=mesh_object.name))
# Ensure that there are either no armature modifiers (static mesh)
# or that there is exactly one armature modifier object shared between
@@ -50,12 +61,15 @@ def get_psk_input_objects(context) -> PskInputObjects:
if len(modifiers) == 0:
continue
elif len(modifiers) > 1:
raise RuntimeError(f'Mesh "{mesh_object.name}" must have only one armature modifier')
message = bpy.app.translations.pgettext_iface('Mesh "{object_name}" must have only one armature modifier')
raise RuntimeError(message.format(object_name=mesh_object.name))
armature_modifier_objects.add(modifiers[0].object)
if len(armature_modifier_objects) > 1:
armature_modifier_names = [x.name for x in armature_modifier_objects]
raise RuntimeError(f'All selected meshes must have the same armature modifier, encountered {len(armature_modifier_names)} ({", ".join(armature_modifier_names)})')
message = bpy.app.translations.pgettext_iface('All selected meshes must have the same armature modifier, encountered {count} ({names})')
message = message.format(count=len(armature_modifier_objects), names=', '.join(armature_modifier_names))
raise RuntimeError(message)
elif len(armature_modifier_objects) == 1:
input_objects.armature_object = list(armature_modifier_objects)[0]
@@ -101,8 +115,8 @@ def build_psk(context, options: PskBuildOptions) -> PskBuildResult:
try:
psk_bone.name = bytes(bone.name, encoding='windows-1252')
except UnicodeEncodeError:
raise RuntimeError(
f'Bone name "{bone.name}" contains characters that cannot be encoded in the Windows-1252 codepage')
message = bpy.app.translations.pgettext_iface('Bone name "{name}" contains characters that cannot be encoded in the Windows-1252 codepage')
raise RuntimeError(message.format(name=bone.name))
psk_bone.flags = 0
psk_bone.children_count = 0
@@ -144,7 +158,8 @@ def build_psk(context, options: PskBuildOptions) -> PskBuildResult:
try:
psk_material.name = bytes(material.name, encoding='windows-1252')
except UnicodeEncodeError:
raise RuntimeError(f'Material name "{material.name}" contains characters that cannot be encoded in the Windows-1252 codepage')
message = bpy.app.translations.pgettext_iface('Material name "{name}" contains characters that cannot be encoded in the Windows-1252 codepage')
raise RuntimeError(message.format(name=material.name))
psk_material.texture_index = len(psk.materials)
psk_material.poly_flags = triangle_type_and_bit_flags_to_poly_flags(material.psk.mesh_triangle_type,
material.psk.mesh_triangle_bit_flags)

View File

@@ -1,3 +1,4 @@
import bpy
from bpy.props import StringProperty
from bpy.types import Operator
from bpy_extras.io_utils import ExportHelper
@@ -26,10 +27,11 @@ def populate_material_list(mesh_objects, material_list):
material = material_slot.material
# TODO: put this in the poll arg?
if material is None:
raise RuntimeError('Material slot cannot be empty (index ' + str(i) + ')')
message = bpy.app.translations.pgettext_iface('Material slot cannot be empty (index {index})')
raise RuntimeError(message.format(index=i))
if material not in materials:
materials.append(material)
for index, material in enumerate(materials):
m = material_list.add()
m.material = material
@@ -121,37 +123,40 @@ class PSK_OT_export(Operator, ExportHelper):
pg = getattr(context.scene, 'psk_export')
# MESH
box = layout.box()
box.label(text='Mesh', icon='MESH_DATA')
box.prop(pg, 'use_raw_mesh_data')
mesh_header, mesh_panel = layout.panel('mesh_panel_id', default_closed=False)
mesh_header.label(text='Mesh', icon='MESH_DATA')
if mesh_panel is not None:
mesh_panel.prop(pg, 'use_raw_mesh_data')
# BONES
box = layout.box()
box.label(text='Bones', icon='BONE_DATA')
bone_filter_mode_items = pg.bl_rna.properties['bone_filter_mode'].enum_items_static
row = box.row(align=True)
for item in bone_filter_mode_items:
identifier = item.identifier
item_layout = row.row(align=True)
item_layout.prop_enum(pg, 'bone_filter_mode', item.identifier)
item_layout.enabled = is_bone_filter_mode_item_available(context, identifier)
bones_header, bones_panel = layout.panel('bones_panel_id', default_closed=False)
bones_header.label(text='Bones', icon='BONE_DATA')
if bones_panel is not None:
bone_filter_mode_items = pg.bl_rna.properties['bone_filter_mode'].enum_items_static
row = bones_panel.row(align=True)
for item in bone_filter_mode_items:
identifier = item.identifier
item_layout = row.row(align=True)
item_layout.prop_enum(pg, 'bone_filter_mode', item.identifier)
item_layout.enabled = is_bone_filter_mode_item_available(context, identifier)
if pg.bone_filter_mode == 'BONE_COLLECTIONS':
row = box.row()
rows = max(3, min(len(pg.bone_collection_list), 10))
row.template_list('PSX_UL_bone_collection_list', '', pg, 'bone_collection_list', pg, 'bone_collection_list_index', rows=rows)
if pg.bone_filter_mode == 'BONE_COLLECTIONS':
row = bones_panel.row()
rows = max(3, min(len(pg.bone_collection_list), 10))
row.template_list('PSX_UL_bone_collection_list', '', pg, 'bone_collection_list', pg, 'bone_collection_list_index', rows=rows)
box.prop(pg, 'should_enforce_bone_name_restrictions')
bones_panel.prop(pg, 'should_enforce_bone_name_restrictions')
# MATERIALS
box = layout.box()
box.label(text='Materials', icon='MATERIAL')
row = box.row()
rows = max(3, min(len(pg.bone_collection_list), 10))
row.template_list('PSK_UL_materials', '', pg, 'material_list', pg, 'material_list_index', rows=rows)
col = row.column(align=True)
col.operator(PSK_OT_material_list_move_up.bl_idname, text='', icon='TRIA_UP')
col.operator(PSK_OT_material_list_move_down.bl_idname, text='', icon='TRIA_DOWN')
materials_header, materials_panel = layout.panel('materials_panel_id', default_closed=False)
materials_header.label(text='Materials', icon='MATERIAL')
if materials_panel is not None:
row = materials_panel.row()
rows = max(3, min(len(pg.bone_collection_list), 10))
row.template_list('PSK_UL_materials', '', pg, 'material_list', pg, 'material_list_index', rows=rows)
col = row.column(align=True)
col.operator(PSK_OT_material_list_move_up.bl_idname, text='', icon='TRIA_UP')
col.operator(PSK_OT_material_list_move_down.bl_idname, text='', icon='TRIA_DOWN')
def execute(self, context):
pg = context.scene.psk_export
@@ -168,7 +173,8 @@ class PSK_OT_export(Operator, ExportHelper):
self.report({'WARNING'}, warning)
write_psk(result.psk, self.filepath)
if len(result.warnings) > 0:
self.report({'WARNING'}, f'PSK export successful with {len(result.warnings)} warnings')
message = bpy.app.translations.pgettext_iface('PSK export successful with {warning_count} warnings')
self.report({'WARNING'}, message.format(warning_count=len(result.warnings)))
else:
self.report({'INFO'}, f'PSK export successful')
except RuntimeError as e:

View File

@@ -1,6 +1,7 @@
import os
import sys
import bpy.app.translations
from bpy.props import StringProperty, BoolProperty, EnumProperty, FloatProperty
from bpy.types import Operator, FileHandler, Context
from bpy_extras.io_utils import ImportHelper
@@ -13,7 +14,7 @@ empty_set = set()
class PSK_FH_import(FileHandler):
bl_idname = 'PSK_FH_import'
bl_label = 'File handler for Unreal PSK/PSKX import'
bl_label = ''
bl_import_operator = 'import_scene.psk'
bl_file_extensions = '.psk;.pskx'
@@ -61,24 +62,22 @@ class PSK_OT_import(Operator, ImportHelper):
default=True,
name='Import Extra UVs',
options=empty_set,
description='Import extra UV maps, if available'
description='Import extra UVs, if available'
)
should_import_mesh: BoolProperty(
default=True,
name='Import Mesh',
options=empty_set,
description='Import mesh'
name='Mesh',
options=empty_set
)
should_import_materials: BoolProperty(
default=True,
name='Import Materials',
name='Materials',
options=empty_set,
)
should_import_skeleton: BoolProperty(
default=True,
name='Import Skeleton',
options=empty_set,
description='Import skeleton'
name='Armature',
options=empty_set
)
bone_length: FloatProperty(
default=1.0,
@@ -87,8 +86,7 @@ class PSK_OT_import(Operator, ImportHelper):
soft_min=1.0,
name='Bone Length',
options=empty_set,
subtype='DISTANCE',
description='Length of the bones'
subtype='DISTANCE'
)
should_import_shape_keys: BoolProperty(
default=True,
@@ -125,11 +123,14 @@ class PSK_OT_import(Operator, ImportHelper):
result = import_psk(psk, context, options)
if len(result.warnings):
message = f'PSK imported with {len(result.warnings)} warning(s)\n'
message = bpy.app.translations.pgettext_iface('PSK imported with {count} warning(s)')
message = message.format(count=len(result.warnings))
message += '\n'.join(result.warnings)
self.report({'WARNING'}, message)
else:
self.report({'INFO'}, f'PSK imported ({options.name})')
message = bpy.app.translations.pgettext_iface('PSK imported ({name})')
message = message.format(name=options.name)
self.report({'INFO'}, message)
return {'FINISHED'}

View File

@@ -28,9 +28,9 @@ class PskImportOptions:
class ImportBone:
'''
"""
Intermediate bone type for the purpose of construction.
'''
"""
def __init__(self, index: int, psk_bone: Psk.Bone):
self.index: int = index
self.psk_bone: Psk.Bone = psk_bone
@@ -165,7 +165,8 @@ def import_psk(psk: Psk, context, options: PskImportOptions) -> PskImportResult:
# TODO: Handle invalid faces better.
if len(invalid_face_indices) > 0:
result.warnings.append(f'Discarded {len(invalid_face_indices)} invalid face(s).')
message = bpy.app.translations.pgettext_iface('Discarded {count} invalid face(s)')
result.warnings.append(message.format(count=len(invalid_face_indices)))
bm.to_mesh(mesh_data)

View File

@@ -2,19 +2,19 @@ from bpy.props import EnumProperty
from bpy.types import PropertyGroup
mesh_triangle_types_items = (
('NORMAL', 'Normal', 'Normal one-sided', 0),
('NORMAL_TWO_SIDED', 'Normal Two-Sided', 'Normal but two-sided', 1),
('TRANSLUCENT', 'Translucent', 'Translucent two-sided', 2),
('MASKED', 'Masked', 'Masked two-sided', 3),
('MODULATE', 'Modulate', 'Modulation blended two-sided', 4),
('PLACEHOLDER', 'Placeholder', 'Placeholder triangle for positioning weapon. Invisible', 8),
('NORMAL', 'Normal', '', 0),
('NORMAL_TWO_SIDED', 'Normal Two-Sided', '', 1),
('TRANSLUCENT', 'Translucent', '', 2),
('MASKED', 'Masked', '', 3),
('MODULATE', 'Modulate', '', 4),
('PLACEHOLDER', 'Placeholder', '', 8),
)
mesh_triangle_bit_flags_items = (
('UNLIT', 'Unlit', 'Full brightness, no lighting', 16),
('FLAT', 'Flat', 'Flat surface, don\'t do bMeshCurvy thing', 32),
('ENVIRONMENT', 'Environment', 'Environment mapped', 64),
('NO_SMOOTH', 'No Smooth', 'No bilinear filtering on this poly\'s texture', 128),
('UNLIT', 'Unlit', '', 16),
('FLAT', 'Flat', '', 32),
('ENVIRONMENT', 'Environment', '', 64),
('NO_SMOOTH', 'No Smooth', '', 128),
)
class PSX_PG_material(PropertyGroup):

View File

@@ -1,9 +1,10 @@
import ctypes
import os
import re
import warnings
from pathlib import Path
import bpy.app.translations
from .data import *
@@ -69,8 +70,9 @@ def read_psk(path: str) -> Psk:
_read_types(fp, Psk.MorphData, section, psk.morph_data)
else:
# Section is not handled, skip it.
fp.seek(section.data_size * section.data_count, os.SEEK_CUR)
warnings.warn(f'Unrecognized section "{section.name} at position {fp.tell():15}"')
message: str = bpy.app.translations.pgettext_iface('Unhandled section "{section_name}" at position {position}')
message.format(section_name=section.name, position=f'{fp.tell():15}')
warnings.warn(message)
'''
UEViewer exports a sidecar file (*.props.txt) with fully-qualified reference paths for each material
@@ -78,14 +80,14 @@ def read_psk(path: str) -> Psk:
'''
psk.material_references = _read_material_references(path)
'''
"""
Tools like UEViewer and CUE4Parse write the point index as a 32-bit integer, exploiting the fact that due to struct
alignment, there were 16-bits of padding following the original 16-bit point index in the wedge struct.
However, this breaks compatibility with PSK files that were created with older tools that treated the
point index as a 16-bit integer and might have junk data written to the padding bits.
To work around this, we check if each point is still addressable using a 16-bit index, and if it is, assume the
point index is a 16-bit integer and truncate the high bits.
'''
"""
if len(psk.points) <= 65536:
for wedge in psk.wedges:
wedge.point_index &= 0xFFFF

View File

@@ -1,6 +1,8 @@
from ctypes import Structure, sizeof
from typing import Type
import bpy.app.translations
from .data import Psk
from ..data import Section, Vector3
@@ -24,15 +26,20 @@ def _write_section(fp, name: bytes, data_type: Type[Structure] = None, data: lis
def write_psk(psk: Psk, path: str):
if len(psk.wedges) > MAX_WEDGE_COUNT:
raise RuntimeError(f'Number of wedges ({len(psk.wedges)}) exceeds limit of {MAX_WEDGE_COUNT}')
message = bpy.app.translations.pgettext_iface('Number of wedges ({wedge_count}) exceeds limit of {MAX_WEDGE_COUNT}')
raise RuntimeError(message.format(wedge_count=len(psk.wedges), MAX_WEDGE_COUNT=MAX_WEDGE_COUNT))
if len(psk.points) > MAX_POINT_COUNT:
raise RuntimeError(f'Numbers of vertices ({len(psk.points)}) exceeds limit of {MAX_POINT_COUNT}')
message = bpy.app.translations.pgettext_iface('Numbers of vertices ({point_count}) exceeds limit of {MAX_POINT_COUNT}')
raise RuntimeError(message.format(point_count=len(psk.points), MAX_POINT_COUNT=MAX_POINT_COUNT))
if len(psk.materials) > MAX_MATERIAL_COUNT:
raise RuntimeError(f'Number of materials ({len(psk.materials)}) exceeds limit of {MAX_MATERIAL_COUNT}')
message = bpy.app.translations.pgettext_iface('Number of materials ({material_count}) exceeds limit of {MAX_MATERIAL_COUNT}')
raise RuntimeError(message.format(material_count=len(psk.materials), MAX_MATERIAL_COUNT=MAX_MATERIAL_COUNT))
if len(psk.bones) > MAX_BONE_COUNT:
raise RuntimeError(f'Number of bones ({len(psk.bones)}) exceeds limit of {MAX_BONE_COUNT}')
message = bpy.app.translations.pgettext_iface('Number of bones ({bone_count}) exceeds limit of {MAX_BONE_COUNT}')
raise RuntimeError(message.format(bone_count=len(psk.bones), MAX_BONE_COUNT=MAX_BONE_COUNT))
elif len(psk.bones) == 0:
raise RuntimeError(f'At least one bone must be marked for export')
message = bpy.app.translations.pgettext_iface('At least one bone must be marked for export')
raise RuntimeError(message)
with open(path, 'wb') as fp:
_write_section(fp, b'ACTRHEAD')

View File

@@ -7,7 +7,7 @@ class PSX_UL_bone_collection_list(UIList):
def draw_item(self, context: Context, layout: UILayout, data: AnyType, item: AnyType, icon: int,
active_data: AnyType, active_property: str, index: int = 0, flt_flag: int = 0):
row = layout.row()
row.prop(item, 'is_selected', text=getattr(item, 'name'))
row.prop(item, 'is_selected', text=item.name, translate=False)
row.label(text=str(getattr(item, 'count')), icon='BONE_DATA')
@@ -19,8 +19,8 @@ class PSX_PG_bone_collection_list_item(PropertyGroup):
class PSX_PG_action_export(PropertyGroup):
compression_ratio: FloatProperty(name='Compression Ratio', default=1.0, min=0.0, max=1.0, subtype='FACTOR', description='The key sampling ratio of the exported sequence.\n\nA compression ratio of 1.0 will export all frames, while a compression ratio of 0.5 will export half of the frames')
key_quota: IntProperty(name='Key Quota', default=0, min=1, description='The minimum number of frames to be exported')
compression_ratio: FloatProperty(name='Compression Ratio', default=1.0, min=0.0, max=1.0, subtype='FACTOR', description='The keyframe sampling ratio of the exported sequence.\n\nA compression ratio of 1.0 will export all frames, while a compression ratio of 0.5 will export half of the frames')
key_quota: IntProperty(name='Keyframe Quota', default=0, min=1, description='The minimum number of keyframes to be exported')
fps: FloatProperty(name='FPS', default=30.0, min=0.0, description='The frame rate of the exported sequence')

1
localization/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
artifacts

132
localization/stringscan.py Normal file
View File

@@ -0,0 +1,132 @@
import ast
import glob
import os
import pathlib
import polib
# Walk the directory and open all .py files using glob
strings = dict()
root_path = pathlib.Path('../io_scene_psk_psa').resolve()
for file in glob.glob('../io_scene_psk_psa/**/*.py', recursive=True):
with open(os.path.join(file), 'r') as f:
if file.endswith('i18n.py'):
# TODO: Don't parse the i18n files.
continue
# Walk the entire tree and build a list of all string literals.
try:
a = ast.parse(f.read())
for node in ast.walk(a):
if isinstance(node, ast.Constant) and isinstance(node.value, str):
a = pathlib.Path(file).resolve()
filepath = a.relative_to(root_path)
if node.s not in strings:
strings[node.s] = filepath, node.lineno, node.col_offset
except UnicodeDecodeError as e:
print(f'Error reading file {file}: {e}')
string_keys = set(strings.keys())
# Remove all keys from the dictionary that are empty or contain only whitespace.
string_keys = set(filter(lambda x: x.strip(), string_keys))
# Remove all strings that have no alphabetic characters.
string_keys = set(filter(lambda x: any(c.isalpha() for c in x), string_keys))
# Remove any strings that have '@return: ' in them.
string_keys = set(filter(lambda x: '@return: ' not in x, string_keys))
# Remove any strings that are entirely lowercase and have no whitespace.
string_keys = set(filter(lambda x: not x.islower() or ' ' in x, string_keys))
# Remove any strings that are in SCREAMING_SNAKE_CASE.
string_keys = set(filter(lambda x: not x.isupper(), string_keys))
# Remove any strings that have underscores and no spaces.
string_keys = set(filter(lambda x: '_' not in x or ' ' in x, string_keys))
# Remove any string that starts with a newline.
string_keys = set(filter(lambda x: not x.startswith('\n'), string_keys))
# Remove any string that looks like a regular expression.
string_keys = set(filter(lambda x: not any(c in x for c in '^'), string_keys))
def write_multiline_string(f, string):
f.write(f'msgid ""\n')
for line in string.split('\n'):
f.write(f'"{line}"\n')
f.write('msgstr ""\n\n')
# TODO: big brain move would be to load the translated Blender strings and remove any that are already translated
# instead of manually removing them.
exclude_strings = {
'Import-Export',
'Linear',
'Masked',
'Normal',
'Placeholder',
'Flat',
'Environment',
'Advanced',
'Action',
'All',
'Assets',
'Armature',
'Materials'
'Bones',
'Custom',
'Data',
'Colin Basnett, Yurii Ti',
'Invert',
'Keyframes', # maybe?
'Mesh',
'None',
'Options',
'Overwrite',
'Scale',
'Scene',
'Select',
'RemoveTracks'
'Source',
'Move Up',
'Move Down',
'Unassigned',
'Prefix',
'Suffix',
'Timeline Markers',
'Pose Markers',
'Actions',
'sRGBA',
}
# Remove any strings that are in the exclude_strings set.
string_keys = set(filter(lambda x: x not in exclude_strings, string_keys))
# Make a new PO file and write the strings to it.
pofile = polib.POFile()
pofile.header = '''msgid ""
msgstr ""
"Language: en\\n"
"MIME-Version: 1.0\\n"
"Content-Type: text/plain; charset=UTF-8\\n"
"Content-Transfer-Encoding: 8bit\\n"
'''
# Sort the string keys into a list.
string_keys = list(string_keys)
string_keys.sort()
for string_key in string_keys:
file, line, col = strings[string_key]
entry = polib.POEntry(
msgid=string_key,
msgstr=string_key,
comment=f'{file}:{line}',
)
pofile.append(entry)
pofile.save('../extern/io_scene_psk_psa-translations/io_scene_psk_psa.en.po')
# Print the # of strings.
print(f'Found {len(string_keys)} strings.')

View File

@@ -0,0 +1,27 @@
import os.path
import pprint
import re
from glob import glob
import polib
langs = {}
for file_path in glob('../extern/io_scene_psk_psa-translations/io_scene_psk_psa.*.po'):
with open(file_path, 'r', encoding='utf-8') as f:
content = f.read()
po = polib.pofile(content)
# Get the language code from the file name.
lang_code = re.match(r'io_scene_psk_psa.(\w*)\.po', os.path.basename(file_path)).group(1)
if lang_code == 'en':
continue
langs[lang_code] = {('*', entry.msgid): entry.msgstr for entry in po if entry.msgid != ''}
with open('../io_scene_psk_psa/i18n.py', 'w', encoding='utf-8') as f:
s = pprint.pformat(langs)
f.write(f'langs = {s}')
print(f'Language_codes = {list(langs.keys())}')
print('Wrote i18n.py')