some of the Track class now remember the mod_config object for easier call. default_track_attributes have been added to set default value of the track attribute object to avoid unreadable safe eval of getattr

This commit is contained in:
Faraphel 2022-08-20 00:24:30 +02:00
parent 622f43c66e
commit fb8d20c08f
13 changed files with 146 additions and 105 deletions

View file

@ -1,17 +1,17 @@
{
"GAME_REGION": "str(extracted_game.original_game.wit_path.region)",
"TRACK_TEXT_SCORE": "get_color(name=['yellow', 'orange', 'dark red', 'azure'][getattr(track, 'warning', 0)]).color_text(f'\\\\x{65296+track.score:x} ') if hasattr(track, 'score') else ''",
"TRACK_TEXT_PREFIX": "f'{prefix} ' if (prefix := get_tag_template('prefix', '')) else ''",
"TRACK_TEXT_SUFFIX": "f' ({suffix})' if (suffix := get_tag_template('suffix', '')) else ''",
"TRACK_TEXT_NAME": "getattr(track, 'name', '')",
"TRACK_TEXT_AUTHORS": "'\\\\n'.join(author) if isinstance(author := getattr(track, 'author', '/'), list) else author",
"TRACK_TEXT_WARNING_IF_DISABLED": "get_color(name='red').color_text('/') if getattr(track, 'warning', 0) != 0 else ''",
"TRACK_TEXT_SCORE": "get_color(name=['yellow', 'orange', 'dark red', 'azure'][track.warning]).color_text(f'\\\\x{65296+track.score:x} ') if track.score else ''",
"TRACK_TEXT_PREFIX": "f'{prefix} ' if (prefix := track.get_tag_template('prefix', '')) else ''",
"TRACK_TEXT_SUFFIX": "f' ({suffix})' if (suffix := track.get_tag_template('suffix', '')) else ''",
"TRACK_TEXT_NAME": "track.name",
"TRACK_TEXT_AUTHORS": "'\\\\n'.join(track.author) if isinstance(track.author, list) else track.author",
"TRACK_TEXT_WARNING_IF_DISABLED": "get_color(name='red').color_text('/') if track.warning != 0 else ''",
"TRACK_TEXT_HIGHLIGHT_START": "get_color(name='azure').raw if eval(highlight_if if (highlight_if := mod_config.specific_settings['highlight_if'].value) is not None else 'False', env={'track': track, 'mod_config': mod_config}) is True else ''",
"TRACK_TEXT_HIGHLIGHT_END": "get_color(name='off').raw",
"SETTINGS_MODE": "mod_config.specific_settings['mode'].value",
"SETTINGS_BALANCING": "mod_config.specific_settings['balancing'].value",
"IF_NO_WARNING": "if getattr(track, 'warning', 0) == 0 else ''"
"IF_NO_WARNING": "if track.warning == 0 else ''"
}

View file

