2093 lines
84 KiB
Python
2093 lines
84 KiB
Python
|
|
# LIVE
|
||
|
|
|
||
|
|
import re
|
||
|
|
import os
|
||
|
|
from typing import Generator, Callable, Self
|
||
|
|
from numbers import Number
|
||
|
|
from abc import ABC, abstractmethod
|
||
|
|
from collections.abc import Iterator, Iterable
|
||
|
|
from collections import UserList, UserDict
|
||
|
|
from copy import copy, deepcopy
|
||
|
|
from time import sleep
|
||
|
|
|
||
|
|
globalSettings = {
|
||
|
|
'parser_commands': [],
|
||
|
|
'context_mod': None,
|
||
|
|
'replace_local_variables': True,
|
||
|
|
'expand_inlines': False,
|
||
|
|
'debug_print': False,
|
||
|
|
'debug_sleep': 0,
|
||
|
|
'max_from_depth': 8,
|
||
|
|
'effectLocations': {},
|
||
|
|
'effectNesting': {},
|
||
|
|
'scopeTransitions': {},
|
||
|
|
'eventTypes': {},
|
||
|
|
'hardcodedOnActions': {},
|
||
|
|
'effectParseAdditionalSetup': lambda mod: None,
|
||
|
|
'additionalEffectBlocks': lambda mod: [],
|
||
|
|
'mod_docs_path': "C:\\Users\\kuyan\\OneDrive\\Documents\\Paradox Interactive\\Stellaris\\mod",
|
||
|
|
'workshop_path': "C:\\Program Files (x86)\\Steam\\steamapps\\workshop\\content\\281990",
|
||
|
|
'vanilla_path': "C:\\Program Files (x86)\\Steam\\steamapps\\common\\Stellaris",
|
||
|
|
'mod_order': [],
|
||
|
|
}
|
||
|
|
globalData = {
|
||
|
|
'eventTargets': {},
|
||
|
|
'factionParameters': {},
|
||
|
|
'onActionScopes': {},
|
||
|
|
'eventScopes': {},
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
def defaultToGlobal(value, key):
|
||
|
|
if value is None:
|
||
|
|
return globalSettings[key]
|
||
|
|
else:
|
||
|
|
return value
|
||
|
|
|
||
|
|
|
||
|
|
def configure(key, value):
|
||
|
|
'''configures global settings
|
||
|
|
available settings:
|
||
|
|
parser_commands (None): default parser commands
|
||
|
|
context_mod (None): mod from which to read inline scripts and effects
|
||
|
|
replace_local_variables (False): whether to replace local variables by default when parsing strings
|
||
|
|
expand_inlines (False): whether to expand inline scripts by default when searching CWLists
|
||
|
|
max_from_depth (8): attempting to access "from" scopes deeper than this will cause an error
|
||
|
|
mod_docs_path: folder in which to look for your own mods
|
||
|
|
workshop_path ("C:\\Program Files (x86)\\Steam\\steamapps\\workshop\\content\\281990"): folder in which to look for downloaded mods
|
||
|
|
effectLocations ({}): dictionary used for effect scoping- see cwp_stellaris for an example
|
||
|
|
effectNesting ({}): dictionary used for effect scoping- see cwp_stellaris for an example
|
||
|
|
scopeTransitions ({}): dictionary used for effect scoping- see cwp_stellaris for an example
|
||
|
|
eventTypes ({}): dictionary used for effect scoping- see cwp_stellaris for an example
|
||
|
|
hardcodedOnActions ({}): dictionary used for effect scoping- see cwp_stellaris for an example
|
||
|
|
effectParseAdditionalSetup (lambda mod: None): used for effect scoping- see cwp_stellaris for an example
|
||
|
|
additionalEffectBlocks (lambda mod: None): used for effect scoping- see cwp_stellaris for an example
|
||
|
|
'''
|
||
|
|
global globalSettings
|
||
|
|
globalSettings[key] = value
|
||
|
|
|
||
|
|
|
||
|
|
def printsleep(s, time=0):
|
||
|
|
if globalSettings['debug_print']:
|
||
|
|
print(s)
|
||
|
|
sleep(time)
|
||
|
|
|
||
|
|
|
||
|
|
def mod_doc_path(name):
|
||
|
|
return os.path.join(globalSettings['mod_docs_path'], name)
|
||
|
|
|
||
|
|
|
||
|
|
class ShallowFromsException(Exception):
|
||
|
|
pass
|
||
|
|
|
||
|
|
|
||
|
|
class UndefinedCollision(Exception):
|
||
|
|
pass
|
||
|
|
|
||
|
|
|
||
|
|
class MissingInlineScript(Exception):
|
||
|
|
pass
|
||
|
|
|
||
|
|
|
||
|
|
# placeholder class definitions
|
||
|
|
class Mod():
|
||
|
|
pass
|
||
|
|
|
||
|
|
|
||
|
|
class CWOverwritable():
|
||
|
|
pass
|
||
|
|
|
||
|
|
|
||
|
|
class CWElement(CWOverwritable):
|
||
|
|
pass
|
||
|
|
|
||
|
|
|
||
|
|
class CWList(UserList):
|
||
|
|
pass
|
||
|
|
|
||
|
|
|
||
|
|
class CWListValue(CWList):
|
||
|
|
pass
|
||
|
|
|
||
|
|
|
||
|
|
class ColorElement(CWListValue):
|
||
|
|
pass
|
||
|
|
|
||
|
|
|
||
|
|
class inlineMathsStep():
|
||
|
|
pass
|
||
|
|
|
||
|
|
|
||
|
|
class inlineMathsBlock():
|
||
|
|
pass
|
||
|
|
|
||
|
|
|
||
|
|
class inlineMathsUnit():
|
||
|
|
pass
|
||
|
|
|
||
|
|
|
||
|
|
class absValBlock():
|
||
|
|
pass
|
||
|
|
|
||
|
|
|
||
|
|
class metascript():
|
||
|
|
pass
|
||
|
|
|
||
|
|
|
||
|
|
class metascriptSubstitution():
|
||
|
|
pass
|
||
|
|
|
||
|
|
|
||
|
|
class metascriptConditional():
|
||
|
|
pass
|
||
|
|
|
||
|
|
|
||
|
|
# class metascriptInlineMathsBlock():
|
||
|
|
# pass
|
||
|
|
class metascriptList(UserList):
|
||
|
|
pass
|
||
|
|
|
||
|
|
|
||
|
|
class scopeUnpackable(ABC):
|
||
|
|
pass
|
||
|
|
|
||
|
|
|
||
|
|
class scopeSet(scopeUnpackable):
|
||
|
|
pass
|
||
|
|
|
||
|
|
|
||
|
|
class scopesContext():
|
||
|
|
pass
|
||
|
|
|
||
|
|
|
||
|
|
def escapeString(s: str) -> str:
|
||
|
|
return s.replace('\\', '\\\\').replace('"', '\\"')
|
||
|
|
|
||
|
|
|
||
|
|
def quote(s: str) -> str:
|
||
|
|
'''puts quote marks around a string'''
|
||
|
|
return f'"{escapeString(s)}"'
|
||
|
|
|
||
|
|
|
||
|
|
def numerify(s: str) -> int | float | str:
|
||
|
|
'''tries to convert a string to an integer or float'''
|
||
|
|
try:
|
||
|
|
sn = s.replace(' ', '').replace('--', '')
|
||
|
|
return int(s)
|
||
|
|
except(ValueError):
|
||
|
|
try:
|
||
|
|
return float(s)
|
||
|
|
except(ValueError):
|
||
|
|
return s
|
||
|
|
|
||
|
|
|
||
|
|
def indent(s: str, count: int = 1, initial_linebreak=False) -> str:
|
||
|
|
'''indents a multi-line string using tabs'''
|
||
|
|
tabs = '\n'
|
||
|
|
for i in range(count):
|
||
|
|
tabs += '\t'
|
||
|
|
if initial_linebreak:
|
||
|
|
s = '\n' + s
|
||
|
|
return s.replace('\n', tabs)
|
||
|
|
|
||
|
|
|
||
|
|
def valueString(v) -> str:
|
||
|
|
'''converts an object back to the corresponding string for use in stellaris script
|
||
|
|
for objects from this module str() is equivalent, but this is useful if you might also receive a string'''
|
||
|
|
if isinstance(v, str):
|
||
|
|
if ' ' in v or '\n' in v or '\t' in v or v == '':
|
||
|
|
return quote(v)
|
||
|
|
else:
|
||
|
|
return (v)
|
||
|
|
else:
|
||
|
|
return str(v)
|
||
|
|
|
||
|
|
|
||
|
|
def generate_joined_folder(path: str, *args) -> str:
|
||
|
|
'''like os.path.join, except it it will create a folder if it doesn't already exists'''
|
||
|
|
for folder in args:
|
||
|
|
path = os.path.join(path, folder)
|
||
|
|
if not os.path.exists(path):
|
||
|
|
os.mkdir(path)
|
||
|
|
return path
|
||
|
|
|
||
|
|
|
||
|
|
def match(string1: str | None, string2: str | None, debug=False) -> bool:
|
||
|
|
'''checks that, of two strings, either both exist or nether exists, and both are the same except for case'''
|
||
|
|
if isinstance(string1, str) and isinstance(string2, str):
|
||
|
|
m = string1.lower() == string2.lower()
|
||
|
|
return m
|
||
|
|
else:
|
||
|
|
return string1 == string2
|
||
|
|
|
||
|
|
|
||
|
|
def to_yesno(bool: bool) -> str:
|
||
|
|
'''converts a boolean to the string "yes" or "no", to match Stellaris syntax'''
|
||
|
|
if bool:
|
||
|
|
return "yes"
|
||
|
|
else:
|
||
|
|
return "no"
|
||
|
|
|
||
|
|
|
||
|
|
def in_common(*args: list[str]) -> str:
|
||
|
|
'''shorthand for os.path.join('common',*args)'''
|
||
|
|
return os.path.join('common', *args)
|
||
|
|
|
||
|
|
|
||
|
|
government_triggers = {
|
||
|
|
'country_type': 'is_country_type',
|
||
|
|
'species_class': 'is_species_class',
|
||
|
|
'species_archetype': 'is_species_class',
|
||
|
|
'origin': 'has_origin',
|
||
|
|
'ethics': 'has_ethic',
|
||
|
|
'authority': 'has_authority',
|
||
|
|
'civics': 'has_civic',
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
class parserCommandObject():
|
||
|
|
def __init__(self, code, key, *args) -> None:
|
||
|
|
self.code = code
|
||
|
|
self.key = key
|
||
|
|
self.parameters = args
|
||
|
|
|
||
|
|
def isspace(self):
|
||
|
|
return False
|
||
|
|
|
||
|
|
def startswith(self, str: str):
|
||
|
|
return False
|
||
|
|
|
||
|
|
def restoreString(self, str: str):
|
||
|
|
text_params = [self.key] + self.parameters
|
||
|
|
return f"#{self.code}:({':'.join(text_params)})"
|
||
|
|
|
||
|
|
|
||
|
|
class tokenizer():
|
||
|
|
def __init__(
|
||
|
|
self,
|
||
|
|
string: str,
|
||
|
|
parser_commands: str | list[str] | None = None,
|
||
|
|
debug=False,
|
||
|
|
) -> None:
|
||
|
|
self.debug = debug
|
||
|
|
# replace parser command tokens with something that doesn't start with "#" so they don't get removed with comments
|
||
|
|
if isinstance(parser_commands, str):
|
||
|
|
parser_command_template = r"#{}:([^ \n]*)".format(parser_commands)
|
||
|
|
string = re.sub(parser_command_template, r"@PARSER:{}:\1".format(parser_commands), string)
|
||
|
|
elif isinstance(parser_commands, list):
|
||
|
|
for key in parser_commands:
|
||
|
|
parser_command_template = r"#{}:([^ \n]*)".format(key)
|
||
|
|
string = re.sub(parser_command_template, r"@PARSER:{}:\1".format(key), string)
|
||
|
|
# # remove comments
|
||
|
|
# # if not include_formatting:
|
||
|
|
# string = string+"\n"
|
||
|
|
# string = re.sub(r"#.*\n",r" ",string)
|
||
|
|
# prepare string for splitting
|
||
|
|
# normal script and metascript special characters
|
||
|
|
string = string.replace('', '') # since vanilla inconsistently uses BOM or non-BOM encoding
|
||
|
|
string = re.sub(r'(\s+)', r'‗\1‗', string)
|
||
|
|
string = string.replace('#', '‗#')
|
||
|
|
string = string.replace('\n', '‗\n')
|
||
|
|
string = string.replace('=', '‗=‗')
|
||
|
|
string = string.replace('<', '‗<‗')
|
||
|
|
string = string.replace('>', '‗>‗')
|
||
|
|
string = string.replace('{', '‗{‗')
|
||
|
|
string = string.replace('}', '‗}‗')
|
||
|
|
string = string.replace('[', '‗[‗')
|
||
|
|
string = string.replace(']', '‗]‗')
|
||
|
|
string = re.sub(r'@(\\*)‗\[', r'‗@[', string)
|
||
|
|
string = string.replace('$', '‗$‗')
|
||
|
|
string = string.replace('|', '‗|‗')
|
||
|
|
# inline maths special characters
|
||
|
|
string = string.replace('"', '‗"‗')
|
||
|
|
# escaping
|
||
|
|
string = string.replace('\\\\', '‗\\\\‗')
|
||
|
|
string = string.replace('\\‗"', '‗\\"')
|
||
|
|
string = string.replace('+', '‗+‗')
|
||
|
|
string = string.replace('-', '‗-‗')
|
||
|
|
string = string.replace('/', '‗/‗')
|
||
|
|
string = string.replace('@PARSER:‗/‗', '@PARSER:/')
|
||
|
|
string = string.replace('*', '‗*‗')
|
||
|
|
string = string.replace('%', '‗%‗')
|
||
|
|
string = string.replace('(', '‗(‗')
|
||
|
|
string = string.replace(')', '‗)‗')
|
||
|
|
string = re.sub(r'‗+', r'‗', string)
|
||
|
|
# split by the metatoken delimeter
|
||
|
|
tokenList = string.split('‗')
|
||
|
|
# apply parser skip command
|
||
|
|
i = 0
|
||
|
|
while i < len(tokenList):
|
||
|
|
token = tokenList[i]
|
||
|
|
if token == '':
|
||
|
|
tokenList.pop(i)
|
||
|
|
elif token == '@PARSER:skip':
|
||
|
|
while token != '@PARSER:/skip' and i < len(tokenList):
|
||
|
|
token = tokenList.pop(i)
|
||
|
|
else:
|
||
|
|
if token.startswith('@PARSER'):
|
||
|
|
parameters = token.split(':')
|
||
|
|
parameters.pop(0)
|
||
|
|
tokenList[i] = parserCommandObject(*parameters)
|
||
|
|
i += 1
|
||
|
|
tokenList.append('\n')
|
||
|
|
self.metatokens = tokenList
|
||
|
|
self.position = -1
|
||
|
|
# self.nextAny = self.nextAny()
|
||
|
|
# self.nextActive = self.nextActive()
|
||
|
|
self.cwtokens = self.mode(
|
||
|
|
special_tokens=['=', '{', '}', '"', '<', '>', '@['],
|
||
|
|
split_on_whitespace=True,
|
||
|
|
debug_name='cwtokens',
|
||
|
|
)
|
||
|
|
self.IMOperators = self.mode(
|
||
|
|
end_token=['+', '-', '*', '/', '%', ']', ')', '|'],
|
||
|
|
start_token=['(', '[', '|'],
|
||
|
|
split_on_whitespace=True,
|
||
|
|
)
|
||
|
|
self.IMValues = self.mode(
|
||
|
|
end_token=['(', '[', '|', '-'],
|
||
|
|
start_token=['+', '-', '*', '/', '%', ']', ')', '|'],
|
||
|
|
split_on_whitespace=True,
|
||
|
|
)
|
||
|
|
self.metascriptTokens = self.mode(
|
||
|
|
special_tokens=['{', '}', '"', '[', '@[', '$', 'optimize_memory'],
|
||
|
|
split_on_whitespace=False,
|
||
|
|
)
|
||
|
|
self.metascriptConditionalTokens = self.mode(
|
||
|
|
special_tokens=[']', '"', '[', '@[', '$'],
|
||
|
|
split_on_whitespace=False,
|
||
|
|
)
|
||
|
|
self.metascriptSubstitutionTokens = self.mode(
|
||
|
|
special_tokens=['|', '$'],
|
||
|
|
split_on_whitespace=False,
|
||
|
|
)
|
||
|
|
|
||
|
|
def current_mt(self) -> str | parserCommandObject:
|
||
|
|
if self.position >= len(self.metatokens):
|
||
|
|
return None
|
||
|
|
else:
|
||
|
|
return self.metatokens[self.position]
|
||
|
|
|
||
|
|
def next_mt(self) -> str | parserCommandObject | None:
|
||
|
|
if self.position + 1 >= len(self.metatokens):
|
||
|
|
return None
|
||
|
|
else:
|
||
|
|
return self.metatokens[self.position + 1]
|
||
|
|
|
||
|
|
def step(self, skip_comments: bool = False, skip_whitespace: bool = False):
|
||
|
|
while self.position <= len(self.metatokens):
|
||
|
|
self.position += 1
|
||
|
|
if not isinstance(self.current_mt(), str):
|
||
|
|
break
|
||
|
|
if skip_comments and self.current_mt().startswith('#'):
|
||
|
|
while not self.current_mt().startswith('\n'):
|
||
|
|
self.position += 1
|
||
|
|
if (not skip_whitespace) or (not self.current_mt().isspace()):
|
||
|
|
break
|
||
|
|
|
||
|
|
def mode(
|
||
|
|
self,
|
||
|
|
special_tokens: list[str] | None = None,
|
||
|
|
end_token: list[str] | None = None,
|
||
|
|
start_token: list[str] | None = None,
|
||
|
|
skip_comments: bool = True,
|
||
|
|
split_on_whitespace: bool = True,
|
||
|
|
debug_name='',
|
||
|
|
) -> Generator[tuple[str | parserCommandObject | None, bool], None, None]:
|
||
|
|
|
||
|
|
if end_token is None:
|
||
|
|
end_token = special_tokens
|
||
|
|
|
||
|
|
if start_token is None:
|
||
|
|
start_token = special_tokens
|
||
|
|
|
||
|
|
token = ''
|
||
|
|
|
||
|
|
while True:
|
||
|
|
self.step(skip_comments=skip_comments, skip_whitespace=split_on_whitespace)
|
||
|
|
if self.position >= len(self.metatokens):
|
||
|
|
break
|
||
|
|
elif not isinstance(self.current_mt(), str):
|
||
|
|
yield self.current_mt()
|
||
|
|
else:
|
||
|
|
token += self.current_mt()
|
||
|
|
if (
|
||
|
|
(
|
||
|
|
split_on_whitespace
|
||
|
|
and
|
||
|
|
(
|
||
|
|
self.next_mt().isspace()
|
||
|
|
or
|
||
|
|
self.next_mt().startswith('#')
|
||
|
|
)
|
||
|
|
)
|
||
|
|
or (token in end_token)
|
||
|
|
or (self.next_mt() in start_token)
|
||
|
|
or (not isinstance(self.next_mt(), str))
|
||
|
|
):
|
||
|
|
yield token
|
||
|
|
token = ''
|
||
|
|
|
||
|
|
def string_before(self, char: str) -> str:
|
||
|
|
token = ''
|
||
|
|
while True:
|
||
|
|
self.step()
|
||
|
|
if self.current_mt() == char:
|
||
|
|
return (token)
|
||
|
|
token += self.current_mt()
|
||
|
|
|
||
|
|
def getQuotedString(self) -> str:
|
||
|
|
token = ''
|
||
|
|
while True:
|
||
|
|
self.step()
|
||
|
|
if self.current_mt() == '"':
|
||
|
|
return (token)
|
||
|
|
elif self.current_mt() == '\\"':
|
||
|
|
token += '"'
|
||
|
|
elif self.current_mt() == '\\\\':
|
||
|
|
token += '\\'
|
||
|
|
else:
|
||
|
|
token += self.current_mt()
|
||
|
|
|
||
|
|
|
||
|
|
class CWOverwritable():
|
||
|
|
def __init__(
|
||
|
|
self,
|
||
|
|
filename: str | None = None,
|
||
|
|
overwrite_type: str | None = None,
|
||
|
|
mod: Mod | None = None,
|
||
|
|
) -> None:
|
||
|
|
self.filename = filename
|
||
|
|
self.overwrite_type = overwrite_type
|
||
|
|
self.mod = mod
|
||
|
|
|
||
|
|
def overwrites(self, other: CWOverwritable, default: bool):
|
||
|
|
if self.overwrite_type == 'LIOS':
|
||
|
|
if self.filename < other.filename:
|
||
|
|
return True
|
||
|
|
elif self.filename > other.filename:
|
||
|
|
return False
|
||
|
|
else:
|
||
|
|
return default
|
||
|
|
if self.overwrite_type == 'FIOS':
|
||
|
|
if self.filename > other.filename:
|
||
|
|
return True
|
||
|
|
elif self.filename < other.filename:
|
||
|
|
return False
|
||
|
|
else:
|
||
|
|
return default
|
||
|
|
|
||
|
|
|
||
|
|
class inlineMathsStep():
|
||
|
|
def __init__(self, operator: str | None = None, value=None, negative=False):
|
||
|
|
self.operator = operator
|
||
|
|
self.value = value
|
||
|
|
self.negative = False
|
||
|
|
|
||
|
|
def __str__(self) -> str:
|
||
|
|
operator = self.operator if (self.operator is not None) else ''
|
||
|
|
sign = '-' if self.negative else ''
|
||
|
|
return f"{self.operator} {sign}{str(self.value)}"
|
||
|
|
|
||
|
|
def __repr__(self) -> str:
|
||
|
|
operator = self.operator if (self.operator is not None) else '='
|
||
|
|
sign = '-' if self.negative else ''
|
||
|
|
return f"{operator}{sign}({str(self.value)})"
|
||
|
|
|
||
|
|
def apply(self, prev):
|
||
|
|
if self.negative:
|
||
|
|
self.value = (-1 * self.value)
|
||
|
|
self.negative = False
|
||
|
|
if self.operator is None:
|
||
|
|
return self.value
|
||
|
|
elif self.operator == '+':
|
||
|
|
return prev + self.value
|
||
|
|
elif self.operator == '-':
|
||
|
|
return prev - self.value
|
||
|
|
elif self.operator == '*':
|
||
|
|
return prev * self.value
|
||
|
|
elif self.operator == '/':
|
||
|
|
try:
|
||
|
|
return prev / self.value
|
||
|
|
except ZeroDivisionError:
|
||
|
|
# Stellaris Evolved relies on this
|
||
|
|
return 2147483647
|
||
|
|
elif self.operator == '%':
|
||
|
|
return prev % self.value
|
||
|
|
|
||
|
|
|
||
|
|
class inlineMathsBlock(UserList):
|
||
|
|
endchar = ')'
|
||
|
|
|
||
|
|
def __str__(self) -> str:
|
||
|
|
contentstrings = map(str, self)
|
||
|
|
return f"( {' '.join(contentstrings)} )"
|
||
|
|
|
||
|
|
def __repr__(self) -> str:
|
||
|
|
return f'({str(self.data)})'
|
||
|
|
|
||
|
|
def parse(self, tokens: tokenizer, replace_local_variables: bool = False, local_variables: dict[str, str] = True,
|
||
|
|
debug=False) -> Self:
|
||
|
|
tree_params = {
|
||
|
|
'replace_local_variables': replace_local_variables,
|
||
|
|
'local_variables': local_variables,
|
||
|
|
}
|
||
|
|
self.append(inlineMathsStep())
|
||
|
|
for val in tokens.IMValues:
|
||
|
|
if val == '-':
|
||
|
|
self[-1].negative = (not self[-1].negative)
|
||
|
|
else:
|
||
|
|
if val == '(':
|
||
|
|
value = inlineMathsBlock().parse(tokens, **tree_params)
|
||
|
|
elif val == '|':
|
||
|
|
value = absValBlock().parse(tokens, **tree_params)
|
||
|
|
else:
|
||
|
|
if replace_local_variables and (val in local_variables):
|
||
|
|
val = local_variables[val]
|
||
|
|
value = numerify(val)
|
||
|
|
self[-1].value = value
|
||
|
|
nextOperator = next(tokens.IMOperators)
|
||
|
|
if nextOperator == self.endchar:
|
||
|
|
return self
|
||
|
|
else:
|
||
|
|
self.append(inlineMathsStep(nextOperator))
|
||
|
|
|
||
|
|
def simplify(self, vars: Mod | None = None):
|
||
|
|
for step in self:
|
||
|
|
if isinstance(step.value, inlineMathsBlock):
|
||
|
|
step.value = step.value.simplify(vars=vars)
|
||
|
|
if isinstance(step.value, str) and isinstance(vars, Mod):
|
||
|
|
gv_value = vars.global_variables(step.value)
|
||
|
|
if gv_value is not None:
|
||
|
|
step.value = numerify(gv_value)
|
||
|
|
while len(self) > 1:
|
||
|
|
if not (
|
||
|
|
isinstance(self[0].value, Number)
|
||
|
|
and isinstance(self[1].value, Number)
|
||
|
|
):
|
||
|
|
return self
|
||
|
|
else:
|
||
|
|
self[0].value = self.pop(1).apply(self[0].value)
|
||
|
|
if isinstance(self[0].value, Number):
|
||
|
|
return self[0].value
|
||
|
|
elif isinstance(self[0].value, str):
|
||
|
|
return self[0].value
|
||
|
|
elif type(self[0]) == inlineMathsBlock:
|
||
|
|
return type(self)(self[0])
|
||
|
|
else:
|
||
|
|
return self
|
||
|
|
|
||
|
|
def simplification(self, vars: Mod | None = None):
|
||
|
|
c = deepcopy(self)
|
||
|
|
return c.simplify(vars=vars)
|
||
|
|
|
||
|
|
|
||
|
|
class inlineMathsUnit(inlineMathsBlock):
|
||
|
|
startchar = '@['
|
||
|
|
endchar = ']'
|
||
|
|
|
||
|
|
def __str__(self) -> str:
|
||
|
|
contentstrings = map(str, self)
|
||
|
|
return f"@[ {' '.join(contentstrings)} ]"
|
||
|
|
|
||
|
|
def __repr__(self) -> str:
|
||
|
|
return f'@[{str(self.data)}]'
|
||
|
|
|
||
|
|
def simplify(self, vars: Mod | None = None):
|
||
|
|
value = super().simplify(vars=vars)
|
||
|
|
if isinstance(value, Number):
|
||
|
|
if isinstance(value, float) and value.is_integer():
|
||
|
|
value = int(value)
|
||
|
|
return str(value)
|
||
|
|
elif isinstance(value, str):
|
||
|
|
return '@' + value
|
||
|
|
else:
|
||
|
|
return value
|
||
|
|
|
||
|
|
|
||
|
|
class absValBlock(inlineMathsBlock):
|
||
|
|
startchar = '|'
|
||
|
|
endchar = '|'
|
||
|
|
|
||
|
|
def __str__(self) -> str:
|
||
|
|
contentstrings = map(str, self)
|
||
|
|
return f"| {' '.join(contentstrings)} |"
|
||
|
|
|
||
|
|
def __repr__(self) -> str:
|
||
|
|
return f'|{str(self.data)}|'
|
||
|
|
|
||
|
|
def simplify(self, vars: Mod | None = None):
|
||
|
|
value = super().simplify(vars=vars)
|
||
|
|
if isinstance(value, Number):
|
||
|
|
return abs(value)
|
||
|
|
elif isinstance(value, str):
|
||
|
|
return self
|
||
|
|
else:
|
||
|
|
return value
|
||
|
|
|
||
|
|
|
||
|
|
def resolveValue(value, vars: Mod | None = None):
|
||
|
|
''' attempts to solve inlineMathsBlocks and look up global variables to give a final value.
|
||
|
|
e.g. resolveValue("@discovery_weight") returns "3"'''
|
||
|
|
vars = defaultToGlobal(vars, 'context_mod')
|
||
|
|
if vars is not None:
|
||
|
|
if isinstance(value, inlineMathsBlock):
|
||
|
|
value = value.simplification(vars=vars)
|
||
|
|
if isinstance(value, str) and value.startswith('@'):
|
||
|
|
key = value[1:]
|
||
|
|
gv_value = vars.global_variables(key)
|
||
|
|
if gv_value is not None:
|
||
|
|
value = gv_value
|
||
|
|
return value
|
||
|
|
|
||
|
|
|
||
|
|
class CWList(UserList):
|
||
|
|
'''Class for representing blocks of CW script such as the contents of a folder or the contents of a pair of brackets. Should generally be created using a function such as stringToCW.'''
|
||
|
|
|
||
|
|
def __init__(self, *args, local_variables={}, bracketClass: type | None = None):
|
||
|
|
if bracketClass is None:
|
||
|
|
bracketClass = CWListValue
|
||
|
|
super().__init__(*args)
|
||
|
|
self.parent_element = None
|
||
|
|
for element in self:
|
||
|
|
element.parent_list = self
|
||
|
|
self.local_variables = local_variables
|
||
|
|
self.bracketClass = bracketClass
|
||
|
|
|
||
|
|
def __iter__(self) -> Iterator[CWElement]:
|
||
|
|
return super().__iter__()
|
||
|
|
|
||
|
|
def __repr__(self):
|
||
|
|
return f'{repr(self.parent_element)}[{len(self)}]'
|
||
|
|
|
||
|
|
def append(self, item: CWElement) -> None:
|
||
|
|
item.parent_list = self
|
||
|
|
return super().append(item)
|
||
|
|
|
||
|
|
def insert(self, i: int, item: CWElement) -> None:
|
||
|
|
item.parent_list = self
|
||
|
|
return super().insert(i, item)
|
||
|
|
|
||
|
|
def __add__(self, other: Iterable) -> Self:
|
||
|
|
new_list = super().__add__(other)
|
||
|
|
for element in new_list:
|
||
|
|
element.parent_list = new_list
|
||
|
|
if isinstance(other, CWList):
|
||
|
|
new_list.local_variables = self.local_variables | other.local_variables
|
||
|
|
else:
|
||
|
|
new_list.local_variables = self.local_variables
|
||
|
|
return new_list
|
||
|
|
|
||
|
|
def __iadd__(self, other: Iterable) -> Self:
|
||
|
|
for element in other:
|
||
|
|
element.parent_list = self
|
||
|
|
if isinstance(other, CWList):
|
||
|
|
self.local_variables.update(other.local_variables)
|
||
|
|
return super().__iadd__(other)
|
||
|
|
|
||
|
|
def __setitem__(self, key, value):
|
||
|
|
# if isinstance(key,str):
|
||
|
|
# self.getElement(key).setValue(value)
|
||
|
|
# else:
|
||
|
|
value.parent_list = self
|
||
|
|
super().__setitem__(key, value)
|
||
|
|
|
||
|
|
# def __getitem__(self,key,*args):
|
||
|
|
# if isinstance(key,str):
|
||
|
|
# return self.getElement(key,*args)
|
||
|
|
# else:
|
||
|
|
# return super().__getitem__(key,*args)
|
||
|
|
|
||
|
|
def parse(
|
||
|
|
self,
|
||
|
|
tokens: tokenizer,
|
||
|
|
replace_local_variables: bool = False,
|
||
|
|
local_variables: dict[str, str] = {},
|
||
|
|
element_params: dict[str, str] = {},
|
||
|
|
debug=False,
|
||
|
|
) -> Self:
|
||
|
|
block_metadata = {}
|
||
|
|
unit_metadata = {}
|
||
|
|
for token in tokens.cwtokens:
|
||
|
|
if isinstance(token, parserCommandObject):
|
||
|
|
if token.key == 'add_block_metadata':
|
||
|
|
if len(token.parameters) == 1:
|
||
|
|
block_metadata[token.parameters[0]] = True
|
||
|
|
else:
|
||
|
|
block_metadata[token.parameters[0]] = token.parameters[1]
|
||
|
|
elif token.key == 'add_metadata':
|
||
|
|
if len(token.parameters) == 1:
|
||
|
|
unit_metadata[token.parameters[0]] = True
|
||
|
|
else:
|
||
|
|
unit_metadata[token.parameters[0]] = token.parameters[1]
|
||
|
|
elif token.key == '/add_block_metadata':
|
||
|
|
block_metadata.pop(token.parameters[0])
|
||
|
|
else:
|
||
|
|
# printsleep(f'-> {token}')
|
||
|
|
if token in ('=', '<', '>'):
|
||
|
|
lastElement.comparitor = [token]
|
||
|
|
elif token == '}':
|
||
|
|
break
|
||
|
|
else:
|
||
|
|
val = CWValue(
|
||
|
|
token,
|
||
|
|
tokens,
|
||
|
|
value_parse_params={
|
||
|
|
'replace_local_variables': replace_local_variables,
|
||
|
|
'local_variables': local_variables.copy(),
|
||
|
|
'debug': debug
|
||
|
|
},
|
||
|
|
element_params=element_params,
|
||
|
|
bracketClass=self.bracketClass
|
||
|
|
)
|
||
|
|
# printsleep(f'-> {val}')
|
||
|
|
if (len(self) == 0) or (lastElement.value is not None) or (
|
||
|
|
isinstance(val, str) and (len(lastElement.comparitor) == 0)):
|
||
|
|
lastElement = CWElement(
|
||
|
|
local_variables=local_variables.copy(),
|
||
|
|
**element_params
|
||
|
|
)
|
||
|
|
lastElement.metadata = unit_metadata
|
||
|
|
unit_metadata = {}
|
||
|
|
for key in block_metadata:
|
||
|
|
lastElement.metadata.setdefault(key, block_metadata[key])
|
||
|
|
self.append(lastElement)
|
||
|
|
if isinstance(val, str):
|
||
|
|
lastElement.name = val
|
||
|
|
continue
|
||
|
|
lastElement.setValue(val)
|
||
|
|
if (lastElement.name is not None) and lastElement.name.startswith('@'):
|
||
|
|
lv_key = lastElement.name[1:]
|
||
|
|
local_variables[lv_key] = val
|
||
|
|
if replace_local_variables:
|
||
|
|
self.pop()
|
||
|
|
# printsleep(len(self))
|
||
|
|
# printsleep(f'>> {lastElement}',0.5)
|
||
|
|
for element in self:
|
||
|
|
if element.value is None:
|
||
|
|
element.setValue(element.name)
|
||
|
|
element.name = None
|
||
|
|
if replace_local_variables:
|
||
|
|
if isinstance(element.value, str) and element.value.startswith('@'):
|
||
|
|
lv_key = element.value[1:]
|
||
|
|
if lv_key in local_variables:
|
||
|
|
element.setValue(local_variables[lv_key])
|
||
|
|
self.local_variables = local_variables
|
||
|
|
return self
|
||
|
|
|
||
|
|
def __str__(self):
|
||
|
|
contentstrings = map(str, self)
|
||
|
|
if len(self) > 1 or (len(self) == 1 and not isinstance(self[0].value, str)):
|
||
|
|
return '\n'.join(contentstrings)
|
||
|
|
else:
|
||
|
|
return ' '.join(contentstrings)
|
||
|
|
|
||
|
|
# def __getattr__(self,attribute):
|
||
|
|
# return self.getElement(attribute)
|
||
|
|
|
||
|
|
def __deepcopy__(self, memo=None):
|
||
|
|
return type(self)(deepcopy(self.data, memo), local_variables=deepcopy(self.local_variables, memo),
|
||
|
|
bracketClass=self.bracketClass)
|
||
|
|
|
||
|
|
def contents(self, expand_inlines: bool | None = None, inlines_mod: Mod | None = None,
|
||
|
|
expansion_exceptions: Callable[[CWElement], bool] = lambda _: False) -> Generator[
|
||
|
|
CWElement, None, None]:
|
||
|
|
'''yields the elements of this CWList.
|
||
|
|
parameters:
|
||
|
|
expand_inlines (optional): whether to expand inline scripts. If not specified, defaults to cw_parser.globalSettings['expand_inlines']
|
||
|
|
inlines_mod (optional): mod to source inline expansions from. If not specified, defaults to cw_parser.globalSettings['context_mod']
|
||
|
|
expansion_exceptions (optional): callable taking a CWElement. Inline scripts for which this will be returned rather than expanded.'''
|
||
|
|
expand_inlines = defaultToGlobal(expand_inlines, 'expand_inlines')
|
||
|
|
inlines_mod = defaultToGlobal(inlines_mod, 'context_mod')
|
||
|
|
for element in self:
|
||
|
|
if expand_inlines and (element.name == 'inline_script') and (
|
||
|
|
inlines_mod is not None) and not expansion_exceptions(element):
|
||
|
|
yield from element.inlineScriptExpansion(mod=inlines_mod, expansion_exceptions=expansion_exceptions,
|
||
|
|
bracketClass=self.bracketClass)
|
||
|
|
else:
|
||
|
|
yield element
|
||
|
|
|
||
|
|
def getElements(self, key: str | None, **kwargs) -> Generator[CWElement, None, None]:
|
||
|
|
'''yields each subelement of this block with the specified key'''
|
||
|
|
for element in self.contents(**kwargs):
|
||
|
|
if match(element.name, key):
|
||
|
|
yield element
|
||
|
|
|
||
|
|
def hasAttribute(self, key: str | None, **kwargs) -> bool:
|
||
|
|
'''checks if a block contains a subelement with the given key'''
|
||
|
|
for element in self.getElements(key, **kwargs):
|
||
|
|
return True
|
||
|
|
return False
|
||
|
|
|
||
|
|
def getElement(self, key: str | None, **kwargs) -> CWElement:
|
||
|
|
'''returns the first subelement of this block with the specified key'''
|
||
|
|
for element in self.getElements(key, **kwargs):
|
||
|
|
return element
|
||
|
|
return CWElement("", parent_list=self)
|
||
|
|
|
||
|
|
def getValue(self, key: str | None, default: str = "no", resolve: bool = False, **kwargs):
|
||
|
|
'''returns the right-hand value of the first subelement of this block with the specified key'''
|
||
|
|
for element in self.getElements(key, **kwargs):
|
||
|
|
if resolve:
|
||
|
|
return resolveValue(element.value)
|
||
|
|
else:
|
||
|
|
return element.value
|
||
|
|
return default
|
||
|
|
|
||
|
|
def getValueBoolean(self, key: str | None, default: str = "no", **kwargs):
|
||
|
|
'''returns the right-hand value of the first subelement of this block with the specified key, as a boolean'''
|
||
|
|
return self.getValue(key, default=default, **kwargs) != 'no'
|
||
|
|
|
||
|
|
def getValueBase(self, key: str | None, default: str = "no", **kwargs):
|
||
|
|
value = self.getValue(key, default, **kwargs)
|
||
|
|
while isinstance(value, CWList):
|
||
|
|
value = value.getValueBase('base', default='0', **kwargs)
|
||
|
|
return value
|
||
|
|
|
||
|
|
def hasKeyValue(self, key: str | None, value: str, **kwargs):
|
||
|
|
'''checks if the object has a subelement with the given key-value pair'''
|
||
|
|
for element in self.getElements(key, **kwargs):
|
||
|
|
if match(element.value, value):
|
||
|
|
return True
|
||
|
|
return False
|
||
|
|
|
||
|
|
def getValues(self, key: str | None, resolve: bool = False, **kwargs):
|
||
|
|
'''yields the right-hand value of each subelement of this block with the specified key'''
|
||
|
|
for element in self.getElements(key, **kwargs):
|
||
|
|
if resolve:
|
||
|
|
yield resolveValue(element.value)
|
||
|
|
else:
|
||
|
|
yield element.value
|
||
|
|
|
||
|
|
def getArrayContents(self, key: str | None, **kwargs):
|
||
|
|
'''yields each string within the specified array subelement'''
|
||
|
|
for element in self.getElements(key, **kwargs):
|
||
|
|
yield from element.getValues(None, **kwargs)
|
||
|
|
|
||
|
|
def getArrayContentsFirst(self, key: str, default: str = "no", **kwargs):
|
||
|
|
'''returns the first string within the specified array subelement'''
|
||
|
|
for element in self.getElements(key, **kwargs):
|
||
|
|
for entry in element.value:
|
||
|
|
return entry.value
|
||
|
|
return default
|
||
|
|
|
||
|
|
def getArrayContentsElements(self, key: str | None, **kwargs) -> Generator[CWElement, None, None]:
|
||
|
|
'''yeilds each element within the specified array subelement'''
|
||
|
|
for element in self.getElements(key, **kwargs):
|
||
|
|
yield from element.getElements(None, **kwargs)
|
||
|
|
|
||
|
|
def navigateByDict(self, directions: dict) -> Generator[tuple[CWElement, object], None, None]:
|
||
|
|
for element in self.contents():
|
||
|
|
yield from element.navigateByDict(directions)
|
||
|
|
|
||
|
|
def effectScopingRun(self, scopes: scopesContext, criteria: Callable[[CWElement], bool]) -> Generator[
|
||
|
|
tuple[CWElement, scopesContext], None, None]:
|
||
|
|
scripted_effects = globalSettings['context_mod'].scripted_effects()
|
||
|
|
effectNesting = globalSettings['effectNesting']
|
||
|
|
eventTypes = globalSettings['eventTypes']
|
||
|
|
for effect in self.contents(expand_inlines=True):
|
||
|
|
effect.scopes = scopes
|
||
|
|
if criteria(effect):
|
||
|
|
yield (effect, scopes)
|
||
|
|
if effect.name is None:
|
||
|
|
print(effect.filename)
|
||
|
|
effectName = effect.name.lower()
|
||
|
|
if (effectName == 'save_event_target_as') or (effectName == 'save_global_event_target_as'):
|
||
|
|
eventTarget(effect.resolve()).add(scopes.this)
|
||
|
|
elif effectName in scripted_effects:
|
||
|
|
yield from effect.metaScriptExpansion(scripted_effects).effectScopingRun(scopes, criteria)
|
||
|
|
elif effect.hasSubelements():
|
||
|
|
if effectName == 'fire_on_action':
|
||
|
|
event_scopes = scopes.firedContext()
|
||
|
|
scope_overwrites = effect.getElement('scopes')
|
||
|
|
for i in range(4):
|
||
|
|
key = 'from' * (i + 1)
|
||
|
|
if scope_overwrites.hasAttribute(key):
|
||
|
|
event_scopes.froms[i] = scopes.link(scope_overwrites.getValue(key, resolve=True))
|
||
|
|
onActionScopes(effect.getValue('on_action', resolve=True)).add(event_scopes)
|
||
|
|
elif effectName in eventTypes:
|
||
|
|
event_scopes = scopes.firedContext()
|
||
|
|
scope_overwrites = effect.getElement('scopes')
|
||
|
|
for i in range(4):
|
||
|
|
key = 'from' * (i + 1)
|
||
|
|
if scope_overwrites.hasAttribute(key):
|
||
|
|
event_scopes.froms[i] = scopes.link(scope_overwrites.getValue(key, resolve=True))
|
||
|
|
globalData['eventScopes'].setdefault(effect.getValue('id', resolve=True), scopesContext()).add(
|
||
|
|
event_scopes)
|
||
|
|
# elif match( effect.name, 'set_next_astral_rift_event' )
|
||
|
|
else:
|
||
|
|
chain = decomposeChain(effectName)
|
||
|
|
if isScopeChain(chain) and effect.hasSubelements():
|
||
|
|
yield from effect.value.effectScopingRun(scopes.link(chain), criteria)
|
||
|
|
# else:
|
||
|
|
# if effectName in ['create_country','create_rebels']:
|
||
|
|
# event_scopes = scopes.step('country').firedContext()
|
||
|
|
# onActionScopes('on_country_created').add(event_scopes)
|
||
|
|
for (effectBlock, scope) in effect.navigateByDict(effectNesting):
|
||
|
|
if effectBlock.hasSubelements():
|
||
|
|
if scope is None:
|
||
|
|
yield from effectBlock.value.effectScopingRun(scopes, criteria)
|
||
|
|
else:
|
||
|
|
yield from effectBlock.value.effectScopingRun(scopes.step(scope), criteria)
|
||
|
|
|
||
|
|
|
||
|
|
class CWListValue(CWList):
|
||
|
|
def __str__(self):
|
||
|
|
contentstrings = map(str, self)
|
||
|
|
if len(self) > 1 or (len(self) == 1 and not isinstance(self[0].value, str)):
|
||
|
|
return "{{{}\n}}".format(indent('\n'.join(contentstrings), initial_linebreak=True))
|
||
|
|
else:
|
||
|
|
return f"{{ {' '.join(contentstrings)} }}"
|
||
|
|
|
||
|
|
|
||
|
|
class CWElement(CWOverwritable):
|
||
|
|
def __init__(
|
||
|
|
self,
|
||
|
|
name: str | None = None,
|
||
|
|
comparitor: list[str] = [],
|
||
|
|
value=None,
|
||
|
|
parent_list: CWList | None = None,
|
||
|
|
filename: str | None = None,
|
||
|
|
overwrite_type: str | None = None,
|
||
|
|
mod: Mod | None = None,
|
||
|
|
local_variables: dict[str, str] = {},
|
||
|
|
scopes: scopesContext | None = None,
|
||
|
|
) -> None:
|
||
|
|
super().__init__(filename, overwrite_type, mod)
|
||
|
|
self.name = name
|
||
|
|
self.comparitor = comparitor
|
||
|
|
self.setValue(value)
|
||
|
|
self.parent_list = parent_list
|
||
|
|
self.local_variables = local_variables
|
||
|
|
self.metadata = {}
|
||
|
|
self.scriptExpansions = {}
|
||
|
|
self.scopes = scopes
|
||
|
|
|
||
|
|
# def __getitem__(self,*args):
|
||
|
|
# return self.value[*args]
|
||
|
|
|
||
|
|
def __deepcopy__(self, memo):
|
||
|
|
dc = CWElement(
|
||
|
|
self.name,
|
||
|
|
deepcopy(self.comparitor, memo),
|
||
|
|
deepcopy(self.value, memo),
|
||
|
|
filename=self.filename,
|
||
|
|
overwrite_type=self.overwrite_type,
|
||
|
|
mod=self.mod,
|
||
|
|
local_variables=deepcopy(self.local_variables, memo),
|
||
|
|
)
|
||
|
|
dc.metadata = deepcopy(self.metadata, memo)
|
||
|
|
return dc
|
||
|
|
|
||
|
|
def parent(self) -> CWElement:
|
||
|
|
return self.parent_list.parent_element
|
||
|
|
|
||
|
|
def setValue(self, value):
|
||
|
|
if isinstance(value, int) or isinstance(value, float):
|
||
|
|
value = str(value)
|
||
|
|
self.value = value
|
||
|
|
if isinstance(value, CWList) or isinstance(value, metaScript):
|
||
|
|
value.parent_element = self
|
||
|
|
|
||
|
|
def parse(self, tokens: tokenizer, replace_local_variables: bool = False, debug=False,
|
||
|
|
bracketClass: type = CWListValue):
|
||
|
|
for token in tokens.cwtokens:
|
||
|
|
if token in ('=', '<', '>'):
|
||
|
|
self.comparitor.append(token)
|
||
|
|
else:
|
||
|
|
self.setValue(
|
||
|
|
CWValue(
|
||
|
|
token,
|
||
|
|
tokens,
|
||
|
|
value_parse_params={
|
||
|
|
'replace_local_variables': replace_local_variables,
|
||
|
|
'local_variables': self.local_variables.copy(),
|
||
|
|
'debug': debug,
|
||
|
|
},
|
||
|
|
element_params={
|
||
|
|
'filename': self.filename,
|
||
|
|
'mod': self.mod,
|
||
|
|
'overwrite_type': self.overwrite_type,
|
||
|
|
},
|
||
|
|
bracketClass=(CWListValue if match(self.name, 'inline_script') else bracketClass),
|
||
|
|
)
|
||
|
|
)
|
||
|
|
break
|
||
|
|
|
||
|
|
def __str__(self):
|
||
|
|
if self.name is None:
|
||
|
|
return valueString(self.value)
|
||
|
|
else:
|
||
|
|
return f"{self.name} {''.join(self.comparitor)} {valueString(self.value)}"
|
||
|
|
|
||
|
|
def reprStem(self):
|
||
|
|
if self.parent() is None:
|
||
|
|
return self.name
|
||
|
|
else:
|
||
|
|
return f"{self.parent().reprStem()}>{self.name}"
|
||
|
|
|
||
|
|
def __repr__(self) -> str:
|
||
|
|
if self.hasSubelements():
|
||
|
|
return f"{self.reprStem()}={{}}"
|
||
|
|
else:
|
||
|
|
return f"{self.reprStem()}={repr(self.value)}"
|
||
|
|
|
||
|
|
def contents(self, expand_inlines: bool | None = None, inlines_mod: Mod | None = None,
|
||
|
|
expansion_exceptions: Callable[[CWElement], bool] = lambda e: False) -> Generator[
|
||
|
|
CWElement, None, None]:
|
||
|
|
if self.hasSubelements():
|
||
|
|
yield from self.value.contents(expand_inlines=expand_inlines, inlines_mod=inlines_mod,
|
||
|
|
expansion_exceptions=expansion_exceptions)
|
||
|
|
|
||
|
|
def resolve(self, vars: Mod | None = None):
|
||
|
|
return resolveValue(self.value, vars)
|
||
|
|
|
||
|
|
def hasSubelements(self):
|
||
|
|
'''returns True for elements of the form <key> = {<stuff>}, false otherwise.'''
|
||
|
|
return isinstance(self.value, CWList)
|
||
|
|
|
||
|
|
def getElements(self, key: str | None, **kwargs) -> Generator[CWElement, None, None]:
|
||
|
|
'''yields each subelement of this block with the specified key'''
|
||
|
|
if self.hasSubelements():
|
||
|
|
yield from self.value.getElements(key, **kwargs)
|
||
|
|
|
||
|
|
def hasAttribute(self, key: str | None, **kwargs) -> bool:
|
||
|
|
'''checks if a block contains a subelement with the given key'''
|
||
|
|
if not self.hasSubelements():
|
||
|
|
return False
|
||
|
|
return self.value.hasAttribute(key, **kwargs)
|
||
|
|
|
||
|
|
def getElement(self, key: str | None, **kwargs) -> CWElement:
|
||
|
|
'''returns the first subelement of this block with the specified key'''
|
||
|
|
if self.hasSubelements():
|
||
|
|
return self.value.getElement(key, **kwargs)
|
||
|
|
else:
|
||
|
|
return CWElement("")
|
||
|
|
|
||
|
|
def getValue(self, key: str | None, resolve=False, default: str = "no", **kwargs):
|
||
|
|
'''returns the right-hand value of the first subelement of this block with the specified key'''
|
||
|
|
if self.hasSubelements():
|
||
|
|
return self.value.getValue(key, default, resolve=resolve, **kwargs)
|
||
|
|
else:
|
||
|
|
return default
|
||
|
|
|
||
|
|
def getValueBoolean(self, key: str | None, default: str = "no", **kwargs):
|
||
|
|
'''returns the right-hand value of the first subelement of this block with the specified key, as a boolean'''
|
||
|
|
return self.getValue(key, default=default, resolve=True, **kwargs) != 'no'
|
||
|
|
|
||
|
|
def getValueBase(self, key: str | None, default: str = "no", **kwargs):
|
||
|
|
'''returns the right-hand value of the first subelement of this block with the specified key'''
|
||
|
|
if self.hasSubelements():
|
||
|
|
return self.value.getValueBase(key, default, **kwargs)
|
||
|
|
else:
|
||
|
|
return default
|
||
|
|
|
||
|
|
def hasKeyValue(self, key: str | None, value: str, **kwargs):
|
||
|
|
'''checks if the object has a subelement with the given key-value pair'''
|
||
|
|
return self.value.hasKeyValue(key, value, **kwargs)
|
||
|
|
|
||
|
|
def getValues(self, key: str | None, **kwargs):
|
||
|
|
'''yields the right-hand value of each subelement of this block with the specified key'''
|
||
|
|
if self.hasSubelements():
|
||
|
|
yield from self.value.getValues(key, **kwargs)
|
||
|
|
|
||
|
|
def getArrayContents(self, key: str | None, **kwargs):
|
||
|
|
'''yields each string within the specified array subelement'''
|
||
|
|
if self.hasSubelements():
|
||
|
|
yield from self.value.getArrayContents(key, **kwargs)
|
||
|
|
|
||
|
|
def getArrayContentsFirst(self, key: str, default: str = "no", **kwargs):
|
||
|
|
'''returns the first string within the specified array subelement'''
|
||
|
|
return self.value.getArrayContentsFirst(key, default, **kwargs)
|
||
|
|
|
||
|
|
def getArrayContentsElements(self, key: str | None, **kwargs) -> Generator[CWElement, None, None]:
|
||
|
|
'''yeilds each element within the specified array subelement'''
|
||
|
|
if self.hasSubelements():
|
||
|
|
yield from self.value.getArrayContentsElements(key, **kwargs)
|
||
|
|
|
||
|
|
def getRoot(self) -> CWElement:
|
||
|
|
'''returns the top-level object containing this one'''
|
||
|
|
if self.parent() is None:
|
||
|
|
return self
|
||
|
|
else:
|
||
|
|
return self.parent().getRoot()
|
||
|
|
|
||
|
|
def navigateByDict(self, directions: dict) -> Generator[tuple[CWElement, object], None, None]:
|
||
|
|
for key in [self.name.lower(), '*']:
|
||
|
|
if key in directions:
|
||
|
|
d = directions[key]
|
||
|
|
if isinstance(d, dict):
|
||
|
|
if self.hasSubelements():
|
||
|
|
yield from self.value.navigateByDict(d)
|
||
|
|
else:
|
||
|
|
yield (self, d)
|
||
|
|
|
||
|
|
def convertGovernmentTrigger(self, trigger: str = None, **kwargs) -> CWElement:
|
||
|
|
'''returns a copy of a government requirements block converted to normal trigger syntax'''
|
||
|
|
# text = <loc key> handled separately, at the next level up
|
||
|
|
if match(self.name, 'text'):
|
||
|
|
return ""
|
||
|
|
# convert "value = <whatever>" to "has_ethic = <whatever>", "has_authority = whatever" etc.
|
||
|
|
elif match(self.name, 'value'):
|
||
|
|
output = CWElement(trigger, ['='], self.value)
|
||
|
|
# "always" and "host_has_dlc" checks remain unchanged
|
||
|
|
elif match(self.name, 'always'):
|
||
|
|
output = CWElement('always', ['='], self.value)
|
||
|
|
elif match(self.name, 'host_has_dlc'):
|
||
|
|
output = CWElement('host_has_dlc', ['='], self.value)
|
||
|
|
# convert "ethic" blocks, "authority" blocks etc. into AND blocks if necessary
|
||
|
|
elif self.name.lower() in government_triggers:
|
||
|
|
output = CWElement('AND', ['='], CWListValue())
|
||
|
|
for element in self.contents(**kwargs):
|
||
|
|
if not match(element.name, 'text'):
|
||
|
|
output.value.append(element.convertGovernmentTrigger(government_triggers[self.name], **kwargs))
|
||
|
|
# no AND block needed for a single trigger
|
||
|
|
if len(output.value) == 1:
|
||
|
|
output = output.value[0]
|
||
|
|
# AND, OR etc. blocks remain unchanged
|
||
|
|
else:
|
||
|
|
output = CWElement(self.name, ['='], CWListValue())
|
||
|
|
for element in self.contents(**kwargs):
|
||
|
|
if not match(element.name, 'text'):
|
||
|
|
output.value.append(element.convertGovernmentTrigger(trigger, **kwargs))
|
||
|
|
# convert "text = <loc key>" to custom_tooltip
|
||
|
|
if self.hasAttribute('text'):
|
||
|
|
text_element = CWElement('text', ['='], self.getValue('text'))
|
||
|
|
if output.name == 'AND':
|
||
|
|
return CWElement(
|
||
|
|
'custom_tooltip',
|
||
|
|
['='],
|
||
|
|
CWListValue([text_element] + output.value)
|
||
|
|
)
|
||
|
|
else:
|
||
|
|
return CWElement(
|
||
|
|
'custom_tooltip',
|
||
|
|
['='],
|
||
|
|
CWListValue([text_element, output])
|
||
|
|
)
|
||
|
|
else:
|
||
|
|
return output
|
||
|
|
|
||
|
|
def getArrayTriggers(self, block: str, trigger: str, mode=None, default='no', **kwargs) -> str:
|
||
|
|
'''generates a trigger or effect block (in string form) from the contents of an array, e.g. you can use this to convert a prerequisite block to something of the form
|
||
|
|
"AND = { has_technology = <tech> has_technology = <tech> }"
|
||
|
|
block: the name of the array, e.g. "prerequisities"
|
||
|
|
trigger: the name of the trigger to use in the output, e.g. "has_technology"
|
||
|
|
mode: whether the triggers should be combined as "AND", "OR", "NAND", or "NOR". Default is appropriate for effect blocks.
|
||
|
|
default: value to return if the array is nonexistant or empty
|
||
|
|
'''
|
||
|
|
lines = []
|
||
|
|
for item in self.getArrayContents(block, **kwargs):
|
||
|
|
lines.append('{} = {}'.format(trigger, item))
|
||
|
|
if mode == 'OR':
|
||
|
|
if len(lines) == 0:
|
||
|
|
return default
|
||
|
|
elif len(lines) == 1:
|
||
|
|
return lines[0]
|
||
|
|
else:
|
||
|
|
lines_block = ' '.join(lines)
|
||
|
|
return 'OR = {{ {} }}'.format(lines_block)
|
||
|
|
elif mode == 'NOR':
|
||
|
|
if len(lines) == 0:
|
||
|
|
return default
|
||
|
|
elif len(lines) == 1:
|
||
|
|
return 'NOT = {{ {} }}'.format(lines[0])
|
||
|
|
else:
|
||
|
|
lines_block = ' '.join(lines)
|
||
|
|
return 'NOR = {{ {} }}'.format(lines_block)
|
||
|
|
elif mode == 'AND':
|
||
|
|
if len(lines) == 0:
|
||
|
|
return default
|
||
|
|
elif len(lines) == 1:
|
||
|
|
return lines[0]
|
||
|
|
else:
|
||
|
|
lines_block = ' '.join(lines)
|
||
|
|
return 'AND = {{ {} }}'.format(lines_block)
|
||
|
|
elif mode == 'NAND':
|
||
|
|
if len(lines) == 0:
|
||
|
|
return default
|
||
|
|
elif len(lines) == 1:
|
||
|
|
return 'NOT = {{ {} }}'.format(lines[0])
|
||
|
|
else:
|
||
|
|
lines_block = ' '.join(lines)
|
||
|
|
return 'NAND = {{ {} }}'.format(lines_block)
|
||
|
|
else:
|
||
|
|
if len(lines) == 0:
|
||
|
|
return default
|
||
|
|
elif len(lines) == 1:
|
||
|
|
return lines[0]
|
||
|
|
else:
|
||
|
|
lines_block = ' '.join(lines)
|
||
|
|
return lines_block
|
||
|
|
|
||
|
|
def parent_hierarchy(self):
|
||
|
|
next_obj = self
|
||
|
|
while next_obj is not None:
|
||
|
|
yield next_obj
|
||
|
|
next_obj = next_obj.parent()
|
||
|
|
|
||
|
|
def inlineScriptExpansion(self, mod: Mod | None = None, parser_commands: str | list[str] | None = None,
|
||
|
|
bracketClass: type = CWListValue,
|
||
|
|
expansion_exceptions: Callable[[CWElement], bool] = lambda _: False) -> Generator[
|
||
|
|
CWElement, None, None]:
|
||
|
|
mod = defaultToGlobal(mod, 'context_mod')
|
||
|
|
parser_commands = defaultToGlobal(parser_commands, 'parser_commands')
|
||
|
|
conditionals = {
|
||
|
|
'generic_parts/giga_toggled_code': ('toggle', 'code'),
|
||
|
|
'conditional/parts/tec_step': ('x', 'code'),
|
||
|
|
}
|
||
|
|
parse_parameters = {
|
||
|
|
'parser_commands': parser_commands,
|
||
|
|
'filename': self.filename,
|
||
|
|
'overwrite_type': self.overwrite_type,
|
||
|
|
'mod': self.mod,
|
||
|
|
'bracketClass': bracketClass
|
||
|
|
}
|
||
|
|
|
||
|
|
if not (mod.mod_path in self.scriptExpansions):
|
||
|
|
# if there are parameters, replace them before parsing
|
||
|
|
if self.hasSubelements():
|
||
|
|
script_path = self.getValue('script')
|
||
|
|
# because quote escaping in inline scripts is too mysterious for me to simulate properly
|
||
|
|
if script_path in conditionals:
|
||
|
|
(toggle, contents) = conditionals[script_path]
|
||
|
|
if self.getElement(toggle).resolve(mod) == '1':
|
||
|
|
inline_contents = stringToCW(self.getValue(contents), **parse_parameters)
|
||
|
|
else:
|
||
|
|
inline_contents = CWList(bracketClass=bracketClass)
|
||
|
|
elif script_path == 'mod_support/tec_inlines_include':
|
||
|
|
# I give up
|
||
|
|
inline_contents = CWList(bracketClass=bracketClass)
|
||
|
|
elif script_path == 'iterators/tec_iterate_number':
|
||
|
|
code = self.getValue('code')
|
||
|
|
script = ' '.join(
|
||
|
|
[
|
||
|
|
code.replace('$current$', str(i))
|
||
|
|
for i
|
||
|
|
in range(
|
||
|
|
numerify(self.getValue('start', resolve=True)),
|
||
|
|
numerify(self.getValue('end', resolve=True)),
|
||
|
|
numerify(self.getValue('increment', resolve=True)),
|
||
|
|
)
|
||
|
|
]
|
||
|
|
)
|
||
|
|
inline_contents = stringToCW(script, **parse_parameters)
|
||
|
|
else:
|
||
|
|
script = mod.lookupInline(script_path)
|
||
|
|
if script is None:
|
||
|
|
raise MissingInlineScript(script_path)
|
||
|
|
else:
|
||
|
|
file = open(script, "r")
|
||
|
|
script = file.read()
|
||
|
|
file.close()
|
||
|
|
for param in self.value:
|
||
|
|
script = script.replace(f'${param.name}$', str(param.resolve(mod)))
|
||
|
|
inline_contents = stringToCW(script, **parse_parameters)
|
||
|
|
# if there are no parameters, read the file immediately
|
||
|
|
else:
|
||
|
|
script = mod.lookupInline(self.value)
|
||
|
|
if script is None:
|
||
|
|
raise MissingInlineScript
|
||
|
|
else:
|
||
|
|
inline_contents = fileToCW(script, **parse_parameters)
|
||
|
|
for subelement in inline_contents:
|
||
|
|
subelement.parent_list = self.parent_list
|
||
|
|
self.scriptExpansions[mod.mod_path] = inline_contents
|
||
|
|
yield from self.scriptExpansions[mod.mod_path].contents(expand_inlines=True, inlines_mod=mod,
|
||
|
|
expansion_exceptions=expansion_exceptions)
|
||
|
|
|
||
|
|
def metaScriptExpansion(self, metascript_dict, parser_commands=None) -> Generator[CWElement, None, None]:
|
||
|
|
parser_commands = defaultToGlobal(parser_commands, 'parser_commands')
|
||
|
|
if self.name in metascript_dict:
|
||
|
|
ms = metascript_dict[self.name].value
|
||
|
|
if self.hasSubelements():
|
||
|
|
parameters = {p.name: p.resolve() for p in self.contents(expand_inlines=True)}
|
||
|
|
return ms.inst(parameters)
|
||
|
|
else:
|
||
|
|
return ms.inst()
|
||
|
|
|
||
|
|
def effectScopingRun(self, scopes: scopesContext, criteria: Callable[[CWElement], bool]) -> Generator[
|
||
|
|
tuple[CWElement, scopesContext], None, None]:
|
||
|
|
if self.hasSubelements():
|
||
|
|
yield from self.value.effectScopingRun(scopes, criteria)
|
||
|
|
|
||
|
|
|
||
|
|
class ColorElement(CWListValue):
|
||
|
|
def __init__(self, format: str, *args):
|
||
|
|
super().__init__(*args)
|
||
|
|
self.format = format
|
||
|
|
|
||
|
|
def __str__(self):
|
||
|
|
return f"{self.format} {super().__str__()}"
|
||
|
|
|
||
|
|
|
||
|
|
def CWValue(
|
||
|
|
token: str,
|
||
|
|
tokens: tokenizer,
|
||
|
|
value_parse_params: dict = {},
|
||
|
|
element_params: dict = {},
|
||
|
|
bracketClass: type = CWListValue,
|
||
|
|
):
|
||
|
|
if token == '"':
|
||
|
|
return tokens.getQuotedString()
|
||
|
|
elif token == '{':
|
||
|
|
obj = bracketClass()
|
||
|
|
obj.parse(tokens, element_params=element_params, **value_parse_params)
|
||
|
|
return obj
|
||
|
|
elif token == '@[':
|
||
|
|
obj = inlineMathsUnit()
|
||
|
|
obj.parse(tokens, **value_parse_params)
|
||
|
|
return obj
|
||
|
|
elif token in ('hsv', 'rgb'):
|
||
|
|
obj = ColorElement(format=token)
|
||
|
|
next(tokens.cwtokens)
|
||
|
|
obj.parse(tokens, element_params=element_params, **value_parse_params)
|
||
|
|
return obj
|
||
|
|
else:
|
||
|
|
return token
|
||
|
|
|
||
|
|
|
||
|
|
registered_mods = {}
|
||
|
|
|
||
|
|
vanilla_mod_object = None
|
||
|
|
|
||
|
|
|
||
|
|
class Mod():
|
||
|
|
'''class for representing mods.
|
||
|
|
parameters:
|
||
|
|
workshop_item (optional): The number for this mod in Steam Workshop.
|
||
|
|
mod_path (optional): The path for this mod. If not specified it will be derived from workshop_item.
|
||
|
|
parents (optional): List of other mods to assume are also loaded if this one is. Default [].
|
||
|
|
is_base (optional): Boolean. While this is True, the script will always assume this mod is loaded.
|
||
|
|
key (optional): String that serves as a key for this mod in the cw_parser.registered_mods dictionary.
|
||
|
|
compat_var (optional): Sets the compat_var property, which is for holding the name of this mod's compatibility variable. Nothing in this module accesses this property; feel free to leave it empty unless your script needs it.
|
||
|
|
'''
|
||
|
|
|
||
|
|
def __init__(self, workshop_item: str | None = None, mod_path: str | None = None, parents: list[Mod] = [],
|
||
|
|
is_base: bool = False, key: str | None = None, compat_var: str | None = None) -> None:
|
||
|
|
if mod_path is None:
|
||
|
|
self.mod_path = os.path.join(globalSettings['workshop_path'], workshop_item)
|
||
|
|
else:
|
||
|
|
self.mod_path = mod_path
|
||
|
|
self.parents = parents
|
||
|
|
self.is_base = is_base
|
||
|
|
self.key = key
|
||
|
|
if key is not None:
|
||
|
|
registered_mods[key] = self
|
||
|
|
self.compat_var = compat_var
|
||
|
|
self.content_dictionaries = {}
|
||
|
|
self.content_lists = {}
|
||
|
|
self.mod_name = '[UNNAMED]'
|
||
|
|
globalSettings['mod_order'] = [self] + globalSettings['mod_order']
|
||
|
|
self.metascripts_loaded = False
|
||
|
|
self.load_global_variables()
|
||
|
|
self.giveName()
|
||
|
|
if self.is_base and vanilla_mod_object is not None:
|
||
|
|
vanilla_mod_object.inherit_content_dictionary('global_variables')
|
||
|
|
|
||
|
|
def __str__(self) -> str:
|
||
|
|
return self.mod_name
|
||
|
|
|
||
|
|
def load_global_variables(self) -> Self:
|
||
|
|
self.generate_content_dictionary('global_variables', in_common('scripted_variables'),
|
||
|
|
primary_key_rule=lambda x: x.name[1:], replace_local_variables=False)
|
||
|
|
self.inherit_content_dictionary('global_variables')
|
||
|
|
return self
|
||
|
|
|
||
|
|
def giveName(self):
|
||
|
|
descriptor_path = os.path.join(self.mod_path, 'descriptor.mod')
|
||
|
|
if os.path.exists(descriptor_path):
|
||
|
|
descriptor = fileToCW(descriptor_path)
|
||
|
|
self.mod_name = descriptor.getValue('name')
|
||
|
|
else:
|
||
|
|
self.mod_name = "Vanilla"
|
||
|
|
|
||
|
|
def activate(self):
|
||
|
|
'''sets this mod as the current active mod - inline scripts and global variables will be sourced from this mod, its parents, and mods where base=True'''
|
||
|
|
configure('context_mod', self)
|
||
|
|
|
||
|
|
def load_metascripts(self) -> Self:
|
||
|
|
'''Loads scropted triggers and effects.'''
|
||
|
|
for mod in self.inheritance():
|
||
|
|
if not mod.metascripts_loaded:
|
||
|
|
mod.generate_content_dictionary('scripted_triggers', in_common('scripted_triggers'),
|
||
|
|
bracketClass=metaScript, replace_local_variables=True)
|
||
|
|
mod.generate_content_dictionary('scripted_effects', in_common('scripted_effects'),
|
||
|
|
bracketClass=metaScript, replace_local_variables=True)
|
||
|
|
mod.generate_content_dictionary('script_values', in_common('script_values'), bracketClass=metaScript,
|
||
|
|
replace_local_variables=True)
|
||
|
|
mod.metascripts_loaded = True
|
||
|
|
self.inherit_content_dictionary('scripted_triggers')
|
||
|
|
self.inherit_content_dictionary('scripted_effects')
|
||
|
|
self.inherit_content_dictionary('script_values')
|
||
|
|
return self
|
||
|
|
|
||
|
|
def inheritance(self) -> Generator[Mod, None, None]:
|
||
|
|
'''yields the mod, then each of its parents in reverse load order, then vanilla'''
|
||
|
|
for mod in globalSettings['mod_order']:
|
||
|
|
if self.inherits_from(mod):
|
||
|
|
yield mod
|
||
|
|
|
||
|
|
def inherits_from(self, other: Mod) -> bool:
|
||
|
|
return ((other == self) or other.is_base or (other in self.parents))
|
||
|
|
|
||
|
|
def lookupInline(self, inline: str) -> str:
|
||
|
|
'''returns the path to the file an inline script would find if used with this mod, its parents, and no other mods
|
||
|
|
parameters:
|
||
|
|
inline'''
|
||
|
|
inline_breakdown = inline.split('/')
|
||
|
|
inline_path = in_common('inline_scripts')
|
||
|
|
for folder in inline_breakdown:
|
||
|
|
inline_path = os.path.join(inline_path, folder)
|
||
|
|
for mod in self.inheritance():
|
||
|
|
try_path = os.path.join(mod.mod_path, inline_path)
|
||
|
|
if os.path.exists(try_path + '.txt'):
|
||
|
|
return try_path + '.txt'
|
||
|
|
if os.path.exists(try_path):
|
||
|
|
return try_path
|
||
|
|
|
||
|
|
def folder(self, *path: list[str]) -> str:
|
||
|
|
return os.path.join(self.mod_path, *path)
|
||
|
|
|
||
|
|
def global_variables(self, key):
|
||
|
|
variables = self.content_dictionaries['global_variables']
|
||
|
|
if key in variables:
|
||
|
|
return variables[key].value
|
||
|
|
else:
|
||
|
|
return None
|
||
|
|
|
||
|
|
def scripted_triggers(self):
|
||
|
|
return self.content_dictionaries['scripted_triggers']
|
||
|
|
|
||
|
|
def scripted_effects(self):
|
||
|
|
return self.content_dictionaries['scripted_effects']
|
||
|
|
|
||
|
|
def script_values(self):
|
||
|
|
return self.content_dictionaries['script_values']
|
||
|
|
|
||
|
|
def getFiles(self, path: str, exclude_files: list[str] = [], include_parents: bool | None = None,
|
||
|
|
file_suffix: str = '.txt'):
|
||
|
|
if include_parents is None:
|
||
|
|
include_parents = self.is_base
|
||
|
|
exclude_files = exclude_files.copy()
|
||
|
|
exclude_files.append('99_README_EDICTS.txt') # please no-one else do whatever this one Vanilla file is doing
|
||
|
|
for mod in self.inheritance():
|
||
|
|
if (mod == self) or include_parents:
|
||
|
|
folder = mod.folder(path)
|
||
|
|
if os.path.exists(folder):
|
||
|
|
for file in os.listdir(path=folder):
|
||
|
|
if file.endswith(file_suffix) and not file in exclude_files:
|
||
|
|
filepath = os.path.join(folder, file)
|
||
|
|
exclude_files.append(file)
|
||
|
|
yield filepath
|
||
|
|
|
||
|
|
def read_folder(self, path: str, exclude_files: list[str] = [], replace_local_variables: bool | None = None,
|
||
|
|
include_parents: bool | None = None, file_suffix: str = '.txt',
|
||
|
|
parser_commands: str | list[str] | None = None, overwrite_type: str | None = 'LIOS',
|
||
|
|
bracketClass: type = CWListValue, save: bool = True, save_key: str | None = None,
|
||
|
|
print_filenames: bool = False, always_read: bool = False) -> CWList[CWElement]:
|
||
|
|
'''reads and parses the files in the specified folder in this mod into a CWList object
|
||
|
|
parameters:
|
||
|
|
path: the path to the specified folder, relative to the mod
|
||
|
|
exclude_files (optional): a lost of filenames to skip, e.g. because the entries within are dummy elements or because they're assumed to have been overwritten
|
||
|
|
replace_local_variables (optional): if True, locally-defined scripted variables will be replaced with their values.
|
||
|
|
include_parents (optional): if True, also load contents of parent folders that aren't file-overwritten
|
||
|
|
file_suffix (optional): only files with this suffix will be read. Default '.txt'
|
||
|
|
save (optional): if not set to False, the returned CWList is saved to the mod's content_lists dictionary.
|
||
|
|
save_key (optional): key under which to save to the content_lists dictionary. Defaults to path.
|
||
|
|
print_filenames (optional): if True, the function will print the name of each file as it reads it.
|
||
|
|
parser_commands (optional): if this is set to a string or list of strings, the following tags will be enabled (where KEY stands for any of the entered strings):
|
||
|
|
"#KEY:skip", "#KEY:/skip": ignore everything between these tags (or from the "#KEY:skip" to the end of the string if "#KEY:/skip" is not encountered)
|
||
|
|
"#KEY:add_metadata:<metadata key>:<metadata value>": set the specified attribute in the "metadata" dictionary to the specified value for the next object
|
||
|
|
"#KEY:add_block_metadata:<metadata key>:<metadata value>", "#KEY:/add_block_metadata:<metadata key>": set the specified attribute in the "metadata" dictionary to the specified value for each top-level object between these tags
|
||
|
|
'''
|
||
|
|
CW_list = None
|
||
|
|
if include_parents is None:
|
||
|
|
include_parents = self.is_base
|
||
|
|
if (path in self.content_lists) and not always_read:
|
||
|
|
return self.content_lists[path]
|
||
|
|
else:
|
||
|
|
CW_list = CWList(bracketClass=bracketClass)
|
||
|
|
for filepath in self.getFiles(path, exclude_files=exclude_files, include_parents=include_parents,
|
||
|
|
file_suffix=file_suffix):
|
||
|
|
if print_filenames:
|
||
|
|
print(filepath)
|
||
|
|
CW_list = CW_list + fileToCW(filepath, replace_local_variables=replace_local_variables,
|
||
|
|
parser_commands=parser_commands, overwrite_type=overwrite_type,
|
||
|
|
bracketClass=bracketClass, mod=self)
|
||
|
|
if save:
|
||
|
|
if save_key is None:
|
||
|
|
save_key = path
|
||
|
|
self.content_lists[save_key] = CW_list
|
||
|
|
return CW_list
|
||
|
|
|
||
|
|
def inherit_content_dictionary(self, key):
|
||
|
|
new_dict = self.content_dictionaries.setdefault(key, {})
|
||
|
|
for mod in self.inheritance():
|
||
|
|
old_dict = mod.content_dictionaries.setdefault(key, {})
|
||
|
|
for k in old_dict:
|
||
|
|
obj = old_dict[k]
|
||
|
|
if (not k in new_dict) or (obj.overwrites(new_dict[k], default=False)):
|
||
|
|
new_dict[k] = obj
|
||
|
|
|
||
|
|
def generate_content_dictionary(self, key, folder_path, primary_key_rule=lambda e: e.name,
|
||
|
|
element_filter=lambda _: True, **kwargs):
|
||
|
|
list = self.read_folder(path=folder_path, **kwargs)
|
||
|
|
content_dict = self.content_dictionaries.setdefault(key, {})
|
||
|
|
for element in list:
|
||
|
|
if element_filter(element):
|
||
|
|
content_dict[primary_key_rule(element)] = element
|
||
|
|
|
||
|
|
def load_events(self):
|
||
|
|
eventTypes = globalSettings['eventTypes']
|
||
|
|
self.content_dictionaries['events'] = {}
|
||
|
|
events = self.read_folder('events', overwrite_type='FIOS', print_filenames=True)
|
||
|
|
for event in events.contents():
|
||
|
|
if event.hasSubelements():
|
||
|
|
event_id = event.getValue('id', resolve=True)
|
||
|
|
self.content_dictionaries['events'][event_id] = event
|
||
|
|
if event.name.lower() in eventTypes:
|
||
|
|
globalData['eventScopes'][event_id] = scopesContext(eventTypes[event.name.lower()], lock=True)
|
||
|
|
|
||
|
|
def load_on_actions(self):
|
||
|
|
on_actions = self.read_folder(in_common('on_actions'), overwrite_type='merge')
|
||
|
|
for on_action in on_actions.contents():
|
||
|
|
onActionScopes(on_action.name)
|
||
|
|
|
||
|
|
def activate_on_actions(self):
|
||
|
|
eventScopes = globalData['eventScopes']
|
||
|
|
for on_action in self.content_lists[in_common('on_actions')].contents():
|
||
|
|
if on_action.hasAttribute('random_events'):
|
||
|
|
for event in on_action.getElement('random_events').contents():
|
||
|
|
event_key = event.resolve()
|
||
|
|
if event_key != '0':
|
||
|
|
eventScopes[event_key].add(onActionScopes(on_action.name))
|
||
|
|
if on_action.hasAttribute('events'):
|
||
|
|
for event in on_action.getElement('events').contents():
|
||
|
|
event_key = event.resolve()
|
||
|
|
if event_key in eventScopes: # excludes '0' and also missing events
|
||
|
|
eventScopes[event_key].add(onActionScopes(on_action.name))
|
||
|
|
|
||
|
|
def events(self) -> Generator[CWElement, None, None]:
|
||
|
|
yield from self.content_lists['events'].contents()
|
||
|
|
|
||
|
|
|
||
|
|
class metascriptSubstitution():
|
||
|
|
def __init__(self, keys: list[str] = [], default: str | None = ''):
|
||
|
|
self.keys = keys
|
||
|
|
self.default = ''
|
||
|
|
|
||
|
|
def parse(self, tokens: tokenizer) -> Self:
|
||
|
|
params = []
|
||
|
|
for token in tokens.metascriptSubstitutionTokens:
|
||
|
|
if token == '|':
|
||
|
|
pass
|
||
|
|
elif token == '$':
|
||
|
|
if len(params) > 1:
|
||
|
|
self.default = params.pop()
|
||
|
|
self.keys = params
|
||
|
|
return self
|
||
|
|
else:
|
||
|
|
params.append(token)
|
||
|
|
|
||
|
|
def inst(self, params: dict[str, str]) -> str:
|
||
|
|
for key in self.keys:
|
||
|
|
if key.lower() in params:
|
||
|
|
return params[key.lower()]
|
||
|
|
if self.default is None:
|
||
|
|
print(self.keys)
|
||
|
|
print(params)
|
||
|
|
return self.default
|
||
|
|
|
||
|
|
|
||
|
|
class metascriptConditional():
|
||
|
|
def __init__(self, key: str | None = None, valence: bool = True):
|
||
|
|
self.key = key
|
||
|
|
self.valence = valence
|
||
|
|
self.rawContents = []
|
||
|
|
|
||
|
|
def parse(self, tokens: tokenizer) -> Self:
|
||
|
|
brackets = 0
|
||
|
|
for token in tokens.metascriptConditionalTokens:
|
||
|
|
if token == '[' and self.key is None:
|
||
|
|
key = tokens.string_before(']')
|
||
|
|
if key.startswith('!'):
|
||
|
|
self.key = key[1:]
|
||
|
|
self.valence = False
|
||
|
|
else:
|
||
|
|
self.key = key
|
||
|
|
self.valence = True
|
||
|
|
elif token == '$':
|
||
|
|
self.rawContents.append(metascriptSubstitution().parse(tokens))
|
||
|
|
else:
|
||
|
|
if token == '@[':
|
||
|
|
brackets += 1
|
||
|
|
elif token == ']':
|
||
|
|
if brackets >= 0:
|
||
|
|
return self
|
||
|
|
else:
|
||
|
|
self.brackets -= 1
|
||
|
|
self.rawContents.append(token)
|
||
|
|
|
||
|
|
def inst(self, params: dict[str, str]) -> str:
|
||
|
|
if ((self.key.lower() in params) and (params[self.key.lower()] != 'no')) == self.valence:
|
||
|
|
inst = ''
|
||
|
|
for section in self.rawContents:
|
||
|
|
if isinstance(section, str):
|
||
|
|
inst += section
|
||
|
|
else:
|
||
|
|
inst += section.inst(params)
|
||
|
|
return inst
|
||
|
|
else:
|
||
|
|
return ''
|
||
|
|
|
||
|
|
|
||
|
|
class metaScript():
|
||
|
|
def __init__(
|
||
|
|
self,
|
||
|
|
parent_element: CWElement = None,
|
||
|
|
**_, # because other possible bracketClass values have more init and parse parameters
|
||
|
|
):
|
||
|
|
super().__init__()
|
||
|
|
self.rawContents = []
|
||
|
|
self.memory_optimized = False
|
||
|
|
self.default = CWListValue()
|
||
|
|
self.parent_element = parent_element
|
||
|
|
|
||
|
|
def parse(self, tokens: tokenizer, **_) -> Self:
|
||
|
|
brackets = 0
|
||
|
|
for token in tokens.metascriptTokens:
|
||
|
|
if isinstance(token, parserCommandObject):
|
||
|
|
token = token.restoreString()
|
||
|
|
if token == 'optimize_memory':
|
||
|
|
self.memory_optimized = True
|
||
|
|
elif token == '"':
|
||
|
|
self.rawContents.append('"{}"'.format(tokens.getQuotedString()))
|
||
|
|
elif token == '$':
|
||
|
|
self.rawContents.append(metascriptSubstitution().parse(tokens))
|
||
|
|
elif token == '[':
|
||
|
|
self.rawContents.append(metascriptConditional().parse(tokens))
|
||
|
|
else:
|
||
|
|
if token == '{':
|
||
|
|
brackets += 1
|
||
|
|
elif token == '}':
|
||
|
|
if brackets <= 0:
|
||
|
|
# self.updateDefault()
|
||
|
|
return self
|
||
|
|
else:
|
||
|
|
brackets -= 1
|
||
|
|
self.rawContents.append(token)
|
||
|
|
|
||
|
|
def inst(self, params: dict[str, str] = {}):
|
||
|
|
inst = ''
|
||
|
|
params = {key.lower(): params[key] for key in params}
|
||
|
|
for section in self.rawContents:
|
||
|
|
if isinstance(section, str):
|
||
|
|
inst += section
|
||
|
|
else:
|
||
|
|
inst += section.inst(params)
|
||
|
|
return stringToCW(
|
||
|
|
inst,
|
||
|
|
filename=self.parent_element.filename,
|
||
|
|
mod=self.parent_element.mod,
|
||
|
|
parent=self.parent_element,
|
||
|
|
)
|
||
|
|
|
||
|
|
def updateDefault(self):
|
||
|
|
try:
|
||
|
|
self.default = self.inst()
|
||
|
|
except TypeError:
|
||
|
|
self.default = None
|
||
|
|
|
||
|
|
|
||
|
|
def stringToCW(string: str, filename: str | None = None, parent: CWElement | None = None,
|
||
|
|
replace_local_variables: bool | None = None, parser_commands: str | list[str] | None = None,
|
||
|
|
overwrite_type: str | None = None, mod: Mod | None = None, bracketClass: type = CWListValue,
|
||
|
|
debug=False) -> CWList[CWElement]:
|
||
|
|
'''parses a string into a CWList object
|
||
|
|
parameters:
|
||
|
|
string: The string to convert.
|
||
|
|
filename (optional): Marks CWElements as being from the specified file.
|
||
|
|
parent (optional): Marks CWElements as being children of the specified CWElement.
|
||
|
|
replace_local_variables (optional): if True, locally-defined scripted variables will be replaced with their values.
|
||
|
|
parser_commands (optional): if this is set to a string or list of strings, the following tags will be enabled (where KEY stands for any of the entered strings):
|
||
|
|
"#KEY:skip", "#KEY:/skip": ignore everything between these tags (or from the "#KEY:skip" to the end of the string if "#KEY:/skip" is not encountered)
|
||
|
|
"#KEY:add_metadata:<metadata key>:<metadata value>": set the specified attribute in the "metadata" dictionary to the specified value for the next object
|
||
|
|
"#KEY:add_block_metadata:<metadata key>:<metadata value>", "#KEY:/add_block_metadata:<metadata key>": set the specified attribute in the "metadata" dictionary to the specified value for each top-level object between these tags
|
||
|
|
'''
|
||
|
|
parser_commands = defaultToGlobal(parser_commands, 'parser_commands')
|
||
|
|
replace_local_variables = defaultToGlobal(replace_local_variables, 'replace_local_variables')
|
||
|
|
tokens = tokenizer(string, parser_commands, debug=debug)
|
||
|
|
if parent is None:
|
||
|
|
local_variables = {}
|
||
|
|
else:
|
||
|
|
local_variables = parent.local_variables
|
||
|
|
return CWList(bracketClass=bracketClass).parse(
|
||
|
|
tokens,
|
||
|
|
replace_local_variables,
|
||
|
|
local_variables,
|
||
|
|
element_params={
|
||
|
|
'filename': filename,
|
||
|
|
'mod': mod,
|
||
|
|
'overwrite_type': overwrite_type,
|
||
|
|
},
|
||
|
|
debug=debug,
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
def fileToCW(path: str, filename: str | None = None, parent: CWElement | None = None,
|
||
|
|
replace_local_variables: bool | None = None, parser_commands: str | list[str] | None = None,
|
||
|
|
overwrite_type: str | None = 'LIOS', mod: Mod | None = None, bracketClass: type = CWListValue,
|
||
|
|
debug=False) -> CWList[CWElement]:
|
||
|
|
'''reads and parses a file into a list of CWElement objects
|
||
|
|
parameters:
|
||
|
|
path: The file path.
|
||
|
|
parent (optional): Marks CWElements as being children of the specified CWElement (for use in the CWElement.replaceInlines method).
|
||
|
|
replace_local_variables (optional): if True, locally-defined scripted variables will be replaced with their values.
|
||
|
|
parser_commands (optional): if this is set to a string or list of strings, the following tags will be enabled (where KEY stands for any of the entered strings):
|
||
|
|
"#KEY:skip", "#KEY:/skip": ignore everything between these tags (or from the "#KEY:skip" to the end of the string if "#KEY:/skip" is not encountered)
|
||
|
|
"#KEY:add_metadata:<metadata key>:<metadata value>": set the specified attribute in the "metadata" dictionary to the specified value for the next object
|
||
|
|
"#KEY:add_block_metadata:<metadata key>:<metadata value>", "#KEY:/add_block_metadata:<metadata key>": set the specified attribute in the "metadata" dictionary to the specified value for each top-level object between these tags
|
||
|
|
'''
|
||
|
|
file = open(path, "r", encoding='utf-8')
|
||
|
|
fileContents = file.read()
|
||
|
|
if filename is None:
|
||
|
|
filename = os.path.basename(path)
|
||
|
|
cw = stringToCW(fileContents, filename=filename, parent=parent, replace_local_variables=replace_local_variables,
|
||
|
|
parser_commands=parser_commands, overwrite_type=overwrite_type, mod=mod, bracketClass=bracketClass,
|
||
|
|
debug=debug)
|
||
|
|
return cw
|
||
|
|
|
||
|
|
|
||
|
|
vanilla_mod_object = Mod(mod_path=globalSettings['vanilla_path'], is_base=True, key='base', compat_var='1')
|
||
|
|
|
||
|
|
|
||
|
|
def set_vanilla_path(path: str):
|
||
|
|
'''tells the module where to find vanilla content'''
|
||
|
|
configure('vanilla_path', path)
|
||
|
|
vanilla_mod_object.mod_path = path
|
||
|
|
|
||
|
|
|
||
|
|
def set_mod_docs_path(path: str):
|
||
|
|
'''tells the module where to find your mods'''
|
||
|
|
configure('mod_docs_path', path)
|
||
|
|
|
||
|
|
|
||
|
|
def set_workshop_path(path: str):
|
||
|
|
'''tells the module where to find steam workshop mods'''
|
||
|
|
configure('workshop_path', path)
|
||
|
|
|
||
|
|
|
||
|
|
def set_expand_inlines(value: bool):
|
||
|
|
'''switches whether to look inside inline scripts by default when reading CWList content'''
|
||
|
|
configure('expand_inlines', value)
|
||
|
|
|
||
|
|
|
||
|
|
def set_replace_local_variables(value: bool):
|
||
|
|
'''switches whether to replace local variables by default when parsing strings'''
|
||
|
|
configure('replace_local_variables', value)
|
||
|
|
|
||
|
|
|
||
|
|
def set_parser_commands(value: list[str]):
|
||
|
|
'''switches what parser command identifiers to use'''
|
||
|
|
configure('parser_commands', value)
|
||
|
|
|
||
|
|
|
||
|
|
def set_load_order(mods: list[Mod]):
|
||
|
|
'''given a list of mods, move those mods to the end of assumed load order (with the first listed mod coming last)'''
|
||
|
|
load_order = globalSettings['mod_order']
|
||
|
|
for mod in mods:
|
||
|
|
load_order.remove(mod)
|
||
|
|
configure('mod_order', mods + load_order)
|
||
|
|
|
||
|
|
|
||
|
|
class scopeUnpackable(ABC):
|
||
|
|
@abstractmethod
|
||
|
|
def unpack(self, unpackTree: set | None = None) -> Iterator[str]:
|
||
|
|
pass
|
||
|
|
|
||
|
|
|
||
|
|
class scopeSet(scopeUnpackable):
|
||
|
|
def __init__(self, *args, locked=False) -> None:
|
||
|
|
self.unpacked_scopes = set()
|
||
|
|
self.pointers = []
|
||
|
|
self.unpacked = True
|
||
|
|
self.locked = False
|
||
|
|
for item in args:
|
||
|
|
self.add(item)
|
||
|
|
self.locked = locked
|
||
|
|
|
||
|
|
def add(self, item):
|
||
|
|
if not self.locked:
|
||
|
|
if isinstance(item, str):
|
||
|
|
self.unpacked_scopes.add(item)
|
||
|
|
if isinstance(item, scopeUnpackable) and not (item in self.pointers):
|
||
|
|
self.pointers.append(item)
|
||
|
|
self.unpacked = False
|
||
|
|
|
||
|
|
def unpack(self, unpackTree: list[scopeUnpackable] | None = None) -> set[str]:
|
||
|
|
if unpackTree is None:
|
||
|
|
unpackTree = []
|
||
|
|
unpackTree.append(self)
|
||
|
|
for item in self.pointers:
|
||
|
|
if not item in unpackTree:
|
||
|
|
self.unpacked_scopes.update(item.unpack())
|
||
|
|
return self.unpacked_scopes
|
||
|
|
|
||
|
|
|
||
|
|
def toScopes(input) -> scopeSet:
|
||
|
|
if isinstance(input, scopeSet):
|
||
|
|
return input
|
||
|
|
if isinstance(input, list):
|
||
|
|
return scopeSet(*input)
|
||
|
|
if isinstance(input, str):
|
||
|
|
return scopeSet(input)
|
||
|
|
|
||
|
|
|
||
|
|
def eventTarget(key: str) -> scopeSet:
|
||
|
|
event_targets = globalData['eventTargets']
|
||
|
|
if '@' in key:
|
||
|
|
key = key.split('@')[0] + '@'
|
||
|
|
return event_targets.setdefault(key, scopeSet())
|
||
|
|
|
||
|
|
|
||
|
|
def onActionScopes(key: str) -> scopesContext:
|
||
|
|
return globalData['onActionScopes'].setdefault(key, scopesContext())
|
||
|
|
|
||
|
|
|
||
|
|
def decomposeChain(s: str) -> list[str]:
|
||
|
|
return s.lower().split('.')
|
||
|
|
|
||
|
|
|
||
|
|
def isScopeChain(chain: list[str]) -> bool:
|
||
|
|
for unit in chain:
|
||
|
|
if not (
|
||
|
|
(unit in ['root', 'this', 'target', 'prev', 'prevprev', 'prevprevprev', 'prevprevprevprev', 'from',
|
||
|
|
'fromfrom', 'fromfromfrom', 'fromfromfromfrom', ])
|
||
|
|
or
|
||
|
|
(unit in globalSettings['scopeTransitions'])
|
||
|
|
or
|
||
|
|
(unit.startswith('event_target:'))
|
||
|
|
or
|
||
|
|
(unit.startswith('parameter:'))
|
||
|
|
):
|
||
|
|
return False
|
||
|
|
return True
|
||
|
|
|
||
|
|
|
||
|
|
class scopesContext():
|
||
|
|
'''Class for representing the context of an effect, trigger etc. in terms of whether it's in country scope, ship scope etc. and where "from" and "prev" point to.
|
||
|
|
Scope list can be accessed using the unpackThis() method.
|
||
|
|
public properties:
|
||
|
|
prev: a scopesContext corresponding to the "prev" scope
|
||
|
|
root: a scopesContext corresponding to the "root" scope
|
||
|
|
froms: a list containing the from, fromfrom, fromfromfrom, fromfromfromfrom, fromfromfromfrom.from etc. If you run into the end of this list, try increasing cw_parser.globalSettings['max_from_depth']
|
||
|
|
'''
|
||
|
|
|
||
|
|
def __init__(self, this=None, *args, froms=[], root=None, prev=None, lock=False) -> None:
|
||
|
|
froms = list(args) + froms
|
||
|
|
self.froms = [toScopes(f) for f in froms]
|
||
|
|
while len(self.froms) < globalSettings['max_from_depth']:
|
||
|
|
self.froms.append(scopeSet())
|
||
|
|
if root is None:
|
||
|
|
self.root = self
|
||
|
|
elif isinstance(root, scopesContext):
|
||
|
|
self.root = root
|
||
|
|
elif isinstance(root, str):
|
||
|
|
self.root = scopesContext(root, froms=froms)
|
||
|
|
if this is None:
|
||
|
|
self.this = scopeSet()
|
||
|
|
else:
|
||
|
|
self.this = toScopes(this)
|
||
|
|
if isinstance(prev, list):
|
||
|
|
if len(prev) > 0:
|
||
|
|
self.prev = scopesContext(prev.pop(), root=self.root, froms=self.froms, prev=prev)
|
||
|
|
else:
|
||
|
|
self.prev = None
|
||
|
|
else:
|
||
|
|
self.prev = prev
|
||
|
|
if self.prev is None:
|
||
|
|
self.prev = self
|
||
|
|
if lock:
|
||
|
|
self.this.locked = True
|
||
|
|
|
||
|
|
def add(self, other: Self):
|
||
|
|
self.this.add(other.this)
|
||
|
|
for i in range(globalSettings['max_from_depth']):
|
||
|
|
self.froms[i].add(other.froms[i])
|
||
|
|
|
||
|
|
def step(self, scope):
|
||
|
|
return scopesContext(
|
||
|
|
this=scope,
|
||
|
|
root=self.root,
|
||
|
|
froms=self.froms,
|
||
|
|
prev=self,
|
||
|
|
)
|
||
|
|
|
||
|
|
def toFrom(self, count):
|
||
|
|
froms = copy(self.froms)
|
||
|
|
for i in range(count):
|
||
|
|
if len(froms) < 1:
|
||
|
|
maxFromDepth = globalSettings['max_from_depth']
|
||
|
|
raise ShallowFromsException(
|
||
|
|
f'Max from depth too low; set to {maxFromDepth}, tried to access from depth {maxFromDepth + count - i}')
|
||
|
|
result = froms.pop(0)
|
||
|
|
self.this = result
|
||
|
|
self.froms = froms
|
||
|
|
|
||
|
|
def setThisKey(self, key):
|
||
|
|
self.this = scopeSet(key)
|
||
|
|
|
||
|
|
def link(self, commands: list[str] | str):
|
||
|
|
'''takes a string such as "prev.owner" and returns a scopesContext correspondinf to where that string would lead to.'''
|
||
|
|
if isinstance(commands, str):
|
||
|
|
commands = decomposeChain(commands)
|
||
|
|
context = copy(self)
|
||
|
|
scopeTransitions = globalSettings['scopeTransitions']
|
||
|
|
for command in commands:
|
||
|
|
command = command.lower()
|
||
|
|
if command == 'root':
|
||
|
|
context = copy(context.root)
|
||
|
|
elif command == 'prev':
|
||
|
|
context = copy(context.prev)
|
||
|
|
elif command == 'prevprev':
|
||
|
|
context = copy(context.prev.prev)
|
||
|
|
elif command == 'prevprevprev':
|
||
|
|
context = copy(context.prev.prev.prev)
|
||
|
|
elif command == 'prevprevprevprev':
|
||
|
|
context = copy(context.prev.prev.prev.prev)
|
||
|
|
elif command == 'from':
|
||
|
|
context.toFrom(1)
|
||
|
|
elif command == 'fromfrom':
|
||
|
|
context.toFrom(2)
|
||
|
|
elif command == 'fromfromfrom':
|
||
|
|
context.toFrom(3)
|
||
|
|
elif command == 'fromfromfromfrom':
|
||
|
|
context.toFrom(4)
|
||
|
|
elif command in scopeTransitions:
|
||
|
|
context.setThisKey(scopeTransitions[command])
|
||
|
|
elif command == 'target':
|
||
|
|
context.setThisKey('[TARGET]')
|
||
|
|
elif command.startswith('event_target:'):
|
||
|
|
key = command.split(':')[1]
|
||
|
|
context.this = eventTarget(key)
|
||
|
|
elif command.startswith('parameter:'):
|
||
|
|
context.setThisKey('country')
|
||
|
|
context.prev = self
|
||
|
|
return context
|
||
|
|
|
||
|
|
def firedContext(self):
|
||
|
|
froms = [self.root.this] + copy(self.root.froms)
|
||
|
|
froms.pop()
|
||
|
|
return scopesContext(self.this, froms=froms)
|
||
|
|
|
||
|
|
def unpackThis(self):
|
||
|
|
'''Returns a list of strings representing the possible "this" scopes for this context.
|
||
|
|
Possible values for Stellaris:
|
||
|
|
- "none" (for e.g. effects directly under an on_game_start event's "immediately" block)
|
||
|
|
- "country"
|
||
|
|
- "pop_faction"
|
||
|
|
- "first_contact"
|
||
|
|
- "situation"
|
||
|
|
- "agreement"
|
||
|
|
- "federation"
|
||
|
|
- "archaeological_site"
|
||
|
|
- "spy_network"
|
||
|
|
- "espionage_asset"
|
||
|
|
- "megastructure"
|
||
|
|
- "planet"
|
||
|
|
- "ship"
|
||
|
|
- "pop"
|
||
|
|
- "fleet"
|
||
|
|
- "galactic_object"
|
||
|
|
- "leader"
|
||
|
|
- "army"
|
||
|
|
- "ambient_object"
|
||
|
|
- "species"
|
||
|
|
- "design"
|
||
|
|
- "war"
|
||
|
|
- "alliance"
|
||
|
|
- "starbase"
|
||
|
|
- "deposit"
|
||
|
|
- "observer"
|
||
|
|
- "sector"
|
||
|
|
- "astral_rift"
|
||
|
|
- "espionage_operation"
|
||
|
|
- "design"
|
||
|
|
- "cosmic_storm"
|
||
|
|
plus the following strings corresponding to contexts where I found figuring out the appropriate scopes automatically was too complicated:
|
||
|
|
- "[EFFECT_BUTTON_SCOPE]"
|
||
|
|
- "[TARGET]" (for anything selected for being the target of an espionage operation, situation, spy network, or agreement)
|
||
|
|
- "[COUNTRY_CREATION_ANTECEDENT]"
|
||
|
|
- "[SOLAR_SYSTEM_INITIALIZER_ANTECEDENT]"
|
||
|
|
'''
|
||
|
|
return self.this.unpack()
|
||
|
|
|
||
|
|
def __repr__(self):
|
||
|
|
return str(self.this.unpack())
|
||
|
|
|
||
|
|
def __str__(self):
|
||
|
|
if self.prev is None:
|
||
|
|
prevtext = "None"
|
||
|
|
else:
|
||
|
|
prevtext = str(self.prev.unpackThis())
|
||
|
|
fromtext = []
|
||
|
|
for f in self.froms:
|
||
|
|
fromtext.append(str(f.unpack()))
|
||
|
|
else:
|
||
|
|
return f"{self.root.unpackThis()}->{self.unpackThis()} Prev: {prevtext} From: {''.join(fromtext)}"
|
||
|
|
|
||
|
|
|
||
|
|
def findEffectScopes(criteria: Callable[[CWElement], bool], mods: Iterator[Mod] = registered_mods.values()) -> list[
|
||
|
|
tuple[CWElement, scopesContext]]:
|
||
|
|
'''Scans all effect blocks (except, currently, those in queue_actions blocks) and returns a list of (CWElement,scopesContext) tuples corresponding to effects.
|
||
|
|
Note: this function requires game-specific configuration. Stellaris configuration can be set up by importing and running cwp_stellaris.configureCWP().
|
||
|
|
parameters:
|
||
|
|
criteria: A callable taking a CWElement and returning a boolean. Effects will be included in the returned list where criteria(effect) return True.
|
||
|
|
mods (optional): An iterator returning mods. This function will scan all mods returned by this iterator. Default is cw_parser.registered_mods.values(). When the iterator returns a mod where is_base=True (for example, when cw_parser.registered_mods.values() returns the vanilla mod object), this function will also scan all mods where is_base=True.'''
|
||
|
|
configure('expand_inlines', True)
|
||
|
|
configure('replace_local_variables', True)
|
||
|
|
for mod in mods:
|
||
|
|
mod.activate()
|
||
|
|
print(f'loading metascripts for mod {str(mod)}')
|
||
|
|
mod.load_metascripts()
|
||
|
|
print(f'loading events for mod {str(mod)}')
|
||
|
|
mod.load_events()
|
||
|
|
print(f'loading on_actions for mod {str(mod)}')
|
||
|
|
mod.load_on_actions()
|
||
|
|
|
||
|
|
print('applying hardcoded on_actions')
|
||
|
|
hardcodedOnActions = globalSettings['hardcodedOnActions']
|
||
|
|
for on_action in hardcodedOnActions:
|
||
|
|
onActionScopes(on_action).add(hardcodedOnActions[on_action])
|
||
|
|
|
||
|
|
for mod in mods:
|
||
|
|
mod.activate()
|
||
|
|
globalSettings['effectParseAdditionalSetup'](mod)
|
||
|
|
print(f'activating on_actions for mod {str(mod)}')
|
||
|
|
mod.activate_on_actions()
|
||
|
|
|
||
|
|
effectLocations = globalSettings['effectLocations']
|
||
|
|
output_effects = []
|
||
|
|
for mod in mods:
|
||
|
|
mod.activate()
|
||
|
|
for folder in effectLocations:
|
||
|
|
print(f'processing mod "{str(mod)}" folder "{folder}"')
|
||
|
|
for (effectBlock, scopes) in mod.read_folder(folder).navigateByDict(effectLocations[folder]):
|
||
|
|
for effect in effectBlock.effectScopingRun(scopes, criteria):
|
||
|
|
output_effects.append(effect)
|
||
|
|
print(f'processing mod "{str(mod)}" events')
|
||
|
|
for event in mod.events():
|
||
|
|
if event.hasSubelements():
|
||
|
|
scopes = globalData['eventScopes'][event.getValue('id', resolve=True)]
|
||
|
|
for element in event.contents():
|
||
|
|
if element.name in ['immediate', 'option', 'after', 'abort_effect']:
|
||
|
|
for effect in element.effectScopingRun(scopes, criteria):
|
||
|
|
output_effects.append(effect)
|
||
|
|
elif event.name == 'namespace':
|
||
|
|
print(f'namespace {event.value}')
|
||
|
|
for (effectBlock, scopes) in globalSettings['additionalEffectBlocks'](mod):
|
||
|
|
for effect in effectBlock.effectScopingRun(scopes, criteria):
|
||
|
|
output_effects.append(effect)
|
||
|
|
|
||
|
|
return output_effects
|