Module objectionpy.assets

Module for working with objection.lol assets.

Each asset can be obtained using its id. Also includes the AssetBank class - a container for organizing project assets.

Expand source code
"""
Module for working with objection.lol assets.

Each asset can be obtained using its id. Also includes the AssetBank class - a container for organizing project assets.
"""

from functools import cache, lru_cache
import re
from warnings import warn
from typing import Optional
from requests import post, get
from json import JSONDecodeError
from . import enums, _utils


class AssetBank:
    """Container for organizing project assets."""
    chars: dict[str, 'Character']
    backgrounds: dict[str, 'Background']
    music: dict[str, 'Music']
    sounds: dict[str, 'Sound']
    popups: dict[str, 'Popup']
    evidence: dict[str, 'Evidence']

    def __init__(self, assetDict: dict[str, '_Asset'] = {}) -> None:
        for key in self.__annotations__.keys():
            setattr(self, key, {})
        self.loadAssets(assetDict)

    def loadAssets(self, assetDict: dict[str, '_Asset']):
        """
        Add a dictionary of asset objects into the AssetBank, automatically distributed by type.

        Args:
            - `assetDict : dict`
                - Dictionary of asset objects.

        Raises:
            TypeError: An object of an invalid type was provided.
        """
        for name, asset in assetDict.items():
            if type(asset) is Character:
                self.chars[name] = asset
            elif type(asset) is Background:
                self.backgrounds[name] = asset
            elif type(asset) is Music:
                self.music[name] = asset
            elif type(asset) is Sound:
                self.sounds[name] = asset
            elif type(asset) is Popup:
                self.popups[name] = asset
            elif type(asset) is Evidence:
                self.evidence[name] = asset
            else:
                raise TypeError('Unknown asset type ' + type(asset).__name__)

    def loadAssetIDs(self, assetType: type, ids: dict[str, int]):
        """
        Add assets of a single type using IDs into the AssetBank.

        Args:
            - `assetType : type`
                - Type of asset to add.
            - `ids : dict[str, int]`
                - Dictionary of asset IDs used to automatically get the assets.

        Raises:
            TypeError: An invalid type was provided.
        """
        if assetType is Character:
            targetDict = self.chars
        elif assetType is Background:
            targetDict = self.backgrounds
        elif assetType is Music:
            targetDict = self.music
        elif assetType is Sound:
            targetDict = self.sounds
        elif assetType is Popup:
            targetDict = self.popups
        elif assetType is Evidence:
            targetDict = self.evidence
        else:
            raise TypeError('Unknown asset type ' + assetType.__name__)

        for name, id in ids.items():
            targetDict[name] = assetType(id)


class _Asset:
    """
    An objection.lol asset, created by ID.

    By default, it simply acts as a representation. If any of the asset data is accessed, though, it will request it using the objection.lol API.
    """
    id: int
    exists: bool = True
    _loaded: bool = False
    _reprKeys: tuple = ('id', 'name')

    def __init__(self, id):
        self.id = id

    def __getattribute__(self, __name: str):
        if __name != '_assetKeys' and __name in self._assetKeys + ('exists',) and not self._loaded:
            self._loaded = True
            assetExists, characterData = type(self)._requestData(self.id)
            self.exists = assetExists
            if assetExists:
                for key, value in characterData.items():
                    setattr(self, key, value)
        return object.__getattribute__(self, __name)

    def __repr__(self) -> str:
        if self.exists:
            return _utils._reprFunc(self, self._reprKeys)
        else:
            return _utils._reprFunc(self, ('id', 'exists'))

    @classmethod
    def _requestData(cls, *_) -> tuple[bool, dict]:
        raise NotImplementedError(
            "_requestData must be implemented by " + cls.__name__ + " subclasses")


class _GetAsset(_Asset):
    _getUrl: str
    _tag: str
    name: str
    url: str

    @classmethod
    @cache
    def _requestData(cls, id):
        request = get(
            url=cls._getUrl + str(id)
        )
        return True, request.json()

    def __init__(self, id):
        super().__init__(id)
        self.tag = '[#' + self._tag + str(self.id) + ']'

    def __str__(self) -> str:
        return self.tag


class _PostAsset(_Asset):
    _postUrl: str
    name: str

    @classmethod
    @cache
    def _requestData(cls, id):
        request = post(
            url=cls._postUrl,
            json={
                "ids": [id]
            }
        )
        try:
            json = request.json()
            return True, json[0]
        except (JSONDecodeError, IndexError):
            AssetWarning.warn(id)
            return False, None


