#!/usr/bin/env python3
#
# _npc_.py
r"""
NPC\_ record type.
"""
#
# Copyright © 2024 Dominic Davis-Foster <dominic@davis-foster.co.uk>
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
# DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
# OR OTHER DEALINGS IN THE SOFTWARE.
#
# stdlib
import struct
from io import BytesIO
from typing import Iterator, NamedTuple, Tuple, Type
# 3rd party
import attrs
from typing_extensions import Self
# this package
from esp_parser.subrecords import ACBS, AIDT, EDID, OBND, Destruction, Item, Model
from esp_parser.types import (
BytesRecordType,
CStringRecord,
FaceGenRecord,
Float32Record,
FormIDRecord,
IntEnumField,
Record,
RecordType,
StructRecord,
Uint16Record
)
from esp_parser.utils import namedtuple_qualname_repr
__all__ = ["NPC_"]
[docs]class NPC_(Record):
"""
Non-Player Character.
"""
[docs] class FULL(CStringRecord):
"""
Name.
"""
# class SNAM(RecordType):
# """
# Faction.
#
# https://tes5edit.github.ioSubrecords/SNAM (CREA, NPC_).md
# """
[docs] class INAM(FormIDRecord):
"""
Death Item.
Form ID of a :class:`~.LVLI` record.
"""
[docs] class VTCK(FormIDRecord):
"""
Voice.
Form ID of a :class:`~.VTYP` record.
"""
[docs] class TPLT(FormIDRecord):
"""
Template.
Form ID of an :class:`~.NPC_` or :class:`~.LVLN` record.
"""
[docs] class RNAM(FormIDRecord):
"""
Race.
Form ID of a :class:`~.RACE`.
"""
[docs] class SPLO(FormIDRecord):
"""
Actor Effect.
Form ID of a :class:`~.SPEL` record.
"""
[docs] class EITM(FormIDRecord):
"""
Unarmed Attack Effect.
Form ID of an :class:`~.ENCH` or :class:`~.SPEL`.
"""
[docs] class EAMT(IntEnumField):
"""
Unarmed Attack Animation.
"""
AttackLeft = 26
AttackLeftUp = 27
AttackLeftDown = 28
AttackLeftIS = 29
AttackLeftISUp = 30
AttackLeftISDown = 31
AttackRight = 32
AttackRightUp = 33
AttackRightDown = 34
AttackRightIS = 35
AttackRightISUp = 36
AttackRightISDown = 37
Attack3 = 38
Attack3Up = 39
Attack3Down = 40
Attack3IS = 41
Attack3ISUp = 42
Attack3ISDown = 43
Attack4 = 44
Attack4Up = 45
Attack4Down = 46
Attack4IS = 47
Attack4ISUp = 48
Attack4ISDown = 49
Attack5 = 50
Attack5Up = 51
Attack5Down = 52
Attack5IS = 53
Attack5ISUp = 54
Attack5ISDown = 55
Attack6 = 56
Attack6Up = 57
Attack6Down = 58
Attack6IS = 59
Attack6ISUp = 60
Attack6ISDown = 61
Attack7 = 62
Attack7Up = 63
Attack7Down = 64
Attack7IS = 65
Attack7ISUp = 66
Attack7ISDown = 67
Attack8 = 68
Attack8Up = 69
Attack8Down = 70
Attack8IS = 71
Attack8ISUp = 72
Attack8ISDown = 73
AttackLoop = 74
AttackLoopUp = 75
AttackLoopDown = 76
AttackLoopIS = 77
AttackLoopISUp = 78
AttackLoopISDown = 79
AttackSpin = 80
AttackSpinUp = 81
AttackSpinDown = 82
AttackSpinIS = 83
AttackSpinISUp = 84
AttackSpinISDown = 85
AttackSpin2 = 86
AttackSpin2Up = 87
AttackSpin2Down = 88
AttackSpin2IS = 89
AttackSpin2ISUp = 90
AttackSpin2ISDown = 91
AttackPower = 92
AttackForwardPower = 93
AttackBackPower = 94
AttackLeftPower = 95
AttackRightPower = 96
PlaceMine = 97
PlaceMineUp = 98
PlaceMineDown = 99
PlaceMineIS = 100
PlaceMineISUp = 101
PlaceMineISDown = 102
PlaceMine2 = 103
PlaceMine2Up = 104
PlaceMine2Down = 105
PlaceMine2IS = 106
PlaceMine2ISUp = 107
PlaceMine2ISDown = 108
AttackThrow = 109
AttackThrowUp = 110
AttackThrowDown = 111
AttackThrowIS = 112
AttackThrowISUp = 113
AttackThrowISDown = 114
AttackThrow2 = 115
AttackThrow2Up = 116
AttackThrow2Down = 117
AttackThrow2IS = 118
AttackThrow2ISUp = 119
AttackThrow2ISDown = 120
AttackThrow3 = 121
AttackThrow3Up = 122
AttackThrow3Down = 123
AttackThrow3IS = 124
AttackThrow3ISUp = 125
AttackThrow3ISDown = 126
AttackThrow4 = 127
AttackThrow4Up = 128
AttackThrow4Down = 129
AttackThrow4IS = 130
AttackThrow4ISUp = 131
AttackThrow4ISDown = 132
AttackThrow5 = 133
AttackThrow5Up = 134
AttackThrow5Down = 135
AttackThrow5IS = 136
AttackThrow5ISUp = 137
AttackThrow5ISDown = 138
PipBoy = 167
PipBoyChild = 178
ANY = 255
[docs] @classmethod
def parse(cls: Type[Self], raw_bytes: BytesIO) -> Self:
"""
Parse this subrecord.
:param raw_bytes: Raw bytes for this record
"""
assert raw_bytes.read(2) == b"\x02\x00"
return cls(*struct.unpack("<H", raw_bytes.read(2)))
[docs] def unparse(self) -> bytes:
"""
Turn this subrecord back into raw bytes for an ESP file.
"""
return b"EAMT" + struct.pack("<HH", 2, self)
[docs] class SCRI(FormIDRecord):
"""
Script.
Form ID of a :class:`~.SCPT` record.
"""
[docs] class PKID(FormIDRecord):
"""
Package.
Form ID of a :class:`~.PACK` record.
"""
[docs] class CNAM(FormIDRecord):
"""
Class.
Form ID of a :class:`~.CLAS` record.
"""
[docs] class DATA(NamedTuple):
"""
Health and SPECIAL attributes.
"""
base_health: int
strength: int
perception: int
endurance: int
charisma: int
intelligence: int
agility: int
luck: int
# unused: int
[docs] @classmethod
def parse(cls: Type[Self], raw_bytes: BytesIO) -> Self:
"""
Parse this subrecord.
:param raw_bytes: Raw bytes for this record
"""
assert raw_bytes.read(2) == b"\x0b\x00" # size field
return cls(*struct.unpack("<iBBBBBBB", raw_bytes.read(11)))
assert raw_bytes.read(2) == b"\x0c\x00" # size field
return cls(*struct.unpack("<iBBBBBBBB", raw_bytes.read(12)))
[docs] def unparse(self) -> bytes:
"""
Turn this subrecord back into raw bytes for an ESP file.
"""
return b"DATA\x0b\x00" + struct.pack("<iBBBBBBB", *self)
return b"DATA\x0c\x00" + struct.pack("<iBBBBBBBB", *self)
def __repr__(self) -> str:
return namedtuple_qualname_repr(self)
[docs] @attrs.define
class DNAM(StructRecord):
"""
Skills.
"""
barter: int
big_guns: int
energy_weapons: int
explosives: int
lockpick: int
medicine: int
melee_weapons: int
repair: int
science: int
#: 'Guns' in Fallout New Vegas
small_guns: int
sneak: int
speech: int
#: Unused Throwing skill in Fallout 3
survival: int
unarmed: int
barter_offset: int
big_guns_offset: int
energy_weapons_offset: int
explosives_offset: int
lockpick_offset: int
medicine_offset: int
melee_weapons_offset: int
repair_offset: int
science_offset: int
small_guns_offset: int
sneak_offset: int
speech_offset: int
#: Unused Throwing skill in Fallout 3
survival_offset: int
unarmed_offset: int
[docs] @staticmethod
def get_struct_and_size() -> Tuple[str, int]:
"""
Returns the pack/unpack struct string and the corresponding size.
"""
return "<BBBBBBBBBBBBBBBBBBBBBBBBBBBB", 28
[docs] @staticmethod
def get_field_names() -> Tuple[str, ...]:
"""
Returns a list of attributes on this class in the order they should be packed.
"""
return (
"barter",
"big_guns",
"energy_weapons",
"explosives",
"lockpick",
"medicine",
"melee_weapons",
"repair",
"science",
"small_guns",
"sneak",
"speech",
"survival",
"unarmed",
"barter_offset",
"big_guns_offset",
"energy_weapons_offset",
"explosives_offset",
"lockpick_offset",
"medicine_offset",
"melee_weapons_offset",
"repair_offset",
"science_offset",
"small_guns_offset",
"sneak_offset",
"speech_offset",
"survival_offset",
"unarmed_offset",
)
[docs] class PNAM(FormIDRecord):
"""
Head Part.
Form ID of a :class:`~.HDPT` record.
"""
[docs] class HNAM(FormIDRecord):
"""
Hair.
Form ID of a :class:`~.HAIR` record.
"""
[docs] class LNAM(Float32Record):
"""
Hair Length.
"""
[docs] class ENAM(FormIDRecord):
"""
Eyes.
Form ID of a :class:`~.EYES` record.
"""
[docs] class HCLR(BytesRecordType):
"""
Hair Color.
RGBA as bytes.
"""
[docs] @classmethod
def parse(cls: Type[Self], raw_bytes: BytesIO) -> Self:
"""
Parse this subrecord.
:param raw_bytes: Raw bytes for this record
"""
assert raw_bytes.read(2) == b"\x04\x00"
return cls(raw_bytes.read(4))
[docs] def unparse(self) -> bytes:
"""
Turn this subrecord back into raw bytes for an ESP file.
"""
return b"HCLR" + b"\x04\x00" + bytes(self)
[docs] class ZNAM(FormIDRecord):
"""
Combat Style.
Form ID of a :class:`~.CSTY` record.
"""
[docs] class NAM4(IntEnumField):
"""
Impact Material Type.
"""
stone = 0
dirt = 1
grass = 2
glass = 3
metal = 4
wood = 5
organic = 6
cloth = 7
water = 8
hollow_metal = 9
organic_bug = 10
organic_glow = 11
[docs] @classmethod
def parse(cls: Type[Self], raw_bytes: BytesIO) -> Self:
"""
Parse this subrecord.
:param raw_bytes: Raw bytes for this record
"""
assert raw_bytes.read(2) == b"\x04\x00"
return cls(*struct.unpack("<I", raw_bytes.read(4)))
[docs] def unparse(self) -> bytes:
"""
Turn this subrecord back into raw bytes for an ESP file.
"""
return b"NAM4" + struct.pack("<HI", 4, self)
RecordType.register(DATA)
RecordType.register(EAMT)
RecordType.register(NAM4)
[docs] class FGGS(FaceGenRecord):
"""
FaceGen Geometry-Symmetric.
"""
[docs] class FGGA(FaceGenRecord):
"""
FaceGen Geometry-Asymmetric.
"""
[docs] class FGTS(FaceGenRecord):
"""
FaceGen Texture-Symmetric.
"""
[docs] class NAM5(Uint16Record):
"""
Unknown.
"""
[docs] class NAM6(Float32Record):
"""
Height.
"""
[docs] class NAM7(Float32Record):
"""
Weight.
"""
[docs] @classmethod
def parse_subrecords(cls, raw_bytes: BytesIO) -> Iterator[RecordType]:
"""
Parse this record's subrecords.
:param raw_bytes: Raw bytes for this record's subrecords
"""
while True:
record_type = raw_bytes.read(4)
if not record_type:
break
if record_type == b"EDID":
yield EDID.parse(raw_bytes)
elif record_type == b"OBND":
yield OBND.parse(raw_bytes)
elif record_type == b"ACBS":
yield ACBS.parse(raw_bytes)
elif record_type == b"AIDT":
yield AIDT.parse(raw_bytes)
elif record_type in {
b"CNAM",
b"DATA",
b"DNAM",
b"EAMT",
b"EITM",
b"ENAM",
b"FGGA",
b"FGGS",
b"FGTS",
b"FULL",
b"HCLR",
b"HNAM",
b"INAM",
b"LNAM",
b"NAM4",
b"NAM5",
b"NAM6",
b"NAM7",
b"PKID",
b"PNAM",
b"RNAM",
b"SCRI",
b"SNAM",
b"SPLO",
b"TPLT",
b"VTCK",
b"ZNAM",
}:
yield getattr(cls, record_type.decode()).parse(raw_bytes)
elif record_type in Model.members:
yield Model.parse_member(record_type, raw_bytes)
elif record_type in Item.members:
yield Item.parse_member(record_type, raw_bytes)
elif record_type in Destruction.members:
yield Destruction.parse_member(record_type, raw_bytes)
else:
raise NotImplementedError(record_type)