Module objectionpy.objection
Main module containing everything related to objection exporting, importing, and structure (except for frame-related components).
Expand source code
"""Main module containing everything related to objection exporting, importing, and structure (except for frame-related components)."""
from copy import deepcopy
from dataclasses import dataclass, field
from functools import cache
from json import loads, dumps
from base64 import b64decode, b64encode
from warnings import warn
from typing import Any, Optional, Sized, Union, TypeVar, TYPE_CHECKING
from . import enums, _utils, frames, assets, preset
from ._version import __version__
if TYPE_CHECKING:
from enum import EnumMeta
EnumT = TypeVar('EnumT')
LATEST_OBJECTION_VERSION = 4
Frame = frames.Frame
@dataclass
class Group:
"""
Container for frames used in Cases.
Attributes:
- `objection : Optional[_ObjectionBase]`
- The case to automatically append to on the group's initialization.
- `name : str`
- Group name.
- `caseTag : str`
- A unique tag used to identify the group in case actions. (A direct reference to the group object works too)
- `frames : list[Frame]`
- The group's frame list. May be of type Frame or CEFrame.
"""
_type: enums.GroupType = field(default=enums.GroupType.NORMAL, init=False)
objection: Optional['_ObjectionBase'] = None
name: Optional[str] = None
caseTag: Optional[str] = None
frames: list[Frame] = field(default_factory=list)
def __post_init__(self):
try:
self.objection.groups.append(self) # type: ignore
except AttributeError:
pass
@dataclass
class CEGroup(Group):
"""
Special container for cross-examination sequences.
Can contain both Frame and CEFrame.
Attributes:
- `objection : Optional[_ObjectionBase]`
- The case to automatically append to on the group's initialization.
- `name : str`
- Group name.
- `caseTag : str`
- A unique tag used to identify the group in case actions. (A direct reference to the group object works too)
- `frames : list[Frame]`
- The group's frame list. May be of type Frame or CEFrame.
- `counselSequence : list[Frame]`
- A sequence of frames played after the last statement of the cross-examination, before looping back to the first.
- `failureSequence : list[Frame]`
- A sequence of frames played when failing to present a correct contradiction.
"""
_type: enums.GroupType = field(
default=enums.GroupType.CE, init=False)
counselSequence: list[Frame] = field(default_factory=list, init=False)
failureSequence: list[Frame] = field(default_factory=list, init=False)
class GameOverGroup(Group):
"""
Special container whose frames are played when the player's health reaches 0.
Attributes:
- `objection : Optional[_ObjectionBase]`
- The case to automatically append to on the group's initialization.
- `name : str`
- Group name.
- `caseTag : str`
- A unique tag used to identify the group in case actions. (A direct reference to the group object works too)
- `frames : list[Frame]`
- The group's frame list. May be of type Frame or CEFrame.
"""
_type = enums.GroupType.GAME_OVER
@dataclass
class Options:
"""
The default options of an objection.
Most can be modified with OptionModifiers in Frames.
"""
dialogueBox: enums.PresetDialogueBox = enums.PresetDialogueBox.CLASSIC
defaultTextSpeed: int = 28
blipFrequency: int = 56
autoplaySpeed: int = 500
continueSoundUrl: str = "/Audio/Case/Continue_Trilogy.wav"
MAX_PAIRS: int = 100
MAX_GROUPS: int = 100
MAX_ALIASES: int = 100
MAX_GROUP_FRAMES: int = 1000
MAX_FRAME_ACTIONS: int = 10
MAX_EVIDENCE: int = 50
MAX_PROFILES: int = 50
class _ObjectionBase:
"""
Base objection class.
Should not be initialized. Use Scene or Case instead.
Attributes:
- `options : Options`
- Default objection options.
- `aliases : dict[str, str]`
- Dictionary of aliases, mapping original name to alias.
"""
_type: enums.ObjectionType
options: Options
aliases: dict[str, str]
_groups: list[Group]
_nextFrameIID: int
def __init__(self, options: Optional[Options] = None) -> None:
self.options = options if options is not None else Options()
self.aliases = {}
self._groups = []
@classmethod
def _verifyFrameChar(cls, char: Optional[frames.FrameCharacter]) -> frames.FrameCharacter:
return char if char is not None else frames.noneCharacter
def _compileFrame(self, frame: Frame, frameList: list[Frame]):
try:
_utils._tupleMapGet(self._frameMap, frame)
frame = deepcopy(frame) # Makes a copy if map get didn't fail
except KeyError:
pass
chars = (
self._verifyFrameChar(frame.char),
self._verifyFrameChar(frame.pairChar),
)
activeIndex = _utils._maxIndex(
[*map(lambda char: char._getIndividualValue(char.isActive), chars)])
activeChar = chars[activeIndex]
secondaryChar = chars[1 - activeIndex]
frontChar = chars[_utils._maxIndex(
[*map(lambda char: char._getIndividualValue(char.isFront) if not char.isNone else -2, chars)])]
secondaryFlip = '1' if secondaryChar.flip else '0'
activeFlip = '1' if activeChar.flip else '0'
if frame.backgroundFlip is None:
backgroundFlip = '0' if not secondaryChar.isNone else activeFlip
else:
backgroundFlip = '1' if frame.backgroundFlip else '0'
flip = backgroundFlip + activeFlip + secondaryFlip
frameDict = {
"id": -1,
"iid": self._nextFrameIID,
"text": frame.text,
"characterId": activeChar.character.id if not activeChar.character.isPreset else None,
"poseId": activeChar.poseId,
"pairPoseId": secondaryChar.poseId,
"bubbleType": frame.bubble if frame.bubble is not None else 0,
"username": frame.customName if frame.customName is not None else "",
"mergeNext": frame.merge,
"doNotTalk": not frame.talk,
"goNext": frame.goNext,
"poseAnimation": frame.poseAnim,
"flipped": flip,
"popupId": frame.popup if frame.popup is not None else None,
"backgroundId": frame.background.id if frame.background is not None else activeChar.character.backgroundId,
"transition": {},
"filter": {},
"frameFades": [],
"frameActions": [],
"caseAction": {},
"pairId": None,
}
if frame.hidden:
frameDict['hide'] = True
if frame.caseTag:
if (frame.caseTag in self._frameTags):
raise ObjectionError('Duplicate frame tag "' + frame.caseTag + '"')
self._frameTags[frame.caseTag] = frameDict
self._nextFrameIID += 1
if frame.transition:
frameDict['transition']['duration'] = frame.transition.duration
frameDict['transition']['easing'] = frame.transition.easing.value
frameDict['transition']['left'] = 0
if frame.wideX is not None:
frameDict['transition']['left'] = int(frame.wideX * 100)
if frame.filter:
frameDict['filter'] = {
"id": 0,
"type": frame.filter.type.value,
"target": frame.filter.target.value,
"amount": frame.filter.amount,
}
if frame.fade:
frameDict['frameFades'].append({
"id": 0,
"fade": frame.fade.direction.value,
"target": frame.fade.target.value,
"easing": frame.fade.easing.value,
"color": str(frame.fade.color),
"duration": frame.fade.duration,
})
if frame.offScreen:
frameDict['frameActions'].append({
'actionId': 6,
})
if frame.centerText:
frameDict['frameActions'].append({
'actionId': 9,
})
presetPopup = frame.presetPopup
if presetPopup is not None:
if presetPopup.value > 0:
frameDict['frameActions'].append({
'actionId': 7,
'actionParam': str(presetPopup.value),
})
elif presetPopup == enums.PresetPopup.TESTIMONY_LABEL_HIDE:
frameDict['frameActions'].append({
'actionId': 8,
'actionParam': '1',
})
presetBlip = frame.presetBlip
if presetBlip is not None:
if presetBlip.value > 0:
frameDict['frameActions'].append({
'actionId': 4,
'actionParam': str(presetBlip.value),
})
elif presetBlip == enums.PresetBlip.MUTE:
frameDict['frameActions'].append({
'actionId': 5,
})
if frame.options.autoplaySpeed is not None:
frameDict['frameActions'].append({
'actionId': 15,
'actionParam': str(frame.options.autoplaySpeed),
})
if frame.options.dialogueBox is not None:
frameDict['frameActions'].append({
'actionId': 12,
'actionParam': str(frame.options.dialogueBox.value),
})
if frame.options.dialogueBoxVisible is not None:
frameDict['frameActions'].append({
'actionId': 1,
'actionParam': str(int(frame.options.dialogueBoxVisible)),
})
if frame.options.defaultTextSpeed is not None:
frameDict['frameActions'].append({
'actionId': 13,
'actionParam': str(frame.options.defaultTextSpeed),
})
if frame.options.blipFrequency is not None:
frameDict['frameActions'].append({
'actionId': 14,
'actionParam': str(frame.options.blipFrequency),
})
if frame.options.frameSkip is not None:
frameDict['frameActions'].append({
'actionId': 16,
'actionParam': str(int(frame.options.frameSkip)),
})
if frame.options.galleryRemove is not None:
# TODO make it choose dynamically between action 3 (PW) and 11 (AJ) galleries
for location in frame.options.galleryRemove:
frameDict['frameActions'].append({
'actionId': 3,
'actionParam': location.value,
})
frameDict['frameActions'].append({
'actionId': 11,
'actionParam': location.value,
})
galleryModifier = frame.options.galleryAssign
if galleryModifier is not None:
for character in galleryModifier.__dict__.values():
if character is None:
continue
if character.isPreset:
actionId = 2 if not character._aj else 10
frameDict['frameActions'].append({
'actionId': actionId,
'actionParam': str(character.id),
})
else:
pass # TODO make it create new frames before current frame to set custom characters gallery sprites
if not secondaryChar.isNone or activeChar.pairOffset != (0, 0):
pairNeeded = True
else:
pairNeeded = False
if pairNeeded:
pairChars = [activeChar, secondaryChar]
pairChars.sort(
key=lambda char: char.character.id if char.character.id is not None else 0, reverse=True)
pair = self._requestPair(pairChars[0].character.id, pairChars[1].character.id, pairChars[0].pairOffset,
pairChars[1].pairOffset, True if chars[0] is frontChar else False)
frameDict['pairId'] = pair['pairId']
self._frameMap.append((frame, frameDict))
if frame.onCompile is not None:
frameDict = frame.onCompile(frameDict)
LimitWarning.checkList(frameDict['frameActions'], self.options.MAX_FRAME_ACTIONS,
'frame actions (frame iid=' + str(frameDict['iid']) + ')')
return frameDict
def compile(self) -> dict:
"""
Compile objection.
Raises:
- `ObjectionError`
- Duplicate case tag was found.
- CEFrame was found in the wrong group.
Returns:
JSON-serializable dictionary in the .objection format.
"""
objectionDict = {
'credit': 'made with objection.py v' + __version__,
'version': LATEST_OBJECTION_VERSION,
'pairs': [],
'groups': [],
'courtRecord': {
'evidence': [],
'profiles': [],
},
'aliases': [],
'options': {},
'type': 'scene' if self._type is enums.ObjectionType.SCENE else 'case' if self._type is enums.ObjectionType.CASE else 'unknown',
}
objectionDict['options']['chatbox'] = self.options.dialogueBox.value
objectionDict['options']['textSpeed'] = self.options.defaultTextSpeed
objectionDict['options']['textBlipFrequency'] = self.options.blipFrequency
objectionDict['options']['autoplaySpeed'] = self.options.autoplaySpeed
objectionDict['options']['continueSoundUrl'] = self.options.continueSoundUrl
for i, alias in enumerate(self.aliases.items()):
objectionDict['aliases'].append({
"id": i,
"from": alias[0],
"to": alias[1],
})
LimitWarning.checkList(
objectionDict['aliases'], self.options.MAX_ALIASES, 'aliases')
nextPairID = 1
@cache
def requestPair(cid1: int, cid2: int, offset1: tuple[int, int], offset2: tuple[int, int], front: bool):
nonlocal nextPairID
pair = {
'id': 0,
'pairId': nextPairID,
'name': 'Generated ' + str(nextPairID),
'characterId': cid1,
'characterId2': cid2,
'offsetX': offset1[0],
'offsetY': offset1[1],
'offsetX2': offset2[0],
'offsetY2': offset2[1],
'front': front,
}
objectionDict['pairs'].append(pair)
nextPairID += 1
return pair
self._requestPair = requestPair
self._nextFrameIID = 1
self._nextGeneratedGroupName = 1
self._groupMap = []
self._frameMap = []
self._groupTags = {}
self._frameTags = {}
for i, group in enumerate(self._groups):
name = group.name
if not name:
name = 'Generated ' + str(self._nextGeneratedGroupName)
self._nextGeneratedGroupName += 1
groupDict = {
"iid": i + 1,
"name": name,
"type": group._type.value,
"frames": [],
}
if (group.caseTag):
if (group.caseTag in self._groupTags):
raise ObjectionError(
'Duplicate group tag "' + group.caseTag + '"')
self._groupTags[group.caseTag] = groupDict
for frame in group.frames:
if isinstance(frame, frames.CEFrame) and not isinstance(group, CEGroup):
raise ObjectionError('CEFrame found in non-CE group')
frameDict = self._compileFrame(frame, frameList=groupDict['frames'])
groupDict['frames'].append(frameDict)
if isinstance(group, CEGroup):
if len(group.counselSequence) > 0:
groupDict["counselFrames"] = []
for frame in group.counselSequence:
if isinstance(frame, frames.CEFrame):
raise ObjectionError('CEFrame found within counsel sequence')
groupDict["counselFrames"].append(
self._compileFrame(frame, frameList=groupDict["counselFrames"])
)
if len(group.failureSequence) > 0:
groupDict["failureFrames"] = []
for frame in group.failureSequence:
if isinstance(frame, frames.CEFrame):
raise ObjectionError('CEFrame found within failure sequence')
groupDict["failureFrames"].append(
self._compileFrame(frame, frameList=groupDict["failureFrames"])
)
self._groupMap.append((group, groupDict))
objectionDict['groups'].append(groupDict)
LimitWarning.checkList(groupDict['frames'], self.options.MAX_GROUP_FRAMES,
'frames in a group (group iid=' + str(groupDict['iid']) + ')')
LimitWarning.checkList(
objectionDict['groups'], self.options.MAX_GROUPS, 'groups')
LimitWarning.checkList(
objectionDict['pairs'], self.options.MAX_PAIRS, 'pairs')
return objectionDict
@classmethod
def makeObjectionFile(cls, objectionDict: dict) -> str:
return b64encode(bytes(dumps(objectionDict), encoding='utf-8')).decode('utf-8')
class Scene(_ObjectionBase):
"""
Objection scene - a linear series of frames that can be recorded or played in a browser.
Primarily modified by editing frames in Scene.frames.
Attributes:
- `options : Options`
- Default objection options.
- `aliases : dict[str, str]`
- Dictionary of aliases, mapping original name to alias.
- `frames : list[Frames]`
- The scene's frame list. Primary way of modifying scenes.
"""
type = enums.ObjectionType.SCENE
def __init__(self, options: Optional[Options] = None) -> None:
super().__init__(options)
mainGroup = Group(name='Main')
self._groups.append(mainGroup)
@property
def frames(self) -> list[Frame]:
return self._groups[0].frames
def compile(self) -> dict:
objectionDict = super().compile()
compiledFrames = self._groupMap[0][1]['frames']
for frameDict in [*compiledFrames]:
if frameDict.get('hide'):
compiledFrames.remove(frameDict)
return objectionDict
class Case(_ObjectionBase):
"""
Objection case - interactive game playable only on a browser, with certain extra features.
Attributes:
- `options : Options`
- Default objection options.
- `aliases : dict[str, str]`
- Dictionary of aliases, mapping original name to alias.
- `evidence : list[RecordItem]`
- `profiles : list[RecordItem]`
- `groups : list[Group]`
- The case's group list. Primary way of modifying cases.
"""
type = enums.ObjectionType.CASE
@dataclass
class RecordItem:
type: enums.RecordType
name: str
iconUrl: str
checkUrl: str = ''
description: str = ''
hidden: bool = False
def _getIid(self, objMap: list[tuple]) -> str:
recordObject = _utils._tupleMapGet(objMap, self)
prefix: str
if (self.type is enums.RecordType.EVIDENCE):
prefix = 'e-'
elif (self.type is enums.RecordType.PROFILE):
prefix = 'p-'
else:
raise TypeError('Unknown record item type ' + self.type.name)
return prefix + str(recordObject["iid"])
evidence: list[RecordItem]
profiles: list[RecordItem]
def __init__(self, options: Optional[Options] = None) -> None:
super().__init__(options)
self.evidence = []
self.profiles = []
@property
def groups(self) -> list[Group]:
return self._groups
def _getByTagOrObj(
self,
identifier: Union[str, Any],
objMap: list[tuple],
tagMap: dict,
errorText: str
) -> dict:
if (type(identifier) is str):
return tagMap[identifier]
else:
try:
return _utils._tupleMapGet(objMap, identifier)
except KeyError:
raise KeyError(errorText)
def _getFrameDict(self, frameParam: Union[str, Frame]) -> dict:
return self._getByTagOrObj(frameParam, objMap=self._frameMap, tagMap=self._frameTags, errorText='Parsed frame object wasn\' found')
def _getGroupDict(self, groupParam: Union[str, Group]) -> dict:
return self._getByTagOrObj(groupParam, objMap=self._groupMap, tagMap=self._groupTags, errorText='Parsed group object wasn\' found')
def _post_process_frame(self, processedList: list, frame: Frame, frameDict: dict):
if frame in processedList:
return
processedList.append(frame)
if isinstance(frame, frames.CEFrame):
if len(frame.pressSequence) > 0:
frameDict["pressFrames"] = []
for pressFrame in frame.pressSequence:
if isinstance(pressFrame, frames.CEFrame):
raise ObjectionError('CEFrame found within press sequence')
frameDict["pressFrames"].append(
self._compileFrame(pressFrame, frameList=frameDict["pressFrames"])
)
if len(frame.contradictions) > 0:
frameDict["contradictions"] = []
for recordItem, frameParam in frame.contradictions:
frameDict["contradictions"].append({
"eid": recordItem._getIid(self._recordMap),
"fid": str(self._getFrameDict(frameParam)["iid"]),
})
action = frame.caseAction
if action is None:
return
actionId: int = -1
actionValue: Any = None
if isinstance(action, frames.CaseActions.ToggleEvidence):
actionId = 16
actionValue = {
"show": [],
"hide": [],
}
item: Case.RecordItem
for item in action.show:
actionValue["show"].append(item._getIid(self._recordMap))
for item in action.hide:
actionValue["hide"].append(item._getIid(self._recordMap))
elif isinstance(action, frames.CaseActions.ToggleFrames):
actionId = 3
actionValue = {
"show": "",
"hide": "",
}
for frameParam in action.show:
targetframeDict: dict = self._getFrameDict(frameParam)
actionValue["show"] += str(targetframeDict["iid"]) + ' '
for frameParam in action.hide:
targetframeDict: dict = self._getFrameDict(frameParam)
actionValue["hide"] += str(targetframeDict["iid"]) + ' '
for key in ("show", "hide"):
if (len(actionValue[key]) > 0):
actionValue[key] = actionValue[key].strip()
elif isinstance(action, frames.CaseActions.GoToFrame):
actionId = 4
actionValue = str(self._getFrameDict(action.frame)["iid"])
elif isinstance(action, frames.CaseActions.SetGameOverGroup):
actionId = 15
actionValue = str(self._getGroupDict(action.group)["iid"])
elif isinstance(action, frames.CaseActions.EndGame):
actionId = 5
elif isinstance(action, (frames.CaseActions.HealthSet, frames.CaseActions.HealthAdd, frames.CaseActions.HealthRemove)):
actionId = 6
actionValue = {
"amount": int(action.amount * 100)
}
if isinstance(action, frames.CaseActions.HealthSet):
actionValue["type"] = 0
elif isinstance(action, frames.CaseActions.HealthAdd):
actionValue["type"] = 1
elif isinstance(action, frames.CaseActions.HealthRemove):
actionValue["type"] = 2
elif isinstance(action, frames.CaseActions.FlashingHealth):
actionId = 7
actionValue = str(int(action.amount * 100))
elif isinstance(action, frames.CaseActions.PromptPresent):
actionId = 8
actionValue = {
"evidence": action.presentEvidence,
"profiles": action.presentProfiles,
"falseFid": str(self._getFrameDict(action.failFrame)["iid"]),
"items": [],
}
for recordItem, frameParam in action.choices:
actionValue["items"].append({
"eid": recordItem._getIid(self._recordMap),
"fid": str(self._getFrameDict(frameParam)["iid"]),
})
elif isinstance(action, frames.CaseActions.PromptChoice):
actionId = 9
actionValue = []
for choiceText, frameParam in action.choices:
actionValue.append({
"text": choiceText,
"fid": str(self._getFrameDict(frameParam)["iid"]),
})
elif isinstance(action, (frames.CaseActions.PromptInt, frames.CaseActions.PromptStr)):
actionId = 12
actionValue = {
"name": action.varName,
"type": "int",
}
if isinstance(action, frames.CaseActions.PromptStr):
actionValue["lowercase"] = action.toLower
actionValue["type"] = "string"
if not action.allowSpaces:
actionValue["type"] = "word"
elif isinstance(action, frames.CaseActions.PromptCursor):
actionId = 17
actionValue = {
"imageUrl": action.previewImageUrl,
"prompt": action.prompt,
"color": str(action.cursorColor),
"falseFid": str(self._getFrameDict(action.failFrame)["iid"]),
"areas": []
}
for cursorRect, frameParam in action.choices:
actionValue["areas"].append({
"fid": str(self._getFrameDict(frameParam)["iid"]),
"shape": {
"left": cursorRect.left,
"top": cursorRect.top,
"width": cursorRect.width,
"height": cursorRect.height,
},
})
elif isinstance(action, frames.CaseActions.VarSet):
actionId = 10
actionValue = {
"name": action.varName,
"value": action.value,
}
elif isinstance(action, frames.CaseActions.VarAdd):
actionId = 11
actionValue = {
"name": action.varName,
"value": str(action.value),
}
elif isinstance(action, frames.CaseActions.VarEval):
actionId = 14
actionValue = {
"expression": action.expression,
"trueFid": str(self._getFrameDict(action.trueFrame)["iid"]),
"falseFid": str(self._getFrameDict(action.falseFrame)["iid"]),
}
if actionId == -1:
raise TypeError('unknown case action')
actionObject = {
"id": actionId,
"value": actionValue,
}
frameDict['caseAction'] = actionObject
def compile(self) -> dict:
self._recordMap = []
courtRecord = {
'evidence': [],
'profiles': [],
}
for items, recordKey in (self.evidence, 'evidence'), (self.profiles, 'profiles'):
for i, item in enumerate(items):
recordObject = {
"iid": i + 1,
"name": item.name,
"iconUrl": item.iconUrl,
"url": item.checkUrl,
"description": item.description,
"hide": item.hidden,
}
courtRecord[recordKey].append(recordObject)
self._recordMap.append((item, recordObject))
LimitWarning.checkList(
courtRecord['evidence'], self.options.MAX_EVIDENCE, 'evidence')
LimitWarning.checkList(
courtRecord['profiles'], self.options.MAX_PROFILES, 'profiles')
objectionDict = super().compile()
objectionDict['courtRecord'] = courtRecord
frame: Frame
frameDict: dict
processedList = []
for i in range(2): # Looping twice to process newly-generated press frames too
for frame, frameDict in [*self._frameMap]:
self._post_process_frame(processedList, frame, frameDict)
return objectionDict
class LimitWarning(Warning):
"""
Displayed when the limit of an individual objection component is exceeded.
By default, they match the limits of the objection.lol GUI, but can be modified in the objection's Options."""
@classmethod
def warn(cls, limit: int, limitTarget: str, value: int):
warn(
f'exceeded limit of {limit} {limitTarget} (at {value}) - objection.lol support is not guaranteed', LimitWarning)
@classmethod
def checkList(cls, lst: Sized, limit: Optional[int], limitTarget: str):
if limit is not None and len(lst) > limit:
cls.warn(limit, limitTarget, len(lst))
class IOWarning(Warning):
"""Warning for the use cases of IOError."""
pass
class ObjectionError(Exception):
"""Error of unspecified type during objection compilation."""
pass
def _checkPresetId(characterId: int) -> Optional[int]:
if characterId >= 1000:
return characterId
return None
def _getEnumByValue(enum: 'EnumMeta', value: Any, enumType: 'EnumT') -> 'EnumT':
if value is None: return value
for member in enum: # type: ignore
if member.value == value: # type: ignore
return member # type: ignore
raise KeyError(f'Value "{value}" not found in {enum}')
def _getFrameDictAction(frameDict: dict, actionId: int, suppressWarnings: bool) -> Any:
actions = frameDict.get('frameActions')
if actions:
param: Any = None
found = False
for action in actions:
if action['actionId'] == actionId:
if found:
if suppressWarnings: warn(f"Duplicate case action of id {actionId} found at frame {frameDict['iid']} (\"{frameDict['text']}\")", IOWarning)
continue
param = action.get('actionParam')
if (param is None):
param = '' # A random placeholder
found = True
return param
def _loadJSONFrame(frameDict: dict, frameClass: type, pairList: list, frameMap: list, frameIIDs: dict, suppressWarnings: bool) -> Frame:
pair: Optional[dict] = None
pairedIs1: bool = False
for pairDict in pairList:
if pairDict['pairId'] == frameDict['pairId']:
pair = pairDict
pairedCharIs1 = frameDict['characterId'] == _checkPresetId(pairDict['characterId2'])
break
flipString = frameDict['flipped'] if frameDict['flipped'] else '000'
charAsset = assets.Character(0)
if frameDict['characterId'] is None:
for char in preset.collectionValues(preset.Characters):
for pose in char.poses:
if pose['id'] == frameDict['poseId']:
charAsset = char
break
else:
charAsset = assets.Character(frameDict['characterId'], _loaded=(frameDict['characterId'] is None))
char = frames.FrameCharacter(
character=charAsset,
poseId=frameDict['poseId'],
flip=bool(int(flipString[1])),
isActive=True,
)
pairChar = None
if pair:
char.pairOffset = (pair['offsetX'], pair['offsetY']) if pairedIs1 else (pair['offsetX2'], pair['offsetY2'])
char.isFront = pair['front'] if pairedIs1 else not pair['front']
pairCharId = pair['characterId'] if frameDict['characterId'] == _checkPresetId(pair['characterId2']) else pair['characterId2']
pairCharAsset = assets.Character(0)
if pairCharId >= 1000:
pairCharAsset = assets.Character(pairCharId)
else:
for char in preset.collectionValues(preset.Characters):
if char.id == pairCharId:
charAsset = char
break
pairChar = frames.FrameCharacter(
character=pairCharAsset,
poseId=frameDict['pairPoseId'],
flip=bool(int(flipString[2])),
isActive=False,
pairOffset=(pair['offsetX2'], pair['offsetY2']) if pairedIs1 else (pair['offsetX'], pair['offsetY']),
isFront=not pair['front'] if pairedIs1 else pair['front'],
)
frame: Frame = frameClass(
char=char,
pairChar=pairChar,
text=frameDict['text'],
customName=frameDict['username'],
bubble=frameDict['bubbleType'],
background=assets.Background(frameDict['backgroundId']) if frameDict['backgroundId'] else None,
backgroundFlip=bool(int(flipString[0])),
wideX=frameDict['transition']['left']/100 if frameDict['transition'] and 'left' in frameDict['transition'] else None,
popup=assets.Popup(frameDict['popupId']) if frameDict['popupId'] else None,
talk=not frameDict['doNotTalk'],
poseAnim=frameDict['poseAnimation'],
goNext=frameDict['goNext'],
merge=frameDict['mergeNext'],
offScreen=_getFrameDictAction(frameDict, 6, suppressWarnings) is not None,
centerText=_getFrameDictAction(frameDict, 9, suppressWarnings) is not None,
presetBlip=_getEnumByValue(enums.PresetBlip, int(_getFrameDictAction(frameDict, 4, suppressWarnings)), enums.PresetBlip.KATONK) if _getFrameDictAction(frameDict, 4, suppressWarnings) else None,
presetPopup=_getEnumByValue(enums.PresetPopup, int(_getFrameDictAction(frameDict, 7, suppressWarnings)), enums.PresetPopup.CROSS_EXAMINATION) if _getFrameDictAction(frameDict, 7, suppressWarnings) else None,
fade=frames.Fade(
direction=_getEnumByValue(enums.FadeDirection, frameDict['frameFades'][0].get('direction'), enums.FadeDirection.IN),
target=_getEnumByValue(enums.FadeTarget, frameDict['frameFades'][0].get('target'), enums.FadeTarget.BACKGROUND),
duration=frameDict['frameFades'][0].get('duration'),
easing=_getEnumByValue(enums.Easing, frameDict['frameFades'][0].get('easing'), enums.Easing.EASE),
color=frames.Color(frameDict['frameFades'][0].get('color')),
) if frameDict['frameFades'] and len(frameDict['frameFades']) > 0 else None,
filter=frames.Filter(
type=_getEnumByValue(enums.FilterType, frameDict['filter'].get('type'), enums.FilterType.GRAYSCALE),
target=_getEnumByValue(enums.FilterTarget, frameDict['filter'].get('target'), enums.FilterTarget.BACKGROUND),
amount=frameDict['filter'].get('amount'),
) if frameDict['filter'] else None,
transition=frames.Transition(
duration=frameDict['transition'].get('duration'),
easing=_getEnumByValue(enums.Easing, frameDict['transition'].get('easing'), enums.Easing.EASE),
) if frameDict['transition'] else None,
options=frames.OptionModifiers(
autoplaySpeed=_getFrameDictAction(frameDict, 15, suppressWarnings),
dialogueBox=_getEnumByValue(enums.PresetDialogueBox, _getFrameDictAction(frameDict, 12, suppressWarnings), enums.PresetDialogueBox.CLASSIC),
dialogueBoxVisible=bool(int(_getFrameDictAction(frameDict, 1, suppressWarnings))) if _getFrameDictAction(frameDict, 1, suppressWarnings) else None,
defaultTextSpeed=_getFrameDictAction(frameDict, 13, suppressWarnings),
blipFrequency=_getFrameDictAction(frameDict, 14, suppressWarnings),
frameSkip=bool(int(_getFrameDictAction(frameDict, 16, suppressWarnings))) if _getFrameDictAction(frameDict, 16, suppressWarnings) else None,
)
)
if _getFrameDictAction(frameDict, 5, suppressWarnings) is not None:
if frame.presetBlip is not None:
frame.presetBlip = enums.PresetBlip.MUTE
else:
if suppressWarnings: warn(f"Conflicting speech blip Set and Mute actions at frame {frameDict['iid']} (\"{frameDict['text']}\")", IOWarning)
if _getFrameDictAction(frameDict, 8, suppressWarnings) is not None:
if frame.presetPopup is not None:
frame.presetPopup = enums.PresetPopup.TESTIMONY_LABEL_HIDE
else:
if suppressWarnings: warn(f"Unsupported combination of popup Display and Remove actions at frame {frameDict['iid']} (\"{frameDict['text']}\")", IOWarning)
for action in frameDict['frameActions']:
id, param = action['actionId'], action.get('actionParam')
if id == 2 or id == 10:
if suppressWarnings: warn(f"Importing Gallery Assign actions is not yet supported at frame {frameDict['iid']} (\"{frameDict['text']}\")", IOWarning)
elif id == 3 or id == 11:
location = _getEnumByValue(enums.CharacterLocation, param, enums.CharacterLocation.DEFENSE)
if location not in frame.options.galleryRemove:
frame.options.galleryRemove.append(location)
frameMap.append((frame, frameDict))
frameIIDs[frameDict['iid']] = frame
return frame
def loadJSONDict(objectionDict: dict, suppressWarnings: bool = False) -> Union[Scene, Case]:
"""
Load objectionpy objection from existing .objection in form of a JSON string.
Args:
- `objectionDict : dict`
- Parsed JSON dictionary of an objection.lol .objection
- `suppressWarnings : bool`
- Defaults to False.
Raises:
- `IOError`
- A JSON object's type is unknown or unsupported
Returns:
Scene or case parsed from the .objection JSON.
"""
if objectionDict['version'] != LATEST_OBJECTION_VERSION:
raise IOError(f"Objection version {objectionDict['version']} cannot be loaded. Objection.py currently supports version {LATEST_OBJECTION_VERSION}. If your autopsy- sorry, if your objection is outdated, load file into objection.lol and re-download to update its version")
objection: _ObjectionBase
if objectionDict['type'] == 'scene':
objection = Scene()
elif objectionDict['type'] == 'case':
objection = Case()
else:
raise IOError('Unknown objection type "' + str(objectionDict['type']) + '"')
dialogueBox = _getEnumByValue(enums.PresetDialogueBox, objectionDict['options']['chatbox'], enums.PresetDialogueBox.CLASSIC)
objection.options = Options(
dialogueBox=dialogueBox,
defaultTextSpeed=objectionDict['options']['textSpeed'],
blipFrequency=objectionDict['options']['textBlipFrequency'],
autoplaySpeed=objectionDict['options']['autoplaySpeed'],
continueSoundUrl=objectionDict['options']['continueSoundUrl'],
)
for alias in objectionDict['aliases']:
objection.aliases[alias['from']] = alias['to']
recordMap = {}
if type(objection) is Case:
for recordName, recordList, recordType in (('evidence', objection.evidence, enums.RecordType.EVIDENCE), ('profiles', objection.profiles, enums.RecordType.PROFILE)):
for item in objectionDict['courtRecord'][recordName]:
recordItem = Case.RecordItem(
type=recordType,
name=item.get('name'),
iconUrl=item.get('iconUrl'),
checkUrl=item.get('url'),
description=item.get('description'),
hidden=item.get('hidden'),
)
recordList.append(recordItem)
recordMap[recordItem._getIid(objMap=[(recordItem, item)])] = recordItem
frameMap = []
frameIIDs = {}
groupIIDs = {}
for groupDict in objectionDict['groups']:
group: Group
if groupDict['type'] == 'n':
group = Group()
elif groupDict['type'] == 'ce':
group = CEGroup()
elif groupDict['type'] == 'go':
group = GameOverGroup()
else:
raise IOError('Unknown group type "' + str(groupDict['type']) + '"')
objection._groups.append(group)
group.name = groupDict['name']
group.frames.append
groupIIDs[groupDict['iid']] = group
mainFrameClass = frames.Frame
if type(group) is CEGroup:
for frameDict in groupDict['counselFrames']:
frame = _loadJSONFrame(frameDict, frames.Frame, objectionDict['pairs'], frameMap, frameIIDs, suppressWarnings)
group.counselSequence.append(frame)
for frameDict in groupDict['failureFrames']:
frame = _loadJSONFrame(frameDict, frames.Frame, objectionDict['pairs'], frameMap, frameIIDs, suppressWarnings)
group.failureSequence.append(frame)
mainFrameClass = frames.CEFrame
for frameDict in groupDict['frames']:
frame = _loadJSONFrame(frameDict, mainFrameClass, objectionDict['pairs'], frameMap, frameIIDs, suppressWarnings)
group.frames.append(frame)
if type(objection) is Case:
processedList = []
frame: Frame
for i in range(2):
for frame, frameDict in frameMap:
if frame in processedList:
continue
processedList.append(frame)
if frameDict['caseAction']:
id, param = frameDict['caseAction']['id'], frameDict['caseAction']['value']
if id == 16:
frame.caseAction = frames.CaseActions.ToggleEvidence(
show=[recordMap[iid] for iid in param['show']],
hide=[recordMap[iid] for iid in param['hide']],
)
elif id == 3:
frame.caseAction = frames.CaseActions.ToggleFrames(
show=[frameIIDs[int(iid)] for iid in param['show'].split()],
hide=[frameIIDs[int(iid)] for iid in param['hide'].split()],
)
elif id == 4:
frame.caseAction = frames.CaseActions.GoToFrame(frameIIDs[int(param)])
elif id == 15:
frame.caseAction = frames.CaseActions.SetGameOverGroup(groupIIDs[int(param)])
elif id == 5:
frame.caseAction = frames.CaseActions.EndGame()
elif id == 6:
if param['type'] == 0:
frame.caseAction = frames.CaseActions.HealthSet(param['amount']/100)
elif param['type'] == 1:
frame.caseAction = frames.CaseActions.HealthAdd(param['amount']/100)
elif param['type'] == 2:
frame.caseAction = frames.CaseActions.HealthRemove(param['amount']/100)
elif id == 7:
frame.caseAction = frames.CaseActions.FlashingHealth(int(param)/100)
elif id == 8:
frame.caseAction = frames.CaseActions.PromptPresent(
failFrame=frameIIDs[int(param['falseFid'])],
presentEvidence=param['evidence'],
presentProfiles=param['profiles'],
choices=[(recordMap[item['eid']], frameIIDs[int(item['fid'])]) for item in param['items']],
)
elif id == 9:
frame.caseAction = frames.CaseActions.PromptChoice([
(choice['text'], frameIIDs[int(choice['fid'])]) for choice in param
])
elif id == 12:
if param['type'] == 'int':
frame.caseAction = frames.CaseActions.PromptInt(param['name'])
else:
frame.caseAction = frames.CaseActions.PromptStr(
varName=param['name'],
allowSpaces=param['type'] == "string",
toLower=param['lowercase'],
)
elif id == 17:
frame.caseAction = frames.CaseActions.PromptCursor(
failFrame=frameIIDs[int(param['falseFid'])],
previewImageUrl=param['imageUrl'],
prompt=param['prompt'],
cursorColor=frames.Color(param['color']),
)
for area in param['areas']:
frame.caseAction.choices.append((
frames.CursorRect(area['shape']['left'], area['shape']['top'], area['shape']['width'], area['shape']['height']),
frameIIDs[int(area['fid'])],
))
elif id == 10:
frame.caseAction = frames.CaseActions.VarSet(param['name'], param['value'])
elif id == 11:
frame.caseAction = frames.CaseActions.VarAdd(param['name'], param['value'])
elif id == 14:
frame.caseAction = frames.CaseActions.VarEval(
trueFrame=frameIIDs[int(param['trueFid'])],
falseFrame=frameIIDs[int(param['falseFid'])],
expression=param['expression'],
)
elif id == 13:
operator: str
if param['type'] == "equals":
operator = '=='
elif param['type'] == "notEquals":
operator = '!='
elif param['type'] == "greaterThan":
operator = '>'
elif param['type'] == "lessThan":
operator = '<'
else:
raise IOError(f"Unknown variable evaluation operator \"{param['type']}\" at frame {frameDict['iid']} (\"{frameDict['text']}\")")
frame.caseAction = frames.CaseActions.VarEval(
trueFrame=frameIIDs[int(param['trueFid'])],
falseFrame=frameIIDs[int(param['falseFid'])],
expression=param['name'] + operator + param['value'],
)
if type(frame) is frames.CEFrame:
if 'contradictions' in frameDict:
for contraDict in frameDict['contradictions']:
frame.contradictions.append((
recordMap[contraDict['eid']],
frameIIDs[int(contraDict['fid'])],
))
if 'pressFrames' in frameDict:
for pressDict in frameDict['pressFrames']:
pressFrame = _loadJSONFrame(pressDict, Frame, objectionDict['pairs'], frameMap, frameIIDs, suppressWarnings)
frame.pressSequence.append(pressFrame)
return objection
def loadJSONStr(objection: str, suppressWarnings: bool = False) -> Union[Scene, Case]:
"""
Load objectionpy objection from existing .objection in form of a JSON string.
Args:
- `objection : str`
- JSON string of an objection.lol .objection
- `suppressWarnings : bool`
- Defaults to False.
Raises:
- `IOError`
- A JSON object's type is unknown or unsupported
Returns:
Scene or case parsed from the .objection JSON.
"""
return loadJSONDict(loads(objection))
def loadB64(objection: str, suppressWarnings: bool = False) -> Union[Scene, Case]:
"""
Load objectionpy objection from existing .objection in form of base64-encoded JSON.
Args:
- `objection : str`
- Base64-encoded JSON string of an objection.lol .objection
- `suppressWarnings : bool`
- Defaults to False.
Raises:
- `IOError`
- A JSON object's type is unknown or unsupported
Returns:
Scene or case parsed from the .objection JSON.
"""
return loadJSONStr(b64decode(objection).decode("utf-8"))
Functions
def loadJSONDict(objectionDict: dict, suppressWarnings: bool = False) ‑> Union[Scene, Case]
-
Load objectionpy objection from existing .objection in form of a JSON string.
Args
objectionDict : dict
- Parsed JSON dictionary of an objection.lol .objection
suppressWarnings : bool
- Defaults to False.
Raises
IOError
- A JSON object's type is unknown or unsupported
Returns
Scene or case parsed from the .objection JSON.
Expand source code
def loadJSONDict(objectionDict: dict, suppressWarnings: bool = False) -> Union[Scene, Case]: """ Load objectionpy objection from existing .objection in form of a JSON string. Args: - `objectionDict : dict` - Parsed JSON dictionary of an objection.lol .objection - `suppressWarnings : bool` - Defaults to False. Raises: - `IOError` - A JSON object's type is unknown or unsupported Returns: Scene or case parsed from the .objection JSON. """ if objectionDict['version'] != LATEST_OBJECTION_VERSION: raise IOError(f"Objection version {objectionDict['version']} cannot be loaded. Objection.py currently supports version {LATEST_OBJECTION_VERSION}. If your autopsy- sorry, if your objection is outdated, load file into objection.lol and re-download to update its version") objection: _ObjectionBase if objectionDict['type'] == 'scene': objection = Scene() elif objectionDict['type'] == 'case': objection = Case() else: raise IOError('Unknown objection type "' + str(objectionDict['type']) + '"') dialogueBox = _getEnumByValue(enums.PresetDialogueBox, objectionDict['options']['chatbox'], enums.PresetDialogueBox.CLASSIC) objection.options = Options( dialogueBox=dialogueBox, defaultTextSpeed=objectionDict['options']['textSpeed'], blipFrequency=objectionDict['options']['textBlipFrequency'], autoplaySpeed=objectionDict['options']['autoplaySpeed'], continueSoundUrl=objectionDict['options']['continueSoundUrl'], ) for alias in objectionDict['aliases']: objection.aliases[alias['from']] = alias['to'] recordMap = {} if type(objection) is Case: for recordName, recordList, recordType in (('evidence', objection.evidence, enums.RecordType.EVIDENCE), ('profiles', objection.profiles, enums.RecordType.PROFILE)): for item in objectionDict['courtRecord'][recordName]: recordItem = Case.RecordItem( type=recordType, name=item.get('name'), iconUrl=item.get('iconUrl'), checkUrl=item.get('url'), description=item.get('description'), hidden=item.get('hidden'), ) recordList.append(recordItem) recordMap[recordItem._getIid(objMap=[(recordItem, item)])] = recordItem frameMap = [] frameIIDs = {} groupIIDs = {} for groupDict in objectionDict['groups']: group: Group if groupDict['type'] == 'n': group = Group() elif groupDict['type'] == 'ce': group = CEGroup() elif groupDict['type'] == 'go': group = GameOverGroup() else: raise IOError('Unknown group type "' + str(groupDict['type']) + '"') objection._groups.append(group) group.name = groupDict['name'] group.frames.append groupIIDs[groupDict['iid']] = group mainFrameClass = frames.Frame if type(group) is CEGroup: for frameDict in groupDict['counselFrames']: frame = _loadJSONFrame(frameDict, frames.Frame, objectionDict['pairs'], frameMap, frameIIDs, suppressWarnings) group.counselSequence.append(frame) for frameDict in groupDict['failureFrames']: frame = _loadJSONFrame(frameDict, frames.Frame, objectionDict['pairs'], frameMap, frameIIDs, suppressWarnings) group.failureSequence.append(frame) mainFrameClass = frames.CEFrame for frameDict in groupDict['frames']: frame = _loadJSONFrame(frameDict, mainFrameClass, objectionDict['pairs'], frameMap, frameIIDs, suppressWarnings) group.frames.append(frame) if type(objection) is Case: processedList = [] frame: Frame for i in range(2): for frame, frameDict in frameMap: if frame in processedList: continue processedList.append(frame) if frameDict['caseAction']: id, param = frameDict['caseAction']['id'], frameDict['caseAction']['value'] if id == 16: frame.caseAction = frames.CaseActions.ToggleEvidence( show=[recordMap[iid] for iid in param['show']], hide=[recordMap[iid] for iid in param['hide']], ) elif id == 3: frame.caseAction = frames.CaseActions.ToggleFrames( show=[frameIIDs[int(iid)] for iid in param['show'].split()], hide=[frameIIDs[int(iid)] for iid in param['hide'].split()], ) elif id == 4: frame.caseAction = frames.CaseActions.GoToFrame(frameIIDs[int(param)]) elif id == 15: frame.caseAction = frames.CaseActions.SetGameOverGroup(groupIIDs[int(param)]) elif id == 5: frame.caseAction = frames.CaseActions.EndGame() elif id == 6: if param['type'] == 0: frame.caseAction = frames.CaseActions.HealthSet(param['amount']/100) elif param['type'] == 1: frame.caseAction = frames.CaseActions.HealthAdd(param['amount']/100) elif param['type'] == 2: frame.caseAction = frames.CaseActions.HealthRemove(param['amount']/100) elif id == 7: frame.caseAction = frames.CaseActions.FlashingHealth(int(param)/100) elif id == 8: frame.caseAction = frames.CaseActions.PromptPresent( failFrame=frameIIDs[int(param['falseFid'])], presentEvidence=param['evidence'], presentProfiles=param['profiles'], choices=[(recordMap[item['eid']], frameIIDs[int(item['fid'])]) for item in param['items']], ) elif id == 9: frame.caseAction = frames.CaseActions.PromptChoice([ (choice['text'], frameIIDs[int(choice['fid'])]) for choice in param ]) elif id == 12: if param['type'] == 'int': frame.caseAction = frames.CaseActions.PromptInt(param['name']) else: frame.caseAction = frames.CaseActions.PromptStr( varName=param['name'], allowSpaces=param['type'] == "string", toLower=param['lowercase'], ) elif id == 17: frame.caseAction = frames.CaseActions.PromptCursor( failFrame=frameIIDs[int(param['falseFid'])], previewImageUrl=param['imageUrl'], prompt=param['prompt'], cursorColor=frames.Color(param['color']), ) for area in param['areas']: frame.caseAction.choices.append(( frames.CursorRect(area['shape']['left'], area['shape']['top'], area['shape']['width'], area['shape']['height']), frameIIDs[int(area['fid'])], )) elif id == 10: frame.caseAction = frames.CaseActions.VarSet(param['name'], param['value']) elif id == 11: frame.caseAction = frames.CaseActions.VarAdd(param['name'], param['value']) elif id == 14: frame.caseAction = frames.CaseActions.VarEval( trueFrame=frameIIDs[int(param['trueFid'])], falseFrame=frameIIDs[int(param['falseFid'])], expression=param['expression'], ) elif id == 13: operator: str if param['type'] == "equals": operator = '==' elif param['type'] == "notEquals": operator = '!=' elif param['type'] == "greaterThan": operator = '>' elif param['type'] == "lessThan": operator = '<' else: raise IOError(f"Unknown variable evaluation operator \"{param['type']}\" at frame {frameDict['iid']} (\"{frameDict['text']}\")") frame.caseAction = frames.CaseActions.VarEval( trueFrame=frameIIDs[int(param['trueFid'])], falseFrame=frameIIDs[int(param['falseFid'])], expression=param['name'] + operator + param['value'], ) if type(frame) is frames.CEFrame: if 'contradictions' in frameDict: for contraDict in frameDict['contradictions']: frame.contradictions.append(( recordMap[contraDict['eid']], frameIIDs[int(contraDict['fid'])], )) if 'pressFrames' in frameDict: for pressDict in frameDict['pressFrames']: pressFrame = _loadJSONFrame(pressDict, Frame, objectionDict['pairs'], frameMap, frameIIDs, suppressWarnings) frame.pressSequence.append(pressFrame) return objection
def loadJSONStr(objection: str, suppressWarnings: bool = False) ‑> Union[Scene, Case]
-
Load objectionpy objection from existing .objection in form of a JSON string.
Args
objection : str
- JSON string of an objection.lol .objection
suppressWarnings : bool
- Defaults to False.
Raises
IOError
- A JSON object's type is unknown or unsupported
Returns
Scene or case parsed from the .objection JSON.
Expand source code
def loadJSONStr(objection: str, suppressWarnings: bool = False) -> Union[Scene, Case]: """ Load objectionpy objection from existing .objection in form of a JSON string. Args: - `objection : str` - JSON string of an objection.lol .objection - `suppressWarnings : bool` - Defaults to False. Raises: - `IOError` - A JSON object's type is unknown or unsupported Returns: Scene or case parsed from the .objection JSON. """ return loadJSONDict(loads(objection))
def loadB64(objection: str, suppressWarnings: bool = False) ‑> Union[Scene, Case]
-
Load objectionpy objection from existing .objection in form of base64-encoded JSON.
Args
objection : str
- Base64-encoded JSON string of an objection.lol .objection
suppressWarnings : bool
- Defaults to False.
Raises
IOError
- A JSON object's type is unknown or unsupported
Returns
Scene or case parsed from the .objection JSON.
Expand source code
def loadB64(objection: str, suppressWarnings: bool = False) -> Union[Scene, Case]: """ Load objectionpy objection from existing .objection in form of base64-encoded JSON. Args: - `objection : str` - Base64-encoded JSON string of an objection.lol .objection - `suppressWarnings : bool` - Defaults to False. Raises: - `IOError` - A JSON object's type is unknown or unsupported Returns: Scene or case parsed from the .objection JSON. """ return loadJSONStr(b64decode(objection).decode("utf-8"))
Classes
class Group (objection: Optional[ForwardRef('_ObjectionBase')] = None, name: Optional[str] = None, caseTag: Optional[str] = None, frames: list = <factory>)
-
Container for frames used in Cases.
Attributes
objection : Optional[_ObjectionBase]
- The case to automatically append to on the group's initialization.
name : str
- Group name.
caseTag : str
- A unique tag used to identify the group in case actions. (A direct reference to the group object works too)
frames : list[Frame]
- The group's frame list. May be of type Frame or CEFrame.
Expand source code
@dataclass class Group: """ Container for frames used in Cases. Attributes: - `objection : Optional[_ObjectionBase]` - The case to automatically append to on the group's initialization. - `name : str` - Group name. - `caseTag : str` - A unique tag used to identify the group in case actions. (A direct reference to the group object works too) - `frames : list[Frame]` - The group's frame list. May be of type Frame or CEFrame. """ _type: enums.GroupType = field(default=enums.GroupType.NORMAL, init=False) objection: Optional['_ObjectionBase'] = None name: Optional[str] = None caseTag: Optional[str] = None frames: list[Frame] = field(default_factory=list) def __post_init__(self): try: self.objection.groups.append(self) # type: ignore except AttributeError: pass
Subclasses
Class variables
var frames : list
var objection : Optional[objectionpy.objection._ObjectionBase]
var name : Optional[str]
var caseTag : Optional[str]
class CEGroup (objection: Optional[ForwardRef('_ObjectionBase')] = None, name: Optional[str] = None, caseTag: Optional[str] = None, frames: list = <factory>)
-
Special container for cross-examination sequences.
Can contain both Frame and CEFrame.
Attributes
objection : Optional[_ObjectionBase]
- The case to automatically append to on the group's initialization.
name : str
- Group name.
caseTag : str
- A unique tag used to identify the group in case actions. (A direct reference to the group object works too)
frames : list[Frame]
- The group's frame list. May be of type Frame or CEFrame.
counselSequence : list[Frame]
- A sequence of frames played after the last statement of the cross-examination, before looping back to the first.
failureSequence : list[Frame]
- A sequence of frames played when failing to present a correct contradiction.
Expand source code
@dataclass class CEGroup(Group): """ Special container for cross-examination sequences. Can contain both Frame and CEFrame. Attributes: - `objection : Optional[_ObjectionBase]` - The case to automatically append to on the group's initialization. - `name : str` - Group name. - `caseTag : str` - A unique tag used to identify the group in case actions. (A direct reference to the group object works too) - `frames : list[Frame]` - The group's frame list. May be of type Frame or CEFrame. - `counselSequence : list[Frame]` - A sequence of frames played after the last statement of the cross-examination, before looping back to the first. - `failureSequence : list[Frame]` - A sequence of frames played when failing to present a correct contradiction. """ _type: enums.GroupType = field( default=enums.GroupType.CE, init=False) counselSequence: list[Frame] = field(default_factory=list, init=False) failureSequence: list[Frame] = field(default_factory=list, init=False)
Ancestors
Class variables
var counselSequence : list
var failureSequence : list
class GameOverGroup (objection: Optional[ForwardRef('_ObjectionBase')] = None, name: Optional[str] = None, caseTag: Optional[str] = None, frames: list = <factory>)
-
Special container whose frames are played when the player's health reaches 0.
Attributes
objection : Optional[_ObjectionBase]
- The case to automatically append to on the group's initialization.
name : str
- Group name.
caseTag : str
- A unique tag used to identify the group in case actions. (A direct reference to the group object works too)
frames : list[Frame]
- The group's frame list. May be of type Frame or CEFrame.
Expand source code
class GameOverGroup(Group): """ Special container whose frames are played when the player's health reaches 0. Attributes: - `objection : Optional[_ObjectionBase]` - The case to automatically append to on the group's initialization. - `name : str` - Group name. - `caseTag : str` - A unique tag used to identify the group in case actions. (A direct reference to the group object works too) - `frames : list[Frame]` - The group's frame list. May be of type Frame or CEFrame. """ _type = enums.GroupType.GAME_OVER
Ancestors
Class variables
var objection : Optional[objectionpy.objection._ObjectionBase]
var name : Optional[str]
var caseTag : Optional[str]
var frames : list
class Options (dialogueBox: PresetDialogueBox = PresetDialogueBox.CLASSIC, defaultTextSpeed: int = 28, blipFrequency: int = 56, autoplaySpeed: int = 500, continueSoundUrl: str = '/Audio/Case/Continue_Trilogy.wav', MAX_PAIRS: int = 100, MAX_GROUPS: int = 100, MAX_ALIASES: int = 100, MAX_GROUP_FRAMES: int = 1000, MAX_FRAME_ACTIONS: int = 10, MAX_EVIDENCE: int = 50, MAX_PROFILES: int = 50)
-
The default options of an objection.
Most can be modified with OptionModifiers in Frames.
Expand source code
@dataclass class Options: """ The default options of an objection. Most can be modified with OptionModifiers in Frames. """ dialogueBox: enums.PresetDialogueBox = enums.PresetDialogueBox.CLASSIC defaultTextSpeed: int = 28 blipFrequency: int = 56 autoplaySpeed: int = 500 continueSoundUrl: str = "/Audio/Case/Continue_Trilogy.wav" MAX_PAIRS: int = 100 MAX_GROUPS: int = 100 MAX_ALIASES: int = 100 MAX_GROUP_FRAMES: int = 1000 MAX_FRAME_ACTIONS: int = 10 MAX_EVIDENCE: int = 50 MAX_PROFILES: int = 50
Class variables
var dialogueBox : PresetDialogueBox
var defaultTextSpeed : int
var blipFrequency : int
var autoplaySpeed : int
var continueSoundUrl : str
var MAX_PAIRS : int
var MAX_GROUPS : int
var MAX_ALIASES : int
var MAX_GROUP_FRAMES : int
var MAX_FRAME_ACTIONS : int
var MAX_EVIDENCE : int
var MAX_PROFILES : int
class Scene (options: Optional[Options] = None)
-
Objection scene - a linear series of frames that can be recorded or played in a browser.
Primarily modified by editing frames in Scene.frames.
Attributes
options : Options
- Default objection options.
aliases : dict[str, str]
- Dictionary of aliases, mapping original name to alias.
frames : list[Frames]
- The scene's frame list. Primary way of modifying scenes.
Expand source code
class Scene(_ObjectionBase): """ Objection scene - a linear series of frames that can be recorded or played in a browser. Primarily modified by editing frames in Scene.frames. Attributes: - `options : Options` - Default objection options. - `aliases : dict[str, str]` - Dictionary of aliases, mapping original name to alias. - `frames : list[Frames]` - The scene's frame list. Primary way of modifying scenes. """ type = enums.ObjectionType.SCENE def __init__(self, options: Optional[Options] = None) -> None: super().__init__(options) mainGroup = Group(name='Main') self._groups.append(mainGroup) @property def frames(self) -> list[Frame]: return self._groups[0].frames def compile(self) -> dict: objectionDict = super().compile() compiledFrames = self._groupMap[0][1]['frames'] for frameDict in [*compiledFrames]: if frameDict.get('hide'): compiledFrames.remove(frameDict) return objectionDict
Ancestors
- objectionpy.objection._ObjectionBase
Class variables
var options : Options
var aliases : dict
var type
Instance variables
var frames : list
-
Expand source code
@property def frames(self) -> list[Frame]: return self._groups[0].frames
Methods
def compile(self) ‑> dict
-
Compile objection.
Raises
ObjectionError
- Duplicate case tag was found.
- CEFrame was found in the wrong group.
Returns
JSON-serializable dictionary in the .objection format.
Expand source code
def compile(self) -> dict: objectionDict = super().compile() compiledFrames = self._groupMap[0][1]['frames'] for frameDict in [*compiledFrames]: if frameDict.get('hide'): compiledFrames.remove(frameDict) return objectionDict
class Case (options: Optional[Options] = None)
-
Objection case - interactive game playable only on a browser, with certain extra features.
Attributes
options : Options
- Default objection options.
aliases : dict[str, str]
- Dictionary of aliases, mapping original name to alias.
evidence : list[RecordItem]
profiles : list[RecordItem]
groups : list[Group]
- The case's group list. Primary way of modifying cases.
Expand source code
class Case(_ObjectionBase): """ Objection case - interactive game playable only on a browser, with certain extra features. Attributes: - `options : Options` - Default objection options. - `aliases : dict[str, str]` - Dictionary of aliases, mapping original name to alias. - `evidence : list[RecordItem]` - `profiles : list[RecordItem]` - `groups : list[Group]` - The case's group list. Primary way of modifying cases. """ type = enums.ObjectionType.CASE @dataclass class RecordItem: type: enums.RecordType name: str iconUrl: str checkUrl: str = '' description: str = '' hidden: bool = False def _getIid(self, objMap: list[tuple]) -> str: recordObject = _utils._tupleMapGet(objMap, self) prefix: str if (self.type is enums.RecordType.EVIDENCE): prefix = 'e-' elif (self.type is enums.RecordType.PROFILE): prefix = 'p-' else: raise TypeError('Unknown record item type ' + self.type.name) return prefix + str(recordObject["iid"]) evidence: list[RecordItem] profiles: list[RecordItem] def __init__(self, options: Optional[Options] = None) -> None: super().__init__(options) self.evidence = [] self.profiles = [] @property def groups(self) -> list[Group]: return self._groups def _getByTagOrObj( self, identifier: Union[str, Any], objMap: list[tuple], tagMap: dict, errorText: str ) -> dict: if (type(identifier) is str): return tagMap[identifier] else: try: return _utils._tupleMapGet(objMap, identifier) except KeyError: raise KeyError(errorText) def _getFrameDict(self, frameParam: Union[str, Frame]) -> dict: return self._getByTagOrObj(frameParam, objMap=self._frameMap, tagMap=self._frameTags, errorText='Parsed frame object wasn\' found') def _getGroupDict(self, groupParam: Union[str, Group]) -> dict: return self._getByTagOrObj(groupParam, objMap=self._groupMap, tagMap=self._groupTags, errorText='Parsed group object wasn\' found') def _post_process_frame(self, processedList: list, frame: Frame, frameDict: dict): if frame in processedList: return processedList.append(frame) if isinstance(frame, frames.CEFrame): if len(frame.pressSequence) > 0: frameDict["pressFrames"] = [] for pressFrame in frame.pressSequence: if isinstance(pressFrame, frames.CEFrame): raise ObjectionError('CEFrame found within press sequence') frameDict["pressFrames"].append( self._compileFrame(pressFrame, frameList=frameDict["pressFrames"]) ) if len(frame.contradictions) > 0: frameDict["contradictions"] = [] for recordItem, frameParam in frame.contradictions: frameDict["contradictions"].append({ "eid": recordItem._getIid(self._recordMap), "fid": str(self._getFrameDict(frameParam)["iid"]), }) action = frame.caseAction if action is None: return actionId: int = -1 actionValue: Any = None if isinstance(action, frames.CaseActions.ToggleEvidence): actionId = 16 actionValue = { "show": [], "hide": [], } item: Case.RecordItem for item in action.show: actionValue["show"].append(item._getIid(self._recordMap)) for item in action.hide: actionValue["hide"].append(item._getIid(self._recordMap)) elif isinstance(action, frames.CaseActions.ToggleFrames): actionId = 3 actionValue = { "show": "", "hide": "", } for frameParam in action.show: targetframeDict: dict = self._getFrameDict(frameParam) actionValue["show"] += str(targetframeDict["iid"]) + ' ' for frameParam in action.hide: targetframeDict: dict = self._getFrameDict(frameParam) actionValue["hide"] += str(targetframeDict["iid"]) + ' ' for key in ("show", "hide"): if (len(actionValue[key]) > 0): actionValue[key] = actionValue[key].strip() elif isinstance(action, frames.CaseActions.GoToFrame): actionId = 4 actionValue = str(self._getFrameDict(action.frame)["iid"]) elif isinstance(action, frames.CaseActions.SetGameOverGroup): actionId = 15 actionValue = str(self._getGroupDict(action.group)["iid"]) elif isinstance(action, frames.CaseActions.EndGame): actionId = 5 elif isinstance(action, (frames.CaseActions.HealthSet, frames.CaseActions.HealthAdd, frames.CaseActions.HealthRemove)): actionId = 6 actionValue = { "amount": int(action.amount * 100) } if isinstance(action, frames.CaseActions.HealthSet): actionValue["type"] = 0 elif isinstance(action, frames.CaseActions.HealthAdd): actionValue["type"] = 1 elif isinstance(action, frames.CaseActions.HealthRemove): actionValue["type"] = 2 elif isinstance(action, frames.CaseActions.FlashingHealth): actionId = 7 actionValue = str(int(action.amount * 100)) elif isinstance(action, frames.CaseActions.PromptPresent): actionId = 8 actionValue = { "evidence": action.presentEvidence, "profiles": action.presentProfiles, "falseFid": str(self._getFrameDict(action.failFrame)["iid"]), "items": [], } for recordItem, frameParam in action.choices: actionValue["items"].append({ "eid": recordItem._getIid(self._recordMap), "fid": str(self._getFrameDict(frameParam)["iid"]), }) elif isinstance(action, frames.CaseActions.PromptChoice): actionId = 9 actionValue = [] for choiceText, frameParam in action.choices: actionValue.append({ "text": choiceText, "fid": str(self._getFrameDict(frameParam)["iid"]), }) elif isinstance(action, (frames.CaseActions.PromptInt, frames.CaseActions.PromptStr)): actionId = 12 actionValue = { "name": action.varName, "type": "int", } if isinstance(action, frames.CaseActions.PromptStr): actionValue["lowercase"] = action.toLower actionValue["type"] = "string" if not action.allowSpaces: actionValue["type"] = "word" elif isinstance(action, frames.CaseActions.PromptCursor): actionId = 17 actionValue = { "imageUrl": action.previewImageUrl, "prompt": action.prompt, "color": str(action.cursorColor), "falseFid": str(self._getFrameDict(action.failFrame)["iid"]), "areas": [] } for cursorRect, frameParam in action.choices: actionValue["areas"].append({ "fid": str(self._getFrameDict(frameParam)["iid"]), "shape": { "left": cursorRect.left, "top": cursorRect.top, "width": cursorRect.width, "height": cursorRect.height, }, }) elif isinstance(action, frames.CaseActions.VarSet): actionId = 10 actionValue = { "name": action.varName, "value": action.value, } elif isinstance(action, frames.CaseActions.VarAdd): actionId = 11 actionValue = { "name": action.varName, "value": str(action.value), } elif isinstance(action, frames.CaseActions.VarEval): actionId = 14 actionValue = { "expression": action.expression, "trueFid": str(self._getFrameDict(action.trueFrame)["iid"]), "falseFid": str(self._getFrameDict(action.falseFrame)["iid"]), } if actionId == -1: raise TypeError('unknown case action') actionObject = { "id": actionId, "value": actionValue, } frameDict['caseAction'] = actionObject def compile(self) -> dict: self._recordMap = [] courtRecord = { 'evidence': [], 'profiles': [], } for items, recordKey in (self.evidence, 'evidence'), (self.profiles, 'profiles'): for i, item in enumerate(items): recordObject = { "iid": i + 1, "name": item.name, "iconUrl": item.iconUrl, "url": item.checkUrl, "description": item.description, "hide": item.hidden, } courtRecord[recordKey].append(recordObject) self._recordMap.append((item, recordObject)) LimitWarning.checkList( courtRecord['evidence'], self.options.MAX_EVIDENCE, 'evidence') LimitWarning.checkList( courtRecord['profiles'], self.options.MAX_PROFILES, 'profiles') objectionDict = super().compile() objectionDict['courtRecord'] = courtRecord frame: Frame frameDict: dict processedList = [] for i in range(2): # Looping twice to process newly-generated press frames too for frame, frameDict in [*self._frameMap]: self._post_process_frame(processedList, frame, frameDict) return objectionDict
Ancestors
- objectionpy.objection._ObjectionBase
Class variables
var evidence : list
var profiles : list
var type
var RecordItem
-
RecordItem(type: objectionpy.enums.RecordType, name: str, iconUrl: str, checkUrl: str = '', description: str = '', hidden: bool = False)
Instance variables
var groups : list
-
Expand source code
@property def groups(self) -> list[Group]: return self._groups
Methods
def compile(self) ‑> dict
-
Compile objection.
Raises
ObjectionError
- Duplicate case tag was found.
- CEFrame was found in the wrong group.
Returns
JSON-serializable dictionary in the .objection format.
Expand source code
def compile(self) -> dict: self._recordMap = [] courtRecord = { 'evidence': [], 'profiles': [], } for items, recordKey in (self.evidence, 'evidence'), (self.profiles, 'profiles'): for i, item in enumerate(items): recordObject = { "iid": i + 1, "name": item.name, "iconUrl": item.iconUrl, "url": item.checkUrl, "description": item.description, "hide": item.hidden, } courtRecord[recordKey].append(recordObject) self._recordMap.append((item, recordObject)) LimitWarning.checkList( courtRecord['evidence'], self.options.MAX_EVIDENCE, 'evidence') LimitWarning.checkList( courtRecord['profiles'], self.options.MAX_PROFILES, 'profiles') objectionDict = super().compile() objectionDict['courtRecord'] = courtRecord frame: Frame frameDict: dict processedList = [] for i in range(2): # Looping twice to process newly-generated press frames too for frame, frameDict in [*self._frameMap]: self._post_process_frame(processedList, frame, frameDict) return objectionDict
class LimitWarning (*args, **kwargs)
-
Displayed when the limit of an individual objection component is exceeded.
By default, they match the limits of the objection.lol GUI, but can be modified in the objection's Options.
Expand source code
class LimitWarning(Warning): """ Displayed when the limit of an individual objection component is exceeded. By default, they match the limits of the objection.lol GUI, but can be modified in the objection's Options.""" @classmethod def warn(cls, limit: int, limitTarget: str, value: int): warn( f'exceeded limit of {limit} {limitTarget} (at {value}) - objection.lol support is not guaranteed', LimitWarning) @classmethod def checkList(cls, lst: Sized, limit: Optional[int], limitTarget: str): if limit is not None and len(lst) > limit: cls.warn(limit, limitTarget, len(lst))
Ancestors
- builtins.Warning
- builtins.Exception
- builtins.BaseException
Static methods
def warn(limit: int, limitTarget: str, value: int)
-
Expand source code
@classmethod def warn(cls, limit: int, limitTarget: str, value: int): warn( f'exceeded limit of {limit} {limitTarget} (at {value}) - objection.lol support is not guaranteed', LimitWarning)
def checkList(lst: Sized, limit: Optional[int], limitTarget: str)
-
Expand source code
@classmethod def checkList(cls, lst: Sized, limit: Optional[int], limitTarget: str): if limit is not None and len(lst) > limit: cls.warn(limit, limitTarget, len(lst))
class IOWarning (*args, **kwargs)
-
Warning for the use cases of IOError.
Expand source code
class IOWarning(Warning): """Warning for the use cases of IOError.""" pass
Ancestors
- builtins.Warning
- builtins.Exception
- builtins.BaseException
class ObjectionError (*args, **kwargs)
-
Error of unspecified type during objection compilation.
Expand source code
class ObjectionError(Exception): """Error of unspecified type during objection compilation.""" pass
Ancestors
- builtins.Exception
- builtins.BaseException