class Background(_PostAsset):
    _postUrl = 'https://api.objection.lol/assets/background/getbackgrounds'
    _assetKeys = ('name', 'url', 'deskUrl', 'isWide')


class Character(_PostAsset):
    _postUrl = 'https://api.objection.lol/character/getcharacters'
    _assetKeys = ('alignment', 'backgroundId', 'blipUrl', 'bubbles', 'galleryAJImageUrl', 'galleryImageUrl',
                  'iconUrl', 'limitWidth', 'name', 'namePlate', 'offsetX', 'offsetY', 'poses', 'side')

    id: Optional[int] = None
    alignment: Optional[list] = None
    backgroundId: int = 0
    blipUrl: str = '/Audio/blip.wav'
    bubbles: list
    galleryAJImageUrl: Optional[str] = None
    galleryImageUrl: Optional[str] = None
    iconUrl: Optional[str] = None
    limitWidth: bool = False
    name: str = 'None'
    namePlate: str = ''
    offsetX: int = 0
    offsetY: int = 0
    poses: list
    side: enums.CharacterLocation = enums.CharacterLocation.WITNESS
    _aj: bool = False

    def __init__(self, id, _loaded=False):
        self.bubbles = []
        self.poses = []
        super().__init__(id)
        if _loaded:
            self._loaded = True
            self.exists = True

    @property
    def background(self):
        if not hasattr(self, '_background'):
            self._background = Background(self.backgroundId)
        return self._background

    @property
    def isPreset(self):
        return self.id is None or self.id < 1000

    @classmethod
    def _poseLookupKey(cls, name: str) -> str:
        name = re.sub('[^a-zA-Z0-9]', '', name)
        name = name.lower()
        return name

    @property
    @lru_cache(1)
    def _poseLookupKeys(self) -> list:
        return list(map(lambda pose: self._poseLookupKey(pose['name']), self.poses))

    def lookupPoseSubstr(self, lookupSubstr: str) -> int:
        """
        Search character pose using a name substring.

        Args:
            - `lookupSubstr : str`
                - The susbtring to search by. Case-insensitive and ignores non-alphanumeric characters when comparing.
                - When substring matches multiple poses, returns the one with the lowest length difference.

        Returns:
            The ID of the pose. If the pose isn't found, returns -1.
        """
        keyToFind = self._poseLookupKey(lookupSubstr)

        bestI = -1
        bestLengthDifference = 0
        for i, key in enumerate(self._poseLookupKeys):
            if keyToFind not in key:
                continue
            lengthDifference = len(key) - len(keyToFind)
            if bestI == -1 or lengthDifference < bestLengthDifference:
                bestI = i
                bestLengthDifference = lengthDifference
        
        if bestI > -1:
            return self.poses[bestI]['id']
        else:
            return -1

    def getPose(self, name: str) -> int:
        """
        Get character pose by its exact name.

        Args:
            - `name : str`
                - The pose's name. Must exactly match the pose name string.

        Returns:
            The ID of the pose. If the pose isn't found, returns -1.
        """
        for pose in self.poses:
            if pose['name'] != name:
                continue
            return pose['id']
        return -1


class Evidence(_Asset):
    _getUrl = 'https://api.objection.lol/assets/evidence/get?id='
    _assetKeys = ('url',)
    _reprKeys = ('id', 'url')

    @classmethod
    @cache
    def _requestData(cls, id):
        request = get(
            url=cls._getUrl + str(id)
        )
        if len(request.text) > 0:
            return True, {'url': request.text}
        else:
            AssetWarning.warn(id)
            return False, None

    def __init__(self, id):
        super().__init__(id)
        self.tag = '[#evd' + str(self.id) + ']'
        self.icon = '[#evdi' + str(self.id) + ']'


class Music(_GetAsset):
    _getUrl = 'https://api.objection.lol/assets/music/get?id='
    _assetKeys = ('name', 'url', 'volume', 'fileSize')
    _tag = 'bgm'


class Popup(_PostAsset):
    _postUrl = 'https://api.objection.lol/assets/popup/getpopups'
    _assetKeys = ('name', 'url', 'alignment', 'center', 'posY', 'resize')


class Sound(_GetAsset):
    _getUrl = 'https://api.objection.lol/assets/sound/get?id='
    _assetKeys = ('name', 'url', 'volume', 'fileSize')
    _tag = 'bgs'


class AssetWarning(Warning):
    @classmethod
    def warn(cls, id):
        warn('asset ' + str(id) + ' not found', AssetWarning)

Classes

class AssetBank (assetDict: dict = {})

Container for organizing project assets.