@ -18,7 +18,7 @@
}
},
"replace_random_new": {
"default": "'Retro' not in getattr(track, 'tags', []) and getattr(track, 'warning', 0) == 0"
"default": "'Retro' not in track.tags and track.warning == 0"
}
},
"specific_settings": {
@ -39,7 +39,7 @@
"fr": "Surligner si"
},
"type": "string",
"default": "'v' + getattr(track, 'since_version', '') == mod_config.version",
"default": "'v' + track.since_version == mod_config.version",
"preview": "track_selecting"
},
"balancing": {
@ -124,9 +124,18 @@
"GP"
],
"track_file_template": "{{ getattr(track, 'sha1', '_') }}",
"multiplayer_disable_if": "getattr(track, 'warning', 0) >= 1",
"track_file_template": "{{ track.sha1 }}",
"multiplayer_disable_if": "track.warning >= 1",
"default_track_attributes": {
"since_version": "",
"sha1": "_",
"name": "",
"author": "/",
"warning": 0,
"score": 0,
"tags": []
},
"tracks": [
{
"name":"Mario Kart Stadium",

View file

@ -52,9 +52,7 @@ class Window(AbstractPreviewWindow):
# insert all the tracks representation
for track in self.mod_config.get_all_arenas_tracks(ignore_filter=True):
try:
track_repr = track.repr_format(
self.mod_config, self.entry_template_input.get()
)
track_repr = track.repr_format(template=self.entry_template_input.get())
offset: int = 0 # the color tag is removed at every sub, so keep track of the offset
tags: list[tuple[int | None, str | None]] = [] # list of all the position of the tags, with the offset

View file

@ -80,7 +80,7 @@ class Window(AbstractPreviewWindow):
self.text_track_select.configure(state=tkinter.NORMAL)
self.text_track_select.delete(1.0, tkinter.END)
template_func = self.mod_config.safe_eval(self.entry_template_input.get(), args=["track"])
template_func = self.mod_config.safe_eval(template=self.entry_template_input.get(), args=["track"])
for track in self.mod_config.get_all_tracks(ignore_filter=True):
value: bool = template_func(track) is True

View file

@ -7,12 +7,13 @@ from PIL import Image
from source import threaded
from source.mkw import Tag
from source.mkw.Cup import Cup
from source.mkw.Track.Cup import Cup
from source.mkw.collection import MKWColor, Slot
from source.mkw.ModSettings import AbstractModSettings
from source.mkw.Track import CustomTrack, DefaultTrack, Arena
from source.progress import Progress
from source.safe_eval import safe_eval, multiple_safe_eval
from source.safe_eval.safe_eval import safe_eval
from source.safe_eval.multiple_safe_eval import multiple_safe_eval
from source.wt.szs import SZSPath
from source.translation import translate as _
@ -78,8 +79,9 @@ class ModConfig:
version: str = "v1.0.0"
variant: str = "01"
_tracks: list["Track | TrackGroup"] = field(default_factory=list)
_arenas: list["Arena"] = field(default_factory=list)
_tracks: list["Track | TrackGroup | dict"] = field(default_factory=list)
default_track_attributes: dict[str, any] = field(default_factory=dict)
_arenas: list["Arena | dict"] = field(default_factory=list)
track_file_template: "TemplateMultipleSafeEval" = "{{ getattr(track, 'sha1', '_') }}"
multiplayer_disable_if: "TemplateSafeEval" = "False"
@ -94,8 +96,8 @@ class ModConfig:
macros: dict[str, "TemplateSafeEval"] = field(default_factory=dict)
messages: dict[str, dict[str, "TemplateMultipleSafeEval"]] = field(default_factory=dict)
global_settings: dict[str, "AbstractModSettings"] = field(default_factory=dict)
specific_settings: dict[str, "AbstractModSettings"] = field(default_factory=dict)
global_settings: dict[str, "AbstractModSettings | dict"] = field(default_factory=dict)
specific_settings: dict[str, "AbstractModSettings | dict"] = field(default_factory=dict)
lpar_template: "TemplateMultipleSafeEval" = "normal.lpar"
@ -103,6 +105,15 @@ class ModConfig:
self.path = Path(self.path)
if self.nickname is None: self.nickname = self.name
self._tracks = [CustomTrack.from_dict(self, track) for track in self._tracks if isinstance(track, dict)]
self._arenas = [Arena.from_dict(self, arena) for arena in self._arenas if isinstance(arena, dict)]
self.global_settings = {name: AbstractModSettings.get(data) for name, data in merge_dict(
default_global_settings, self.global_settings, dict_keys=default_global_settings.keys()
# Avoid modder to add their own settings to globals one
).items()}
self.specific_settings = {name: AbstractModSettings.get(data) for name, data in self.specific_settings.items()}
def __hash__(self) -> int:
return hash(self.name)
@ -125,21 +136,11 @@ class ModConfig:
kwargs = config_dict | {
"path": path,
"_tracks": [CustomTrack.from_dict(track) for track in config_dict.pop("tracks", [])],
"_arenas": [Arena.from_dict(arena) for arena in config_dict.pop("arenas", [])],
"_tracks": config_dict.pop("tracks", []),
"_arenas": config_dict.pop("arenas", []),
"macros": macros,
"messages": messages,
"global_settings": {name: AbstractModSettings.get(data) for name, data in merge_dict(
default_global_settings,
config_dict.get("global_settings", {}),
dict_keys=default_global_settings.keys() # Avoid modder to add their own settings to globals one
).items()},
"specific_settings": {name: AbstractModSettings.get(data) for name, data in config_dict.get(
"specific_settings", {}
).items()},
}
return cls(**kwargs)
@ -233,7 +234,7 @@ class ModConfig:
# filter_template_func is the function checking if the track should be included. If no parameter is set,
# then always return True
filter_template_func: Callable = self.safe_eval(
filter_template if filter_template is not None else "True", args=["track"]
template=filter_template if filter_template is not None else "True", args=["track"]
)
# if a sorting function is set, use it. If no function is set, but sorting is not disabled, use settings.
@ -247,7 +248,7 @@ class ModConfig:
# wrap the iterator inside a sort function
iterator = sorted(iterator, key=sorting_template_func)
# Go though all the tracks and filter them if enabled
# Go through all the tracks and filter them if enabled
for track in iterator:
yield track
@ -267,13 +268,13 @@ class ModConfig:
if len(track_buffer) > 4:
current_tag_count += 1
yield Cup(tracks=track_buffer, cup_name=f"{current_tag_name}/{current_tag_count}")
yield Cup(mod_config=self, tracks=track_buffer, cup_name=f"{current_tag_name}/{current_tag_count}")
track_buffer = []
# if there is still tracks in the buffer, create a cup with them and fill with default>
if len(track_buffer) > 0:
track_buffer.extend([DefaultTrack()] * (4 - len(track_buffer)))
yield Cup(tracks=track_buffer, cup_name=f"{current_tag_name}/{current_tag_count + 1}")
track_buffer.extend([DefaultTrack(mod_config=self)] * (4 - len(track_buffer)))
yield Cup(mod_config=self, tracks=track_buffer, cup_name=f"{current_tag_name}/{current_tag_count + 1}")
def get_unordered_cups(self) -> Generator["Cup", None, None]:
"""
@ -289,13 +290,13 @@ class ModConfig:
track_buffer.append(track)
if len(track_buffer) > 4:
yield Cup(tracks=track_buffer)
yield Cup(mod_config=self, tracks=track_buffer)
track_buffer = []
# if there is still tracks in the buffer, create a cup with them and fill with default
if len(track_buffer) > 0:
track_buffer.extend([DefaultTrack()] * (4 - len(track_buffer)))
yield Cup(tracks=track_buffer)
track_buffer.extend([DefaultTrack(mod_config=self)] * (4 - len(track_buffer)))
yield Cup(mod_config=self, tracks=track_buffer)
def get_cups(self) -> Generator["Cup", None, None]:
"""
@ -328,11 +329,11 @@ class ModConfig:
for cup in self.get_cups():
# get all the cup ctfile, use "-" for the template since the track's name are not used here
ctfile += cup.get_ctfile(mod_config=self, template=template)
ctfile += cup.get_ctfile(template=template)
ctfile_override_property = "[SETUP-ARENA]\n"
for arena in self.get_arenas():
arena_definition, arena_override_property = arena.get_ctfile(mod_config=self, template=template)
arena_definition, arena_override_property = arena.get_ctfile(template=template)
ctfile += arena_definition
ctfile_override_property += arena_override_property
@ -433,7 +434,7 @@ class ModConfig:
for track in self.get_all_arenas_tracks():
track_file: Path = next(
track_directory.rglob(f"{track.repr_format(self, self.track_file_template)}.*")
track_directory.rglob(f"{track.repr_format(template=self.track_file_template)}.*")
)
@threaded

View file

@ -49,11 +49,11 @@ class FormatOriginalTrackLayer(AbstractLayer):
else: tag = "Wii"
patched_name = CustomTrack(
mod_config=patch.mod_config,
name=name,
tags=[tag]
).repr_format(
patch.mod_config,
self.template
template=self.template
)
return f" {id}\t= {patched_name}"

View file

@ -15,13 +15,17 @@ class TrackForbiddenCustomAttribute(Exception):
class AbstractTrack(ABC):
mod_config: "ModConfig"
music: Slot.Slot
special: Slot.Slot
tags: list[Tag]
weight: int
def __init__(self, music: Slot.Slot = "T11", special: Slot.Slot = "T11",
def __init__(self, mod_config: "ModConfig", music: Slot.Slot = "T11", special: Slot.Slot = "T11",
tags: list[Tag] = None, weight: int = 1, **kwargs):
self.mod_config = mod_config
self.music = Slot.find(music)
self.special = Slot.find(special)
self.tags = tags if tags is not None else []
@ -30,7 +34,8 @@ class AbstractTrack(ABC):
# others not mandatory attributes
for key, value in kwargs.items():
# if the attribute start with __, this is a magic attribute, and it should not be modified
if "__" in key or hasattr(self, key): raise TrackForbiddenCustomAttribute(key)
if "__" in key: raise TrackForbiddenCustomAttribute(key)
# TODO: check potential security issue with setattr and already implemented method and attribute
setattr(self, key, value)
def __repr__(self):
@ -45,34 +50,33 @@ class AbstractTrack(ABC):
yield self
@abstractmethod
def repr_format(self, mod_config: "ModConfig", template: "TemplateMultipleSafeEval") -> str:
def repr_format(self, template: "TemplateMultipleSafeEval") -> str:
"""
return the representation of the track from the format
:param template: template for the way the text will be represented
:param mod_config: configuration of the mod
:return: formatted representation of the track
"""
...
@property
@abstractmethod
def get_filename(self, mod_config: "ModConfig") -> str:
def filename(self) -> str:
"""
Return the filename of the track
:param mod_config: the mod_config object
:return: the filename of the track
"""
...
@property
@abstractmethod
def is_new(self, mod_config: "ModConfig") -> bool:
def is_new(self) -> bool:
"""
Return if the track should be considered as new for random selection
:param mod_config: mod configuration
:return: is the track new
"""
...
def get_ctfile(self, mod_config: "ModConfig", template: "TemplateMultipleSafeEval", hidden: bool = False) -> str:
def get_ctfile(self, template: "TemplateMultipleSafeEval", hidden: bool = False) -> str:
"""
return the ctfile of the track
:hidden: if the track is in a group
@ -80,11 +84,11 @@ class AbstractTrack(ABC):
:return: ctfile
"""
category: str = "H" if hidden else "T"
name: str = self.repr_format(mod_config=mod_config, template=template)
filename: str = self.get_filename(mod_config=mod_config)
name: str = self.repr_format(template=template)
filename: str = self.filename
flags: int = (
(0x04 if hidden else 0) |
(0x01 if self.is_new(mod_config) else 0)
(0x01 if self.is_new else 0)
)
return (

View file

@ -16,13 +16,20 @@ class ArenaForbiddenCustomAttribute(Exception):
class Arena(RealArenaTrack):
"""
Represent an arena object
"""
mod_config: "ModConfig"
slot: Slot.Slot
music: Slot.Slot
special: Slot.Slot
tags: list[Tag]
def __init__(self, slot: Slot.Slot, music: Slot.Slot = None, special: Slot.Slot = None,
def __init__(self, mod_config: "ModConfig", slot: Slot.Slot, music: Slot.Slot = None, special: Slot.Slot = None,
tags: list[Tag] = None, **kwargs):
self.mod_config = mod_config
self.slot = Slot.find(slot)
self.music = Slot.find(music if music is not None else slot)
self.special = Slot.find(special if special is not None else slot)
@ -31,26 +38,25 @@ class Arena(RealArenaTrack):
# others not mandatory attributes
for key, value in kwargs.items():
# if the attribute start with __, this is a magic attribute, and it should not be modified
if "__" in key or hasattr(self, key): raise ArenaForbiddenCustomAttribute(key)
if "__" in key: raise ArenaForbiddenCustomAttribute(key)
setattr(self, key, value)
def __repr__(self):
return f"<{self.__class__.__name__} name={getattr(self, 'name', '/')} tags={getattr(self, 'tags', '/')}>"
@classmethod
def from_dict(cls, arena_dict: dict[str, any]) -> "Arena":
return cls(**arena_dict)
def from_dict(cls, mod_config: "ModConfig", arena_dict: dict[str, any]) -> "Arena":
return cls(mod_config, **arena_dict)
def get_ctfile(self, mod_config: "ModConfig", template: "TemplateMultipleSafeEval") -> (str, str):
def get_ctfile(self, template: "TemplateMultipleSafeEval") -> (str, str):
"""
Return the ctfile for the arena and the redefinition of the slot property
:param mod_config: the mod_config object
:param template: the template of the track name
:return: the ctfile for the arena and the redefinition of the slot property
"""
name: str = self.repr_format(mod_config=mod_config, template=template)
filename: str = self.get_filename(mod_config=mod_config)
name: str = self.repr_format(template=template)
filename: str = self.filename
return (
(

View file

@ -12,10 +12,12 @@ class Cup:
class that represent a mario kart wii track cup
"""
__slots__ = ["_tracks", "cup_name", "cup_id"]
__slots__ = ["_tracks", "cup_name", "cup_id", "mod_config"]
_last_cup_id = 0
mod_config: "ModConfig"
def __init__(self, tracks: list["Track | TrackGroup"], cup_name: str | None = None):
def __init__(self, mod_config: "ModConfig", tracks: list["Track | TrackGroup"], cup_name: str | None = None):
self.mod_config = mod_config
self._tracks = tracks[:4]
self.cup_id = self.__class__._last_cup_id
@ -25,7 +27,7 @@ class Cup:
def __repr__(self):
return f"<Cup name={self.cup_name} id={self.cup_id} tracks={self._tracks}>"
def get_default_cticon(self, mod_config: "ModConfig") -> Image.Image:
def get_default_cticon(self) -> Image.Image:
"""
Get the default cticon for this cup
:return: the default cticon
@ -33,7 +35,7 @@ class Cup:
from source.mkw.ModConfig import CT_ICON_SIZE
ct_icon = Image.new("RGBA", (CT_ICON_SIZE, CT_ICON_SIZE))
default_font_path = str(mod_config.get_default_font().resolve())
default_font_path = str(self.mod_config.get_default_font().resolve())
draw = ImageDraw.Draw(ct_icon)
draw.text(
@ -63,15 +65,15 @@ class Cup:
path = mod_config.get_mod_directory() / f"_CUPS/{self.cup_name}.png"
if path.exists(): return Image.open(path)
# if the icon doesn't exist, use the default automatically generated one
return self.get_default_cticon(mod_config=mod_config)
return self.get_default_cticon()
def get_ctfile(self, mod_config: "ModConfig", template: "TemplateMultipleSafeEval") -> str:
def get_ctfile(self, template: "TemplateMultipleSafeEval") -> str:
"""
Get the ctfile for this cup
:return: the ctfile
"""
ctfile = f'C "{self.cup_name}"\n'
for track in self._tracks: ctfile += track.get_ctfile(mod_config, template)
for track in self._tracks: ctfile += track.get_ctfile(template=template)
ctfile += "\n"
return ctfile

View file

@ -15,19 +15,21 @@ class CustomTrack(RealArenaTrack, AbstractTrack):
return f"<{self.__class__.__name__} name={getattr(self, 'name', '/')} tags={getattr(self, 'tags', '/')}>"
@classmethod
def from_dict(cls, track_dict: dict) -> "Track | TrackGroup":
def from_dict(cls, mod_config: "ModConfig", track_dict: dict) -> "Track | TrackGroup":
"""
create a track from a dict, or create a track group is it is a group
:param mod_config: the mod configuration
:param track_dict: dict containing the track information
:return: Track
"""
if "group" in track_dict:
from source.mkw.Track.TrackGroup import TrackGroup
return TrackGroup.from_dict(track_dict)
return cls(**track_dict)
return TrackGroup.from_dict(mod_config, track_dict)
return cls(mod_config, **track_dict)
def is_new(self, mod_config: "ModConfig") -> bool:
return mod_config.safe_eval(
mod_config.global_settings["replace_random_new"].value,
@property
def is_new(self) -> bool:
return self.mod_config.safe_eval(
self.mod_config.global_settings["replace_random_new"].value,
args=["track"]
)(track=self) is True

View file

@ -7,11 +7,13 @@ if TYPE_CHECKING:
class DefaultTrack(AbstractTrack):
def repr_format(self, mod_config: "ModConfig", template: str) -> str:
def repr_format(self, template: str) -> str:
return " " # the name is always a blank space. Using nothing result in the filename being used instead
def get_filename(self, mod_config: "ModConfig") -> str:
@property
def filename(self) -> str:
return "beginner_course" # by default, use the T11 track, beginner_course
def is_new(self, mod_config: "ModConfig") -> bool:
@property
def is_new(self) -> bool:
return False # default track are never selected for random cup

View file

@ -12,32 +12,29 @@ class RealArenaTrack:
(For example, DefaultTrack is not considered a real track class)
"""
mod_config: "ModConfig"
tags: list["Tag"]
def get_tag_template(self, mod_config: "ModConfig", template_name: str, default: any = None) -> any:
def get_tag_template(self, template_name: str, default: any = None) -> any:
"""
Return the tag template found in templates. If not found, return default
:param mod_config: mod configuration
:param template_name: name of the template of the tags
:param default: default value if no tag template is found
:return: formatted representation of the tag
"""
for tag in filter(lambda tag: tag in mod_config.tags_templates[template_name], self.tags):
return mod_config.multiple_safe_eval(
mod_config.tags_templates[template_name][tag],
for tag in filter(lambda tag: tag in self.mod_config.tags_templates[template_name], self.tags):
return self.mod_config.multiple_safe_eval(
template=self.mod_config.tags_templates[template_name][tag],
args=["tag"],
)(tag=tag)
return default
def repr_format(self, mod_config: "ModConfig", template: "TemplateMultipleSafeEval") -> str:
return mod_config.multiple_safe_eval(
template,
args=["track", "get_tag_template"]
)(
track=self,
get_tag_template=lambda *args, **kwargs: self.get_tag_template(mod_config, *args, **kwargs)
# get_tag_template can't be in env because it is dependent of the track self
)
def repr_format(self, template: "TemplateMultipleSafeEval") -> str:
return self.mod_config.multiple_safe_eval(template=template, args=["track"])(track=self)
def get_filename(self, mod_config: "ModConfig") -> str:
return self.repr_format(mod_config=mod_config, template=mod_config.track_file_template)
@property
def filename(self) -> str:
return self.repr_format(template=self.mod_config.track_file_template)
def __getattr__(self, item):
return self.mod_config.default_track_attributes.get(item, None)

View file

@ -1,6 +1,7 @@
from typing import Generator, TYPE_CHECKING
from source.mkw import Tag
from source.translation import translate as _
if TYPE_CHECKING:
from source import TemplateMultipleSafeEval
@ -8,11 +9,29 @@ if TYPE_CHECKING:
from source.mkw.ModConfig import ModConfig
class TrackGroupForbiddenCustomAttribute(Exception):
def __init__(self, attribute_name: str):
super().__init__(_("FORBIDDEN_TRACKGROUP_ATTRIBUTE", " : ", repr(attribute_name)))
class TrackGroup:
def __init__(self, tracks: list["CustomTrack"] = None, tags: list[Tag] = None):
mod_config: "ModConfig"
tracks: list["CustomTrack"]
tags: list["Tag"]
def __init__(self, mod_config: "ModConfig", tracks: list["CustomTrack"] = None,
tags: list["Tag"] = None, **kwargs):
self.mod_config = mod_config
self.tracks = tracks if tracks is not None else []
self.tags = tags if tags is not None else []
# others not mandatory attributes
for key, value in kwargs.items():
# if the attribute start with __, this is a magic attribute, and it should not be modified
if "__" in key: raise TrackGroupForbiddenCustomAttribute(key)
setattr(self, key, value)
def get_tracks(self) -> Generator["CustomTrack", None, None]:
"""
Get all the track elements
@ -22,27 +41,28 @@ class TrackGroup:
yield from track.get_tracks()
@classmethod
def from_dict(cls, group_dict: dict) -> "TrackGroup | Track":
def from_dict(cls, mod_config: "ModConfig", group_dict: dict) -> "TrackGroup | Track":
"""
create a track group from a dict, or create a track from the dict if not a group
:param mod_config: the mod configuration
:param group_dict: dict containing the track information
:return: TrackGroup or Track
"""
from source.mkw.Track.CustomTrack import CustomTrack
if "group" not in group_dict: return CustomTrack.from_dict(group_dict)
if "group" not in group_dict: return CustomTrack.from_dict(mod_config, group_dict)
return cls(
tracks=[CustomTrack.from_dict(track) for track in group_dict["group"]],
tags=group_dict.get("tags"),
tracks=[CustomTrack.from_dict(mod_config, track) for track in group_dict["group"]],
**group_dict,
)
def get_ctfile(self, mod_config: "ModConfig", template: "TemplateMultipleSafeEval") -> str:
def get_ctfile(self, template: "TemplateMultipleSafeEval") -> str:
"""
return the ctfile of the track group
:return: ctfile
"""
ctfile = f'T T11; T11; 0x02; "-"; "info"; "-"\n'
for track in self.get_tracks():
ctfile += track.get_ctfile(template=template, mod_config=mod_config, hidden=True)
ctfile += track.get_ctfile(template=template, hidden=True)
return ctfile