Source code for tibiawikisql.models.npc

#  Copyright 2018 Allan Galarza
#
#  Licensed under the Apache License, Version 2.0 (the "License");
#  you may not use this file except in compliance with the License.
#  You may obtain a copy of the License at
#
#  http://www.apache.org/licenses/LICENSE-2.0
#
#  Unless required by applicable law or agreed to in writing, software
#  distributed under the License is distributed on an "AS IS" BASIS,
#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#  See the License for the specific language governing permissions and
#  limitations under the License.

import re
import sqlite3

from tibiawikisql import schema
from tibiawikisql.models import abc
from tibiawikisql.utils import clean_links, convert_tibiawiki_position

price_to_template = re.compile(r"{{Price to (?:Buy|Sell)\s*([^}]+)}}")
npc_offers = re.compile(r"\|([^|:\[]+)(?::\s?(\d+))?(?:\s?\[\[([^\]]+))?")

teaches_template = re.compile(r"{{Teaches\s*(?:\|name=([^|]+))?([^}]+)}}")
spells_pattern = re.compile(r"\|([^|]+)")

trades_sell_template = re.compile(r"{{Trades/Sells\s*(?:\|note=([^|]+))?([^}]+)}}")
npc_trades = re.compile(r"\|([^|,\[]+)(?:,\s?([+-]?\d+))?(?:\s?\[\[([^\]]+))?")

transport_template = re.compile(r"{{Transport\s*(?:\|discount=([^|]+))?([^}]+)}}")
npc_destinations = re.compile(r"\|([^,]+),\s?(\d+)(?:;\s?([^|]+))?")

ilink_pattern = re.compile(r"{{Ilink\|([^}]+)}}")


def parse_destinations(value):
    """
        Parses an NPC destinations into a list of tuples.

        The tuple contains the  destination's name, price and notes.
        Price and notes may not be present.

        Parameters
        ----------
        value: :class:`str`
            A string containing destinations.

        Returns
        -------
        list(:class:`tuple`)
            A list of tuples containing the parsed destinations.
        """
    result = []
    for __, destinations in transport_template.findall(value):
        result.extend(npc_destinations.findall(destinations))
    return result


def parse_item_offers(value):
    """
    Parses NPC item offers into a list of tuples.

    The tuple contains the item's name, price and currency.
    Price and currency may not be present.

    Parameters
    ----------
    value: :class:`str`
        The string  containing NPC offers.

    Returns
    -------
    list(:class:`tuple`)
        A list of tuples containing the parsed offers.
    """
    match = price_to_template.search(value)
    if match:
        return npc_offers.findall(match.group(1))
    else:
        return []


def parse_item_trades(value):
    """
    Parses an NPC item trades into a list of tuples.

    The tuple contains the item's name, price and currency.
    Price and currency may not be present.

    Parameters
    ----------
    value: :class:`str`
        A string containing item trades.

    Returns
    -------
    list(:class:`tuple`)
        A list of tuples containing the parsed offers.
    """
    result = []
    value = replace_ilinks(value)
    for note, trades in trades_sell_template.findall(value):
        result.extend(npc_trades.findall(trades))
    return result


def parse_spells(value):
    """Parses an NPC's teacheable spells.

    Parameters
    ----------
    value: :class:`str`
        A string containing teachable spells.

    Returns
    -------
    A list of spells grouped by vocation.
    """
    result = []
    for name, spell_list in teaches_template.findall(value):
        spells = spells_pattern.findall(spell_list)
        spells = [s.strip() for s in spells]
        result.append((name, spells))
    return result


def replace_ilinks(value):
    """Replaces the ILink template with a regular link.

    Parameters
    ----------
    value: :class:`str`
        A string containing ILink templates.

    Returns
    -------
    :class:`str`
        The string with regular links instead of ILink templates.
    """
    return ilink_pattern.sub("[[\g<1>]]", value)