Expand source code
class AssetBank:
    """Container for organizing project assets."""
    chars: dict[str, 'Character']
    backgrounds: dict[str, 'Background']
    music: dict[str, 'Music']
    sounds: dict[str, 'Sound']
    popups: dict[str, 'Popup']
    evidence: dict[str, 'Evidence']

    def __init__(self, assetDict: dict[str, '_Asset'] = {}) -> None:
        for key in self.__annotations__.keys():
            setattr(self, key, {})
        self.loadAssets(assetDict)

    def loadAssets(self, assetDict: dict[str, '_Asset']):
        """
        Add a dictionary of asset objects into the AssetBank, automatically distributed by type.

        Args:
            - `assetDict : dict`
                - Dictionary of asset objects.

        Raises:
            TypeError: An object of an invalid type was provided.
        """
        for name, asset in assetDict.items():
            if type(asset) is Character:
                self.chars[name] = asset
            elif type(asset) is Background:
                self.backgrounds[name] = asset
            elif type(asset) is Music:
                self.music[name] = asset
            elif type(asset) is Sound:
                self.sounds[name] = asset
            elif type(asset) is Popup:
                self.popups[name] = asset
            elif type(asset) is Evidence:
                self.evidence[name] = asset
            else:
                raise TypeError('Unknown asset type ' + type(asset).__name__)

    def loadAssetIDs(self, assetType: type, ids: dict[str, int]):
        """
        Add assets of a single type using IDs into the AssetBank.

        Args:
            - `assetType : type`
                - Type of asset to add.
            - `ids : dict[str, int]`
                - Dictionary of asset IDs used to automatically get the assets.

        Raises:
            TypeError: An invalid type was provided.
        """
        if assetType is Character:
            targetDict = self.chars
        elif assetType is Background:
            targetDict = self.backgrounds
        elif assetType is Music:
            targetDict = self.music
        elif assetType is Sound:
            targetDict = self.sounds
        elif assetType is Popup:
            targetDict = self.popups
        elif assetType is Evidence:
            targetDict = self.evidence
        else:
            raise TypeError('Unknown asset type ' + assetType.__name__)

        for name, id in ids.items():
            targetDict[name] = assetType(id)

Class variables

var chars : dict
var backgrounds : dict
var music : dict
var sounds : dict
var popups : dict
var evidence : dict

Methods

def loadAssets(self, assetDict: dict)

Add a dictionary of asset objects into the AssetBank, automatically distributed by type.

Args

  • assetDict : dict
    • Dictionary of asset objects.

Raises

TypeError
An object of an invalid type was provided.
Expand source code
def loadAssets(self, assetDict: dict[str, '_Asset']):
    """
    Add a dictionary of asset objects into the AssetBank, automatically distributed by type.

    Args:
        - `assetDict : dict`
            - Dictionary of asset objects.

    Raises:
        TypeError: An object of an invalid type was provided.
    """
    for name, asset in assetDict.items():
        if type(asset) is Character:
            self.chars[name] = asset
        elif type(asset) is Background:
            self.backgrounds[name] = asset
        elif type(asset) is Music:
            self.music[name] = asset
        elif type(asset) is Sound:
            self.sounds[name] = asset
        elif type(asset) is Popup:
            self.popups[name] = asset
        elif type(asset) is Evidence:
            self.evidence[name] = asset
        else:
            raise TypeError('Unknown asset type ' + type(asset).__name__)
def loadAssetIDs(self, assetType: type, ids: dict)

Add assets of a single type using IDs into the AssetBank.

Args

  • assetType : type
    • Type of asset to add.
  • ids : dict[str, int]
    • Dictionary of asset IDs used to automatically get the assets.

Raises

TypeError
An invalid type was provided.
Expand source code
def loadAssetIDs(self, assetType: type, ids: dict[str, int]):
    """
    Add assets of a single type using IDs into the AssetBank.

    Args:
        - `assetType : type`
            - Type of asset to add.
        - `ids : dict[str, int]`
            - Dictionary of asset IDs used to automatically get the assets.

    Raises:
        TypeError: An invalid type was provided.
    """
    if assetType is Character:
        targetDict = self.chars
    elif assetType is Background:
        targetDict = self.backgrounds
    elif assetType is Music:
        targetDict = self.music
    elif assetType is Sound:
        targetDict = self.sounds
    elif assetType is Popup:
        targetDict = self.popups
    elif assetType is Evidence:
        targetDict = self.evidence
    else:
        raise TypeError('Unknown asset type ' + assetType.__name__)

    for name, id in ids.items():
        targetDict[name] = assetType(id)
