feat: add ballance map sector field in scene

- add ballance map sector info in scene to indicate the maximum sector count of this map.
- this adding will prevent the bug that the exported ballance map do not have successive sector groups. because original implement will not create sector group if no component in corresponding sector and previous remedy still have bug. and if this happended, ballance will show spaceship in wrong sector. this adding is the final solution of this bug.
- exlarge ballance map sector info when user adding component. the enlarged value will be calculated by user input sector.
- auto enlarge ballance map sector info when importing. this will give user a fluent experience when modifying existing map.
- exporting map will also use ballance map sector info to pre-create successive sector group as term 2 stated.
- move sector name extractor from virtools file exporting module to naming convention module.
This commit is contained in:
yyc12345 2024-04-01 14:39:11 +08:00
parent 6cf2ab895d
commit 3396947115
7 changed files with 190 additions and 88 deletions

View File

@ -1,7 +1,7 @@
import bpy, mathutils
import math, typing
from . import UTIL_functions, UTIL_icons_manager, UTIL_naming_convension
from . import PROP_ballance_element, PROP_virtools_group
from . import PROP_ballance_element, PROP_virtools_group, PROP_ballance_map_info
#region Param Help Classes
@ -128,6 +128,7 @@ class _GeneralComponentCreator():
"""
# get element info first
ele_info: UTIL_naming_convension.BallanceObjectInfo = _get_component_info(comp_type, comp_sector)
# create blc element context
with PROP_ballance_element.BallanceElementsHelper(bpy.context.scene) as creator:
# object creation counter
@ -143,6 +144,27 @@ class _GeneralComponentCreator():
# put into created object list
self.__mObjList.append(obj)
# enlarge scene sector field for non-PS (start point) PE (end point) component
# read from scene and create var for enlarged sector count
map_info: PROP_ballance_map_info.RawBallanceMapInfo = PROP_ballance_map_info.get_raw_ballance_map_info(bpy.context.scene)
enlarged_sector: int
# check component type to get enlarged value
match(ele_info.mBasicType):
case UTIL_naming_convension.BallanceObjectType.COMPONENT:
enlarged_sector = comp_sector
case UTIL_naming_convension.BallanceObjectType.CHECKPOINT:
# checkpoint 1 means that there is sector 2, so we plus 1 for it.
enlarged_sector = comp_sector + 1
case UTIL_naming_convension.BallanceObjectType.RESETPOINT:
enlarged_sector = comp_sector
case _:
# this component is not a sector based component
# so we do not change it (use original value)
enlarged_sector = map_info.mSectorCount
# enlarge it
map_info.mSectorCount = max(map_info.mSectorCount, enlarged_sector)
PROP_ballance_map_info.set_raw_ballance_map_info(bpy.context.scene, map_info)
def finish_component(self) -> None:
"""
Finish up component creation.

View File