[docs]class Npc(abc.Row, abc.Parseable, table=schema.Npc): """ Represents a non-playable character. Attributes ---------- article_id: :class:`int` The id of the containing article. title: :class:`str` The title of the containing article. timestamp: :class:`int` The last time the containing article was edited. name: :class:`str` The in-game name of the NPC. gender: :class:`str` The gender of the NPC. race: :class:`str` The race of the NPC. job: :class:`str` The NPC's job. location: :class:`str` The location of the NPC. city: :class:`str` The city where the NPC is located. x: :class:`int` The x coordinates of the NPC. y: :class:`int` The y coordinates of the NPC. z: :class:`int` The z coordinates of the NPC. version: :class:`str` The client version where the NPC was implemented. image: :class:`bytes` The NPC's image in bytes. sell_offers: list of :class:`NpcSellOffer` Items sold by the NPC. buy_offers: list of :class:`NpcBuyOffer` Items bought by the NPC. destinations: list of :class:`NpcSellOffer` Places where the NPC can travel to. teaches: list of :class:`NpcSpell` Spells this NPC can teach. """ __slots__ = ("article_id", "title", "timestamp", "name", "gender", "race", "job", "location", "city", "x", "y", "z", "version", "image", "sell_offers", "buy_offers", "destinations", "teaches") def __init__(self, **kwargs): super().__init__(**kwargs) _map = { "name": ("name", lambda x: x), "actualname": ("name", lambda x: x), "location": ("location", clean_links), "gender": ("gender", lambda x: x), "race": ("race", lambda x: x), "job": ("job", lambda x: x), "city": ("city", lambda x: x), "posx": ("x", convert_tibiawiki_position), "posy": ("y", convert_tibiawiki_position), "posz": ("z", int), "implemented": ("version", lambda x: x) } _pattern = re.compile(r"Infobox[\s_]NPC")
[docs] @classmethod def from_article(cls, article): npc = super().from_article(article) if npc is None: return None if "buys" in npc._raw_attributes: cls._parse_buy_offers(npc) if "sells" in npc._raw_attributes: cls._parse_sell_offers(npc) cls._parse_spells(npc) destinations = [] if "notes" in npc._raw_attributes and "{{Transport" in npc._raw_attributes["notes"]: destinations.extend(parse_destinations(npc._raw_attributes["notes"])) if "sells" in npc._raw_attributes and "{{Transport" in npc._raw_attributes["sells"]: destinations.extend(parse_destinations(npc._raw_attributes["sells"])) npc.destinations = [] for destination, price, notes in destinations: destination.strip() notes = clean_links(notes.strip()) price = int(price) if not notes: notes = None npc.destinations.append(NpcDestination(npc_id=npc.article_id, name=destination, price=price, notes=notes)) return npc
@classmethod def _parse_buy_offers(cls, npc): buy_items = parse_item_offers(npc._raw_attributes["buys"]) npc.buy_offers = [] for item, price, currency in buy_items: # Some items have extra requirements, separated with ;, so we remove them item = item.split(";")[0] if not currency.strip(): currency = "Gold Coin" value = None if price.strip(): value = int(price) npc.buy_offers.append(NpcBuyOffer(item_title=item.strip(), currency_title=currency, value=value, npc_id=npc.article_id)) @classmethod def _parse_sell_offers(cls, npc): sell_items = parse_item_offers(npc._raw_attributes["sells"]) npc.sell_offers = [] for item, price, currency in sell_items: # Some items have extra requirements, separated with ;, so we remove them item = item.split(";")[0] if not currency.strip(): currency = "Gold Coin" value = None if price.strip(): value = int(price) npc.sell_offers.append(NpcSellOffer(item_title=item.strip(), currency_title=currency, value=value, npc_id=npc.article_id)) # Items traded by npcs (these have a different template) trade_items = parse_item_trades(npc._raw_attributes["sells"]) for item, price, currency in trade_items: item = item.split(";")[0] value = None if price.strip(): value = abs(int(price)) if not currency.strip(): currency = "Gold Coin" npc.sell_offers.append(NpcSellOffer(item_title=item.strip(), currency_title=currency, value=value, npc_id=npc.article_id)) @classmethod def _parse_spells(cls, npc): spell_list = parse_spells(npc._raw_attributes["sells"]) npc.teaches = [] for group, spells in spell_list: for spell in spells: spell = spell.strip() knight = "knight" in group.lower() or npc.name == "Eliza" paladin = "paladin" in group.lower() or npc.name == "Ursula" or npc.name == "Eliza" druid = "druid" in group.lower() or npc.name == "Elathriel" or npc.name == "Eliza" sorcerer = "sorcerer" in group.lower() or npc.name == "Eliza" if not(knight or paladin or druid or sorcerer): def in_jobs(vocation, _npc): return vocation in (_npc.job+_npc._raw_attributes.get("job2", "") + _npc._raw_attributes.get("job3", "")).lower() knight = in_jobs("knight", npc) paladin = in_jobs("paladin", npc) druid = in_jobs("druid", npc) sorcerer = in_jobs("sorcerer", npc) exists = False for j, s in enumerate(npc.teaches): # Spell was already in list, so we update vocations if s.spell_title == spell: npc.teaches[j] = NpcSpell(npc_id=npc.article_id, spell_title=spell, knight=knight or s.knight, paladin=paladin or s.paladin, druid=druid or s.druid, sorcerer=sorcerer or s.sorcerer) exists = True break if not exists: npc.teaches.append(NpcSpell(npc_id=npc.article_id, spell_title=spell, knight=knight, paladin=paladin, druid=druid, sorcerer=sorcerer))
[docs] def insert(self, c): super().insert(c) for offer in getattr(self, "buy_offers", []): offer.insert(c) for offer in getattr(self, "sell_offers", []): offer.insert(c) for spell in getattr(self, "teaches", []): spell.insert(c) for destination in getattr(self, "destinations", []): destination.insert(c)
[docs] @classmethod def get_by_field(cls, c, field, value, use_like=False): npc: cls = super().get_by_field(c, field, value, use_like) if npc is None: return None npc.sell_offers = NpcSellOffer.search(c, "npc_id", npc.article_id, sort_by="value", ascending=True) npc.buy_offers = NpcBuyOffer.search(c, "npc_id", npc.article_id, sort_by="value", ascending=False) npc.teaches = NpcSpell.search(c, "npc_id", npc.article_id) npc.destinations = NpcDestination.search(c, "npc_id", npc.article_id) return npc
class NpcOffer: def __init__(self, **kwargs): self.npc_id = kwargs.get("npc_id") self.npc_title = kwargs.get("npc_title") self.item_id = kwargs.get("item_id") self.item_title = kwargs.get("item_title") self.currency_id = kwargs.get("currency_id") self.currency_title = kwargs.get("currency_title") self.npc_city = kwargs.get("npc_city") self.value = kwargs.get("value") def __repr__(self): attributes = [] for attr in self.__slots__: try: v = getattr(self, attr) if v is None: continue attributes.append("%s=%r" % (attr, v)) except AttributeError: pass return "{0.__class__.__name__}({1})".format(self, ",".join(attributes))
[docs]class NpcSellOffer(NpcOffer, abc.Row, table=schema.NpcSelling): """ Represents an item sellable by an NPC. Attributes ---------- npc_id: :class:`int` The article id of the npc that sells the item. npc_title: :class:`str` The title of the npc that sells the item. npc_city: :class:`str` The city where the NPC is located. item_id: :class:`int` The id of the item sold by the npc. item_title: :class:`str` The title of the item sold by the npc. currency_id: :class:`int` The item id of the currency used to buy the item. currency_title: :class:`str` The title of the currency used to buy the item value: :class:`str` The value of the item in the specified currency. """ __slots__ = ("npc_id", "npc_title", "npc_city", "item_id", "item_title", "value", "currency_id", "currency_title") def __init__(self, **kwargs): super().__init__(**kwargs)
[docs] def insert(self, c): try: if getattr(self, "item_id", None) and getattr(self, "value", None) and getattr(self, "currency_id", None): super().insert(c) elif getattr(self, "value", 0): query = f"""INSERT INTO {self.table.__tablename__}({','.join(col.name for col in self.table.columns)}) VALUES( ?, (SELECT article_id from item WHERE title = ?), ?, (SELECT article_id from item WHERE title = ?))""" c.execute(query, (self.npc_id, self.item_title, self.value, self.currency_title)) else: query = f"""INSERT INTO {self.table.__tablename__}({','.join(col.name for col in self.table.columns)}) VALUES( ?, (SELECT article_id from item WHERE title = ?), (SELECT value_buy from item WHERE title = ?), (SELECT article_id from item WHERE title = ?))""" c.execute(query, (self.npc_id, self.item_title, self.item_title, self.currency_title)) except sqlite3.IntegrityError: pass
@classmethod def _is_column(cls, name): return name in cls.__slots__ @classmethod def _get_base_query(cls): return """SELECT %s.*, item.title as item_title, npc.title as npc_title, npc.city as npc_city, currency.title as currency_title FROM %s LEFT JOIN npc ON npc.article_id = npc_id LEFT JOIN item ON item.article_id = item_id LEFT JOIN item currency on currency.article_id = currency_id """ % (cls.table.__tablename__, cls.table.__tablename__)
[docs]class NpcBuyOffer(NpcOffer, abc.Row, table=schema.NpcBuying): """ Represents an item buyable by an NPC. Attributes ---------- npc_id: :class:`int` The article id of the npc that buys the item. npc_title: :class:`str` The title of the npc that buys the item. npc_city: :class:`str` The city where the NPC is located. item_id: :class:`int` The id of the item bought by the npc. item_title: :class:`str` The title of the item bought by the npc. currency_id: :class:`int` The item id of the currency used to sell the item. currency_title: :class:`str` The title of the currency used to sell the item value: :class:`str` The value of the item in the specified currency. """ __slots__ = ("npc_id", "npc_title", "npc_city", "item_id", "item_title", "value", "currency_id", "currency_title") def __init__(self, **kwargs): super().__init__(**kwargs)
[docs] def insert(self, c): try: if getattr(self, "item_id", None) and getattr(self, "value", None) and getattr(self, "currency_id", None): super().insert(c) elif getattr(self, "value", 0): query = f"""INSERT INTO {self.table.__tablename__}({','.join(col.name for col in self.table.columns)}) VALUES( ?, (SELECT article_id from item WHERE title = ?), ?, (SELECT article_id from item WHERE title = ?))""" c.execute(query, (self.npc_id, self.item_title, self.value, self.currency_title)) else: query = f"""INSERT INTO {self.table.__tablename__}({','.join(col.name for col in self.table.columns)}) VALUES( ?, (SELECT article_id from item WHERE title = ?), (SELECT value_sell from item WHERE title = ?), (SELECT article_id from item WHERE title = ?))""" c.execute(query, (self.npc_id, self.item_title, self.item_title, self.currency_title)) except sqlite3.IntegrityError: pass
@classmethod def _is_column(cls, name): return name in cls.__slots__ @classmethod def _get_base_query(cls): return """SELECT %s.*, item.title as item_title, npc.title as npc_title, npc.city as npc_city, currency.title as currency_title FROM %s LEFT JOIN npc ON npc.article_id = npc_id LEFT JOIN item ON item.article_id = item_id LEFT JOIN item currency on currency.article_id = currency_id """ % (cls.table.__tablename__, cls.table.__tablename__)
[docs]class NpcSpell(abc.Row, table=schema.NpcSpell): """ Represents a spell that a NPC can teach. Attributes ---------- npc_id: :class:`int` The article id of the npc that teaches the spell. npc_title: :class:`str` The title of the npc that teaches the spell. spell_id: :class:`int` The article id of the spell taught by the npc. spell_title: :class:`str` The title of the spell taught by the npc. price: :class:`int` The price paid to have this spell taught. npc_city: :class:`str` The city where the NPC is located. knight: :class:`bool` If the spell is taught to knights. paladin: :class:`bool` If the spell is taught to paladins. druid: :class:`bool` If the spell is taught to druids. sorcerer: :class:`bool` If the spell is taught to sorcerers. """ __slots__ = ("npc_id", "npc_title", "npc_city", "spell_id", "spell_title", "price", "knight", "sorcerer", "paladin", "druid") def __init__(self, **kwargs): super().__init__(**kwargs) self.npc_city = kwargs.get("npc_city") self.npc_title = kwargs.get("npc_title") self.spell_title = kwargs.get("spell_title") self.price = kwargs.get("price") def __repr__(self): attributes = [] for attr in self.__slots__: try: v = getattr(self, attr) if v is None: continue if isinstance(v, bool) and not v: continue attributes.append("%s=%r" % (attr, v)) except AttributeError: pass return "{0.__class__.__name__}({1})".format(self, ",".join(attributes))
[docs] def insert(self, c): if getattr(self, "spell_id", None): super().insert(c) else: query = f"""INSERT INTO {self.table.__tablename__}({','.join(c.name for c in self.table.columns)}) VALUES(?, (SELECT article_id from spell WHERE title = ?), ?, ?, ?, ?)""" c.execute(query, (self.npc_id, self.spell_title, self.knight, self.sorcerer, self.paladin, self.druid))
@classmethod def _is_column(cls, name): return name in cls.__slots__ @classmethod def _get_base_query(cls): return """SELECT %s.*, spell.title as spell_title, npc.title as npc_title, spell.price as price, npc.city as npc_city FROM %s LEFT JOIN npc ON npc.article_id = npc_id LEFT JOIN spell ON spell.article_id = spell_id""" % (cls.table.__tablename__, cls.table.__tablename__)
[docs]class NpcDestination(abc.Row, table=schema.NpcDestination): """ Represents a NPC's travel destination Attributes ---------- npc_id: :class:`int` The article id of the NPC. name: :class:`str` The name of the destination price: :class:`int` The price in gold to travel. notes: :class:`str` Notes about the destination, such as requirements. """ __slots__ = ("npc_id", "name", "price", "notes") def __init__(self, **kwargs): super().__init__(**kwargs)
[docs]class RashidPosition(abc.Row, table=schema.RashidPosition): """Represents a Rashid position. Attributes ----------- day: :class:`int` Day of the week, Monday starts at 0. x: :class:`int` The x coordinate of Rashid that day. y: :class:`int` The y coordinate of Rashid that day. z: :class:`int` The z coordinate of Rashid that day. city: :class:`str` The city where Rashid is that day. location: :class:`str` The location where Rashid is that day. """ __slots__ = ("day", "x", "y", "z", "city", "location") def __init__(self, **kwargs): super().__init__(**kwargs)
rashid_positions = [ RashidPosition(day=0, x=32210, y=31157, z=7, city="Svargrond", location="Dankwart's Tavern, south of the temple."), RashidPosition(day=1, x=32303, y=32834, z=7, city="Liberty Bay", location="Lyonel's tavern, west of the depot."), RashidPosition(day=2, x=32578, y=32754, z=7, city="Port Hope", location="Clyde's tavern, west of the depot."), RashidPosition(day=3, x=33068, y=32879, z=6, city="Ankrahmun", location="Arito's tavern, above the post office."), RashidPosition(day=4, x=33239, y=32480, z=7, city="Darashia", location="Miraia's tavern, south of the guildhalls."), RashidPosition(day=5, x=33172, y=31813, z=6, city="Edron", location="Mirabell's tavern, above the depot."), RashidPosition(day=6, x=32326, y=31784, z=6, city="Carlin", location="Carlin depot, one floor above.") ]