class Background (id)

An objection.lol asset, created by ID.

By default, it simply acts as a representation. If any of the asset data is accessed, though, it will request it using the objection.lol API.

Expand source code
class Background(_PostAsset):
    _postUrl = 'https://api.objection.lol/assets/background/getbackgrounds'
    _assetKeys = ('name', 'url', 'deskUrl', 'isWide')

Ancestors

  • objectionpy.assets._PostAsset
  • objectionpy.assets._Asset

Class variables

var name : str
class Character (id)

An objection.lol asset, created by ID.

By default, it simply acts as a representation. If any of the asset data is accessed, though, it will request it using the objection.lol API.

Expand source code
class Character(_PostAsset):
    _postUrl = 'https://api.objection.lol/character/getcharacters'
    _assetKeys = ('alignment', 'backgroundId', 'blipUrl', 'bubbles', 'galleryAJImageUrl', 'galleryImageUrl',
                  'iconUrl', 'limitWidth', 'name', 'namePlate', 'offsetX', 'offsetY', 'poses', 'side')

    id: Optional[int] = None
    alignment: Optional[list] = None
    backgroundId: int = 0
    blipUrl: str = '/Audio/blip.wav'
    bubbles: list
    galleryAJImageUrl: Optional[str] = None
    galleryImageUrl: Optional[str] = None
    iconUrl: Optional[str] = None
    limitWidth: bool = False
    name: str = 'None'
    namePlate: str = ''
    offsetX: int = 0
    offsetY: int = 0
    poses: list
    side: enums.CharacterLocation = enums.CharacterLocation.WITNESS
    _aj: bool = False

    def __init__(self, id, _loaded=False):
        self.bubbles = []
        self.poses = []
        super().__init__(id)
        if _loaded:
            self._loaded = True
            self.exists = True

    @property
    def background(self):
        if not hasattr(self, '_background'):
            self._background = Background(self.backgroundId)
        return self._background

    @property
    def isPreset(self):
        return self.id is None or self.id < 1000

    @classmethod
    def _poseLookupKey(cls, name: str) -> str:
        name = re.sub('[^a-zA-Z0-9]', '', name)
        name = name.lower()
        return name

    @property
    @lru_cache(1)
    def _poseLookupKeys(self) -> list:
        return list(map(lambda pose: self._poseLookupKey(pose['name']), self.poses))

    def lookupPoseSubstr(self, lookupSubstr: str) -> int:
        """
        Search character pose using a name substring.

        Args:
            - `lookupSubstr : str`
                - The susbtring to search by. Case-insensitive and ignores non-alphanumeric characters when comparing.
                - When substring matches multiple poses, returns the one with the lowest length difference.

        Returns:
            The ID of the pose. If the pose isn't found, returns -1.
        """
        keyToFind = self._poseLookupKey(lookupSubstr)

        bestI = -1
        bestLengthDifference = 0
        for i, key in enumerate(self._poseLookupKeys):
            if keyToFind not in key:
                continue
            lengthDifference = len(key) - len(keyToFind)
            if bestI == -1 or lengthDifference < bestLengthDifference:
                bestI = i
                bestLengthDifference = lengthDifference
        
        if bestI > -1:
            return self.poses[bestI]['id']
        else:
            return -1

    def getPose(self, name: str) -> int:
        """
        Get character pose by its exact name.

        Args:
            - `name : str`
                - The pose's name. Must exactly match the pose name string.

        Returns:
            The ID of the pose. If the pose isn't found, returns -1.
        """
        for pose in self.poses:
            if pose['name'] != name:
                continue
            return pose['id']
        return -1

Ancestors

  • objectionpy.assets._PostAsset
  • objectionpy.assets._Asset

Class variables

var bubbles : list
var poses : list
var id : Optional[int]
var alignment : Optional[list]
var backgroundId : int
var blipUrl : str
var galleryAJImageUrl : Optional[str]
var galleryImageUrl : Optional[str]
var iconUrl : Optional[str]
var limitWidth : bool
var name : str
var namePlate : str
var offsetX : int
var offsetY : int
var sideCharacterLocation

Instance variables

var background
Expand source code
@property
def background(self):
    if not hasattr(self, '_background'):
        self._background = Background(self.backgroundId)
    return self._background
var isPreset
Expand source code
@property
def isPreset(self):
    return self.id is None or self.id < 1000

Methods

def lookupPoseSubstr(self, lookupSubstr: str) ‑> int

Search character pose using a name substring.