@ -1,9 +1,9 @@
import bpy
from bpy_extras.wm_utils.progress_report import ProgressReport
import tempfile, os, typing, re
import tempfile, os, typing
from . import PROP_preferences, UTIL_ioport_shared
from . import UTIL_virtools_types, UTIL_functions, UTIL_file_browser, UTIL_blender_mesh, UTIL_ballance_texture, UTIL_icons_manager
from . import PROP_virtools_group, PROP_virtools_material, PROP_virtools_mesh, PROP_virtools_texture
from . import UTIL_virtools_types, UTIL_functions, UTIL_file_browser, UTIL_blender_mesh, UTIL_ballance_texture, UTIL_icons_manager, UTIL_naming_convension
from . import PROP_virtools_group, PROP_virtools_material, PROP_virtools_mesh, PROP_virtools_texture, PROP_ballance_map_info
from .PyBMap import bmap_wrapper as bmap
# define global tex save opt blender enum prop helper
@ -84,6 +84,11 @@ class BBP_OT_export_virtools(bpy.types.Operator, UTIL_file_browser.ExportVirtool
if self.use_compress:
box.prop(self, 'compress_level')
# show sector info to notice user
layout.separator()
map_info: PROP_ballance_map_info.RawBallanceMapInfo = PROP_ballance_map_info.get_raw_ballance_map_info(bpy.context.scene)
layout.label(text = f'Map Sectors: {map_info.mSectorCount}')
_TObj3dPair = tuple[bpy.types.Object, bmap.BM3dObject]
_TMeshPair = tuple[bpy.types.Object, bpy.types.Mesh, bmap.BMMesh]
_TMaterialPair = tuple[bpy.types.Material, bmap.BMMaterial]
@ -170,8 +175,22 @@ def _export_virtools_groups(
# start saving
progress.enter_substeps(len(obj3d_crets), "Saving Groups")
# create group exporting helper
group_cret_guard: VirtoolsGroupCreationGuard = VirtoolsGroupCreationGuard(writer)
# create sector group first
# This step is designed for ensure that the created sector group is successive.
#
# Due to the design of Ballance, Ballance rely on checking the existance of Sector_XX to get how many sectors this map have.
# Thus if there are no component in a sector, it still need to create a empty Sector_XX group, otherwise the game will crash
# or be ended at a accident sector.
#
# So we create all needed sector group in here to make sure exported virtools file can be read by Ballancde correctly.
map_info: PROP_ballance_map_info.RawBallanceMapInfo = PROP_ballance_map_info.get_raw_ballance_map_info(bpy.context.scene)
for i in range(map_info.mSectorCount):
gp_name: str = UTIL_naming_convension.build_name_from_sector_index(i + 1)
vtgroup: bmap.BMGroup | None = group_cret_map.get(gp_name, None)
if vtgroup is None:
vtgroup = writer.create_group()
vtgroup.set_name(gp_name)
group_cret_map[gp_name] = vtgroup
for obj3d, vtobj3d in obj3d_crets:
# open group visitor
@ -180,8 +199,8 @@ def _export_virtools_groups(
# get group or create new group from guard
vtgroup: bmap.BMGroup | None = group_cret_map.get(gp_name, None)
if vtgroup is None:
# note: no need to set name, guard has set it.
vtgroup = group_cret_guard.create_group(gp_name)
vtgroup = writer.create_group()
vtgroup.set_name(gp_name)
group_cret_map[gp_name] = vtgroup
# group this object
@ -471,73 +490,6 @@ def _save_virtools_document(
progress.step()
progress.leave_substeps()
class VirtoolsGroupCreationGuard():
"""
This class is designed for ensure that the created sector group is successive.
Due to the design of Ballance, Ballance rely on checking the existance of Sector_XX to get how many sectors this map have.
Thus if there are no component in a sector, it still need to create a empty Sector_XX group, otherwise the game will crash
or be ended at a accident sector.
This class hook the operation of Virtools group creation and check all Sector group creation.
Create essential group to make Sector_XX group successive.
Thus all group creation in this module should be passed by this class.
"""
cRegexGroupSector: typing.ClassVar[re.Pattern] = re.compile('^Sector_(0[1-8]|[1-9][0-9]{1,2}|9)$')
@staticmethod
def __get_group_index(group_name: str) -> int | None:
"""
Return the sector index of group name if it is. Otherwise None.
"""
regex_result = VirtoolsGroupCreationGuard.cRegexGroupSector.match(group_name)
if regex_result is not None:
return int(regex_result.group(1))
else:
return None
@staticmethod
def __get_group_name(group_index: int) -> str:
"""
Output Sector group name by given sector index
"""
if group_index == 9:
return 'Sector_9'
else:
return f'Sector_{group_index:0>2d}'
__mWriter: bmap.BMFileWriter
__mSectors: list[bmap.BMGroup]
def __init__(self, assoc_writer: bmap.BMFileWriter):
self.__mWriter = assoc_writer
self.__mSectors = []
def create_group(self, group_name: str) -> bmap.BMGroup:
"""
The hooked group creation function.
Please note the passed group name argument is just for name checking.
This function will set group name for you, not like BMFileWriter.create_group() operated.
"""
# check whether it is sector group
# note: the return sector index is 1 based, not 0
sector_idx: int | None = VirtoolsGroupCreationGuard.__get_group_index(group_name)
# if it is regular group, return normal creation
if sector_idx is None:
gp: bmap.BMGroup = self.__mWriter.create_group()
gp.set_name(group_name)
return gp
# get from sector cahce list
# enlarge sector cache list if it is not fulfilled given sector index
while sector_idx > len(self.__mSectors):
gp: bmap.BMGroup = self.__mWriter.create_group()
self.__mSectors.append(gp)
gp.set_name(self.__get_group_name(len(self.__mSectors)))
# return ordered sector from sector caches
return self.__mSectors[sector_idx - 1]
def register() -> None:
bpy.utils.register_class(BBP_OT_export_virtools)

View File

@ -2,8 +2,8 @@ import bpy
from bpy_extras.wm_utils.progress_report import ProgressReport
import tempfile, os, typing
from . import PROP_preferences, UTIL_ioport_shared
from . import UTIL_virtools_types, UTIL_functions, UTIL_file_browser, UTIL_blender_mesh, UTIL_ballance_texture
from . import PROP_virtools_group, PROP_virtools_material, PROP_virtools_mesh, PROP_virtools_texture
from . import UTIL_virtools_types, UTIL_functions, UTIL_file_browser, UTIL_blender_mesh, UTIL_ballance_texture, UTIL_naming_convension
from . import PROP_virtools_group, PROP_virtools_material, PROP_virtools_mesh, PROP_virtools_texture, PROP_ballance_map_info
from .PyBMap import bmap_wrapper as bmap
class BBP_OT_import_virtools(bpy.types.Operator, UTIL_file_browser.ImportVirtoolsFile, UTIL_ioport_shared.ImportParams, UTIL_ioport_shared.VirtoolsParams):
@ -344,10 +344,12 @@ def _import_virtools_groups(
reader: bmap.BMFileReader,
progress: ProgressReport,
obj3d_cret_map: dict[bmap.BM3dObject, bpy.types.Object]
) -> dict[bmap.BM3dObject, bpy.types.Object]:
) -> None:
# we need iterate all groups to construct a reversed map
# to indicate which groups should this 3dobject be grouped into.
reverse_map: dict[bmap.BM3dObject, set[str]] = {}
# sector counter to record the maximum sector we have processed.
sector_count: int = 1
# prepare progress
progress.enter_substeps(reader.get_material_count(), "Loading Groups")
@ -357,6 +359,12 @@ def _import_virtools_groups(
group_name: str | None = vtgroup.get_name()
if group_name is None: continue
# try extracting sector info
potential_sector_count: int | None = UTIL_naming_convension.extract_sector_from_name(group_name)
if potential_sector_count is not None:
sector_count = max(sector_count, potential_sector_count)
# creating map
for item in vtgroup.get_objects():
# get or create set
objgroups: set[str] = reverse_map.get(item, None)
@ -370,6 +378,11 @@ def _import_virtools_groups(
# step
progress.step()
# assign to ballance map info according to gotten sector count
map_info: PROP_ballance_map_info.RawBallanceMapInfo = PROP_ballance_map_info.get_raw_ballance_map_info(bpy.context.scene)
map_info.mSectorCount = max(map_info.mSectorCount, sector_count)
PROP_ballance_map_info.set_raw_ballance_map_info(bpy.context.scene, map_info)
# leave progress
progress.leave_substeps()

View File

@ -0,0 +1,81 @@
import bpy
import typing
from . import UTIL_functions
class RawBallanceMapInfo():
cSectorCount: typing.ClassVar[int] = 1
mSectorCount: int
def __init__(self, **kwargs):
self.mSectorCount = kwargs.get("mSectorCount", RawBallanceMapInfo.cSectorCount)
def regulate(self):
self.mSectorCount = UTIL_functions.clamp_int(self.mSectorCount, 1, 999)
#region Prop Decl & Getter Setter
class BBP_PG_ballance_map_info(bpy.types.PropertyGroup):
sector_count: bpy.props.IntProperty(
name = "Sector",
description = "The sector count of this Ballance map which is used in exporting map and may be changed when importing map.",
default = 1,
max = 999, min = 1,
soft_max = 8, soft_min = 1,
step = 1
) # type: ignore
def get_ballance_map_info(scene: bpy.types.Scene) -> BBP_PG_ballance_map_info:
return scene.ballance_map_info
def get_raw_ballance_map_info(scene: bpy.types.Scene) -> RawBallanceMapInfo:
props: BBP_PG_ballance_map_info = get_ballance_map_info(scene)
rawdata: RawBallanceMapInfo = RawBallanceMapInfo()
rawdata.mSectorCount = props.sector_count
rawdata.regulate()
return rawdata
def set_raw_ballance_map_info(scene: bpy.types.Scene, rawdata: RawBallanceMapInfo) -> None:
props: BBP_PG_ballance_map_info = get_ballance_map_info(scene)
props.sector_count = rawdata.mSectorCount
#endregion
class BBP_PT_ballance_map_info(bpy.types.Panel):
"""Show Ballance Map Infos."""
bl_label = "Ballance Map"
bl_idname = "BBP_PT_ballance_map_info"
bl_space_type = 'PROPERTIES'
bl_region_type = 'WINDOW'
bl_context = "scene"
@classmethod
def poll(cls, context):
return context.scene is not None
def draw(self, context):
layout: bpy.types.UILayout = self.layout
target: bpy.types.Scene = context.scene
props: BBP_PG_ballance_map_info = get_ballance_map_info(target)
# show map sector count numberbox
layout.prop(props, 'sector_count')
def register() -> None:
# register
bpy.utils.register_class(BBP_PG_ballance_map_info)
bpy.utils.register_class(BBP_PT_ballance_map_info)
# add into scene metadata
bpy.types.Scene.ballance_map_info = bpy.props.PointerProperty(type = BBP_PG_ballance_map_info)
def unregister() -> None:
# del from scene metadata
del bpy.types.Scene.ballance_map_info
# unregister
bpy.utils.unregister_class(BBP_PG_ballance_map_info)
bpy.utils.unregister_class(BBP_PT_ballance_map_info)

View File

@ -8,7 +8,7 @@ class BBP_PG_virtools_group(bpy.types.PropertyGroup):
group_name: bpy.props.StringProperty(
name = "Group Name",
default = ""
)
) # type: ignore
def get_virtools_groups(obj: bpy.types.Object) -> bpy.types.CollectionProperty:
return obj.virtools_groups

View File

@ -184,6 +184,42 @@ class BallanceObjectInfo():
#endregion
#region Sector Extractor
_g_RegexBlcSectorGroup: re.Pattern = re.compile('^Sector_(0[1-8]|[1-9][0-9]{1,2}|9)$')
def extract_sector_from_name(group_name: str) -> int | None:
"""
A convenient function to extract sector index from given group name.
This function also supports 999 sector plugin.
Not only in this module, but also in outside modules, this function is vary used to extract sector index info.
Function return the index extracted, or None if given group name is not a valid sector group.
The valid sector index is range from 1 to 999 (inclusive)
"""
regex_result = _g_RegexBlcSectorGroup.match(group_name)
if regex_result is not None:
return int(regex_result.group(1))
else:
return None
def build_name_from_sector_index(sector_index: int) -> str:
"""
A convenient function to build Ballance recognizable sector group name.
This function also supports 999 sector plugin.
This function also is used in this module or other modules outside.
Function return a sector name string. It basically the reverse operation of `extract_sector_from_name`.
"""
if sector_index == 9:
return 'Sector_9'
else:
return f'Sector_{sector_index:0>2d}'
#endregion
#region Naming Convention Declaration
_g_BlcNormalComponents: set[str] = set((
@ -227,7 +263,6 @@ _g_BlcWood: set[str] = set((
))
class VirtoolsGroupConvention():
cRegexGroupSector: typing.ClassVar[re.Pattern] = re.compile('^Sector_(0[1-8]|[1-9][0-9]{1,2}|9)$')
cRegexComponent: typing.ClassVar[re.Pattern] = re.compile('^(' + '|'.join(_g_BlcNormalComponents) + ')_(0[1-9]|[1-9][0-9])_.*$')
cRegexPC: typing.ClassVar[re.Pattern] = re.compile('^PC_TwoFlames_(0[1-7])$')
cRegexPR: typing.ClassVar[re.Pattern] = re.compile('^PR_Resetpoint_(0[1-8])$')
@ -255,9 +290,9 @@ class VirtoolsGroupConvention():
counter: int = 0
last_matched_sector: int = 0
for i in gps:
regex_result = VirtoolsGroupConvention.cRegexGroupSector.match(i)
regex_result: int | None = extract_sector_from_name(i)
if regex_result is not None:
last_matched_sector = int(regex_result.group(1))
last_matched_sector = regex_result
counter += 1
if counter != 1: return None
@ -377,12 +412,8 @@ class VirtoolsGroupConvention():
# group into component type
# use typing.cast() to force linter accept it because None is impossible
gp.add_group(typing.cast(str, info.mComponentType))
# group to sector
if info.mSector == 9:
gp.add_group('Sector_9')
else:
gp.add_group(f'Sector_{info.mSector:0>2d}')
gp.add_group(build_name_from_sector_index(typing.cast(int, info.mSector)))
case _:
if reporter is not None:

View File

@ -29,7 +29,8 @@ from . import UTIL_icons_manager
UTIL_icons_manager.register()
# then load other modules
from . import PROP_preferences, PROP_ptrprop_resolver, PROP_virtools_material, PROP_virtools_texture, PROP_virtools_mesh, PROP_virtools_group, PROP_ballance_element, PROP_bme_material
from . import PROP_preferences, PROP_ptrprop_resolver, PROP_virtools_material, PROP_virtools_texture, PROP_virtools_mesh, PROP_virtools_group
from . import PROP_ballance_element, PROP_bme_material, PROP_ballance_map_info
from . import OP_IMPORT_bmfile, OP_EXPORT_bmfile, OP_IMPORT_virtools, OP_EXPORT_virtools
from . import OP_UV_flatten_uv, OP_UV_rail_uv
from . import OP_MTL_fix_material
@ -217,6 +218,7 @@ def register() -> None:
PROP_virtools_group.register()
PROP_ballance_element.register()
PROP_bme_material.register()
PROP_ballance_map_info.register()
OP_IMPORT_bmfile.register()
OP_EXPORT_bmfile.register()
@ -275,6 +277,7 @@ def unregister() -> None:
OP_EXPORT_bmfile.unregister()
OP_IMPORT_bmfile.unregister()
PROP_ballance_map_info.unregister()
PROP_bme_material.unregister()
PROP_ballance_element.unregister()
PROP_virtools_group.unregister()