Args

  • lookupSubstr : str
    • The susbtring to search by. Case-insensitive and ignores non-alphanumeric characters when comparing.
    • When substring matches multiple poses, returns the one with the lowest length difference.

Returns

The ID of the pose. If the pose isn't found, returns -1.

Expand source code
def lookupPoseSubstr(self, lookupSubstr: str) -> int:
    """
    Search character pose using a name substring.

    Args:
        - `lookupSubstr : str`
            - The susbtring to search by. Case-insensitive and ignores non-alphanumeric characters when comparing.
            - When substring matches multiple poses, returns the one with the lowest length difference.

    Returns:
        The ID of the pose. If the pose isn't found, returns -1.
    """
    keyToFind = self._poseLookupKey(lookupSubstr)

    bestI = -1
    bestLengthDifference = 0
    for i, key in enumerate(self._poseLookupKeys):
        if keyToFind not in key:
            continue
        lengthDifference = len(key) - len(keyToFind)
        if bestI == -1 or lengthDifference < bestLengthDifference:
            bestI = i
            bestLengthDifference = lengthDifference
    
    if bestI > -1:
        return self.poses[bestI]['id']
    else:
        return -1
def getPose(self, name: str) ‑> int

Get character pose by its exact name.

Args

  • name : str
    • The pose's name. Must exactly match the pose name string.

Returns

The ID of the pose. If the pose isn't found, returns -1.

Expand source code
def getPose(self, name: str) -> int:
    """
    Get character pose by its exact name.

    Args:
        - `name : str`
            - The pose's name. Must exactly match the pose name string.

    Returns:
        The ID of the pose. If the pose isn't found, returns -1.
    """
    for pose in self.poses:
        if pose['name'] != name:
            continue
        return pose['id']
    return -1
class Evidence (id)

An objection.lol asset, created by ID.

By default, it simply acts as a representation. If any of the asset data is accessed, though, it will request it using the objection.lol API.

Expand source code
class Evidence(_Asset):
    _getUrl = 'https://api.objection.lol/assets/evidence/get?id='
    _assetKeys = ('url',)
    _reprKeys = ('id', 'url')

    @classmethod
    @cache
    def _requestData(cls, id):
        request = get(
            url=cls._getUrl + str(id)
        )
        if len(request.text) > 0:
            return True, {'url': request.text}
        else:
            AssetWarning.warn(id)
            return False, None

    def __init__(self, id):
        super().__init__(id)
        self.tag = '[#evd' + str(self.id) + ']'
        self.icon = '[#evdi' + str(self.id) + ']'

Ancestors

  • objectionpy.assets._Asset

Class variables

var id : int
var exists : bool
class Music (id)

An objection.lol asset, created by ID.

By default, it simply acts as a representation. If any of the asset data is accessed, though, it will request it using the objection.lol API.

Expand source code
class Music(_GetAsset):
    _getUrl = 'https://api.objection.lol/assets/music/get?id='
    _assetKeys = ('name', 'url', 'volume', 'fileSize')
    _tag = 'bgm'

Ancestors

  • objectionpy.assets._GetAsset
  • objectionpy.assets._Asset

Class variables

var name : str
var url : str
class Popup (id)

An objection.lol asset, created by ID.

By default, it simply acts as a representation. If any of the asset data is accessed, though, it will request it using the objection.lol API.

Expand source code
class Popup(_PostAsset):
    _postUrl = 'https://api.objection.lol/assets/popup/getpopups'
    _assetKeys = ('name', 'url', 'alignment', 'center', 'posY', 'resize')

Ancestors

  • objectionpy.assets._PostAsset
  • objectionpy.assets._Asset

Class variables

var name : str
class Sound (id)

An objection.lol asset, created by ID.

By default, it simply acts as a representation. If any of the asset data is accessed, though, it will request it using the objection.lol API.

Expand source code
class Sound(_GetAsset):
    _getUrl = 'https://api.objection.lol/assets/sound/get?id='
    _assetKeys = ('name', 'url', 'volume', 'fileSize')
    _tag = 'bgs'

Ancestors

  • objectionpy.assets._GetAsset
  • objectionpy.assets._Asset

Class variables

var name : str
var url : str
class AssetWarning (*args, **kwargs)

Base class for warning categories.

Expand source code
class AssetWarning(Warning):
    @classmethod
    def warn(cls, id):
        warn('asset ' + str(id) + ' not found', AssetWarning)

Ancestors

  • builtins.Warning
  • builtins.Exception
  • builtins.BaseException

Static methods

def warn(id)
Expand source code
@classmethod
def warn(cls, id):
    warn('asset ' + str(id) + ' not found', AssetWarning)