#!/usr/bin/env python3
#
# _weap.py
"""
WEAP 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, Type
# 3rd party
import attrs
from typing_extensions import Self
# this package
from esp_parser.subrecords import EDID, OBND, Destruction, Model
from esp_parser.types import (
CStringRecord,
FormIDRecord,
Int16Record,
Int32Record,
Record,
RecordType,
Uint32Record
)
from esp_parser.utils import namedtuple_qualname_repr
__all__ = ["WEAP"]
[docs]class WEAP(Record):
"""
Weapon.
"""
[docs] class FULL(CStringRecord):
"""
Name.
"""
[docs] class ICON(CStringRecord):
"""
Large icon filename.
"""
[docs] class MICO(CStringRecord):
"""
Small icon filename.
"""
[docs] class SCRI(FormIDRecord):
"""
Script.
Form ID of a :class:`~.SCPT` record.
"""
[docs] class EITM(FormIDRecord):
"""
Object Effect.
Form ID of an :class:`~.ENCH` or :class:`~.SPEL` record.
"""
[docs] class EAMT(Int16Record):
"""
Enchantment Charge Amount.
"""
[docs] class NAM0(FormIDRecord):
"""
Ammo.
Form ID of an :class:`~.AMMO` or :class:`~.FLST` record.
"""
[docs] class REPL(FormIDRecord):
"""
Repair List.
Form ID of a :class:`~.FLST` record.
"""
[docs] class ETYP(Int32Record):
"""
Equipment Type.
See https://tes5edit.github.io/fopdoc/FalloutNV/Records/Subrecords/ETYP.html
"""
[docs] class BIPL(FormIDRecord):
"""
Biped Model List.
Form ID of a FLST record.
"""
[docs] class YNAM(FormIDRecord):
"""
Sound - Pick Up.
Form ID of a :class:`~.SOUN` record.
"""
[docs] class ZNAM(FormIDRecord):
"""
Sound - Drop.
Form ID of a :class:`~.SOUN` record.
"""
[docs] class EFSD(FormIDRecord):
"""
Scope Effect.
Form ID of an EFSH record.
"""
[docs] class MWD1(CStringRecord):
"""
Model With Mod 1 (New Vegas only).
"""
[docs] class MWD2(CStringRecord):
"""
Model With Mod 2 (New Vegas only).
"""
[docs] class MWD3(CStringRecord):
"""
Model With Mods 1 and 2 (New Vegas only).
"""
[docs] class MWD4(CStringRecord):
"""
Model With Mod 3 (New Vegas only).
"""
[docs] class MWD5(CStringRecord):
"""
Model With Mods 1 and 3 (New Vegas only).
"""
[docs] class MWD6(CStringRecord):
"""
Model With Mods 2 and 3 (New Vegas only).
"""
[docs] class MWD7(CStringRecord):
"""
Model With Mods 1, 2 and 3 (New Vegas only).
"""
[docs] class VANM(CStringRecord):
"""
VATS Attack Name (New Vegas only).
"""
[docs] class NNAM(CStringRecord):
"""
Embedded Weapon Node.
"""
[docs] class INAM(FormIDRecord):
"""
Impact Dataset.
Form ID of a :class:`~.IPDS` record.
"""
[docs] class WNAM(FormIDRecord):
"""
First Person Model.
Form ID of a :class:`~.STAT` record.
"""
[docs] class WNM1(FormIDRecord):
"""
1st Person Model With Mod 1 (New Vegas only).
Form ID of a :class:`~.STAT` record.
"""
[docs] class WNM2(FormIDRecord):
"""
1st Person Model With Mod 2 (New Vegas only).
Form ID of a :class:`~.STAT` record.
"""
[docs] class WNM3(FormIDRecord):
"""
1st Person Model With Mods 1 and 2 (New Vegas only).
Form ID of a :class:`~.STAT` record.
"""
[docs] class WNM4(FormIDRecord):
"""
1st Person Model With Mod 3 (New Vegas only).
Form ID of a :class:`~.STAT` record.
"""
[docs] class WNM5(FormIDRecord):
"""
1st Person Model With Mods 1 and 3 (New Vegas only).
Form ID of a :class:`~.STAT` record.
"""
[docs] class WNM6(FormIDRecord):
"""
1st Person Model With Mods 2 and 3 (New Vegas only).
Form ID of a :class:`~.STAT` record.
"""
[docs] class WNM7(FormIDRecord):
"""
1st Person Model With Mods 1, 2 and 3 (New Vegas only).
Form ID of a :class:`~.STAT` record.
"""
[docs] class WMI1(FormIDRecord):
"""
Weapon Mod 1 (New Vegas only).
Form ID of an :class:`~.IMOD` record.
"""
[docs] class WMI2(FormIDRecord):
"""
Weapon Mod 2 (New Vegas only).
Form ID of an :class:`~.IMOD` record.
"""
[docs] class WMI3(FormIDRecord):
"""
Weapon Mod 3 (New Vegas only).
Form ID of an :class:`~.IMOD` record.
"""
[docs] class SNAM(FormIDRecord):
"""
Sound - Gun - Shoot 3D / Shoot Dist.
Form ID of a :class:`~.SOUN` record.
"""
[docs] class XNAM(FormIDRecord):
"""
Sound - Gun - Shoot 2D.
Form ID of a :class:`~.SOUN` record.
"""
[docs] class NAM7(FormIDRecord):
"""
Sound - Gun - Shoot 3D Looping.
Form ID of a :class:`~.SOUN` record.
"""
[docs] class TNAM(FormIDRecord):
"""
Sound - Melee - Swing / Gun - No Ammo.
Form ID of a :class:`~.SOUN` record.
"""
[docs] class NAM6(FormIDRecord):
"""
Sound - Block.
Form ID of a :class:`~.SOUN` record.
"""
[docs] class UNAM(FormIDRecord):
"""
Sound - Idle.
Form ID of a :class:`~.SOUN` record.
"""
[docs] class NAM9(FormIDRecord):
"""
Sound - Equip.
Form ID of a :class:`~.SOUN` record.
"""
[docs] class NAM8(FormIDRecord):
"""
Sound - Unequip.
Form ID of a :class:`~.SOUN` record.
"""
[docs] class WMS1(FormIDRecord):
"""
Sound - Mod 1 - Shoot 3D / Dist (New Vegas only).
Form ID of a :class:`~.SOUN` record.
"""
[docs] class WMS2(FormIDRecord):
"""
Sound - Mod 1 - Shoot 2D (New Vegas only).
Form ID of a :class:`~.SOUN` record.
"""
[docs] class DATA(NamedTuple):
"""
Weapon value, health (conditon), weight etc.
"""
value: int
health: int
weight: float
base_damage: int
clip_size: 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"\x0f\x00" # size field
return cls(*struct.unpack("<iifhB", raw_bytes.read(15)))
[docs] def unparse(self) -> bytes:
"""
Turn this subrecord back into raw bytes for an ESP file.
"""
return b"DATA\x0f\x00" + struct.pack("<iifhB", *self)
def __repr__(self) -> str:
return namedtuple_qualname_repr(self)
RecordType.register(DATA)
[docs] @attrs.define
class DNAM(RecordType):
"""
Weapon animation, projectile, mod data etc.
"""
# # See https://tes5edit.github.io/fopdoc/Fallout3/Records/WEAP.html for enum and flag values
animation_type: int # enum
animation_multiplier: float
reach: float
flags: int
grip_animation: int # enum
ammo_use: int
reload_animation: int # enum
min_spread: float
spread: float
unknown: bytes
sight_fov: float
unused: bytes
#: 4-byte form id of a :class:`~.PROJ` record, or null.
projectile: bytes
base_vats_hit_chance: int
attack_animation: int # enum
projectile_count: int
embedded_weapon_actor_value: int # enum
min_range: float
max_range: float
on_hit: int # enum
flags_: int
animation_attack_multiplier: float
fire_rate: float
override_action_points: float
rumble_left_motor_strength: float
rumble_right_motor_strength: float
rumble_duration: float
override_damage_to_weapon_mult: float
attack_shots_per_sec: float
reload_time: float
jam_time: float
aim_arc: float
skill: int # enum
rumble_pattern: int # enum
rumble_wavelength: float
limb_damage_multiplier: float
resistance_type: int # enum
sight_usage: float
semi_automatic_fire_delay_min: float
semi_automatic_fire_delay_max: float
# The following are New Vegas only
unknown__: float = 0
effect_mod_1: int = 0
effect_mod_2: int = 0
effect_mod_3: int = 0
value_a_mod_1: float = 0
value_a_mod_2: float = 0
value_a_mod_3: float = 0
power_attack_animation_override: int = 0
strength_requirement: int = 0
unknown___: bytes = b''
reload_animation_mod: int = 0
unknown____: bytes = b''
regen_rate: float = 0
kill_impulse: float = 0
value_b_mod_1: float = 0
value_b_mod_2: float = 0
value_b_mod_3: float = 0
impulse_dist: float = 0
skill_requirement: int = 0
#: Indicates that the New Vegas-specific fields should be included with ``unparse()``.
new_vegas: bool = False
[docs] @classmethod
def parse(cls: Type[Self], raw_bytes: BytesIO) -> Self:
"""
Parse this subrecord.
:param raw_bytes: Raw bytes for this record
"""
size = struct.unpack("<H", raw_bytes.read(2))[0]
unpack_string = "<IffBBBBff4sf4s4sBBBBffIIfffffffffffiIffifff"
if size == 204:
# New Vegas
unpack_string += "fIIIfffIIsB2sffffffI"
return cls( # type: ignore[misc] # false positive re: new_vegas=True being there twice (it isn't)
*struct.unpack(unpack_string, raw_bytes.read(size)),
new_vegas=True,
)
else:
assert size == 136
# Fallout 3
return cls(*struct.unpack(unpack_string, raw_bytes.read(size)))
[docs] def unparse(self) -> bytes:
"""
Turn this subrecord back into raw bytes for an ESP file.
"""
if self.new_vegas:
packed = struct.pack(
"<IffBBBBff4sf4s4sBBBBffIIfffffffffffiIffiffffIIIfffIIsB2sffffffI",
self.animation_type,
self.animation_multiplier,
self.reach,
self.flags,
self.grip_animation,
self.ammo_use,
self.reload_animation,
self.min_spread,
self.spread,
self.unknown,
self.sight_fov,
self.unused,
self.projectile,
self.base_vats_hit_chance,
self.attack_animation,
self.projectile_count,
self.embedded_weapon_actor_value,
self.min_range,
self.max_range,
self.on_hit,
self.flags_,
self.animation_attack_multiplier,
self.fire_rate,
self.override_action_points,
self.rumble_left_motor_strength,
self.rumble_right_motor_strength,
self.rumble_duration,
self.override_damage_to_weapon_mult,
self.attack_shots_per_sec,
self.reload_time,
self.jam_time,
self.aim_arc,
self.skill,
self.rumble_pattern,
self.rumble_wavelength,
self.limb_damage_multiplier,
self.resistance_type,
self.sight_usage,
self.semi_automatic_fire_delay_min,
self.semi_automatic_fire_delay_max,
self.unknown__,
self.effect_mod_1,
self.effect_mod_2,
self.effect_mod_3,
self.value_a_mod_1,
self.value_a_mod_2,
self.value_a_mod_3,
self.power_attack_animation_override,
self.strength_requirement,
self.unknown___,
self.reload_animation_mod,
self.unknown____,
self.regen_rate,
self.kill_impulse,
self.value_b_mod_1,
self.value_b_mod_2,
self.value_b_mod_3,
self.impulse_dist,
self.skill_requirement,
)
else:
# FO3 Only
packed = struct.pack(
"<IffBBBBff4sf4s4sBBBBffIIfffffffffffiIffifff",
self.animation_type,
self.animation_multiplier,
self.reach,
self.flags,
self.grip_animation,
self.ammo_use,
self.reload_animation,
self.min_spread,
self.spread,
self.unknown,
self.sight_fov,
self.unused,
self.projectile,
self.base_vats_hit_chance,
self.attack_animation,
self.projectile_count,
self.embedded_weapon_actor_value,
self.min_range,
self.max_range,
self.on_hit,
self.flags_,
self.animation_attack_multiplier,
self.fire_rate,
self.override_action_points,
self.rumble_left_motor_strength,
self.rumble_right_motor_strength,
self.rumble_duration,
self.override_damage_to_weapon_mult,
self.attack_shots_per_sec,
self.reload_time,
self.jam_time,
self.aim_arc,
self.skill,
self.rumble_pattern,
self.rumble_wavelength,
self.limb_damage_multiplier,
self.resistance_type,
self.sight_usage,
self.semi_automatic_fire_delay_min,
self.semi_automatic_fire_delay_max,
)
return b"DNAM\xcc\x00" + packed
[docs] @attrs.define
class CRDT(RecordType):
"""
Critical Data.
"""
critical_damage: int
unused: bytes
ctit_percent_mul: float
flags: int # see https://tes5edit.github.io/fopdoc/Fallout3/Records/WEAP.html
unused_: bytes
#: Form ID of a :class:`~.SPEL` record, or null.s
effect: 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"\x10\x00" # size field
return cls(*struct.unpack("<H2sfB3s4s", raw_bytes.read(16)))
[docs] def unparse(self) -> bytes:
"""
Turn this subrecord back into raw bytes for an ESP file.
"""
packed = struct.pack(
"<H2sfB3s4s",
self.critical_damage,
self.unused,
self.ctit_percent_mul,
self.flags,
self.unused_,
self.effect,
)
return b"CRDT\x10\x00" + packed
[docs] @attrs.define
class VATS(RecordType):
"""
VATS (New Vegas only).
"""
#: Form ID of a :class:`~.SPEL`, or null.
effect: bytes
skill: float
damage_multiplier: float
ap: float
silent: int # Enum - see https://tes5edit.github.io/fopdoc/FalloutNV/Records/WEAP.html
mod_required: int # Enum - see https://tes5edit.github.io/fopdoc/FalloutNV/Records/WEAP.html
unused: 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"\x14\x00" # size field
return cls(*struct.unpack("<4sfffBB2s", raw_bytes.read(20)))
[docs] def unparse(self) -> bytes:
"""
Turn this subrecord back into raw bytes for an ESP file.
"""
packed = struct.pack(
"<4sfffBB2s",
self.effect,
self.skill,
self.damage_multiplier,
self.ap,
self.silent,
self.mod_required,
self.unused,
)
return b"VATS\x14\x00" + packed
[docs] class VNAM(Uint32Record):
"""
Sound Level.
See https://tes5edit.github.io/fopdoc/FalloutNV/Records/Values/Sound%20Levels.html
"""
[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 in {
b"BIPL",
b"CRDT",
b"DATA",
b"DNAM",
b"EAMT",
b"EFSD",
b"EITM",
b"ETYP",
b"FULL",
b"ICON",
b"INAM",
b"MICO",
b"MWD1",
b"MWD2",
b"MWD3",
b"MWD4",
b"MWD5",
b"MWD6",
b"MWD7",
b"NAM0",
b"NAM6",
b"NAM7",
b"NAM8",
b"NAM9",
b"NNAM",
b"REPL",
b"SCRI",
b"SNAM",
b"TNAM",
b"UNAM",
b"VANM",
b"VATS",
b"VNAM",
b"WMI1",
b"WMI2",
b"WMI3",
b"WMS1",
b"WMS2",
b"WNAM",
b"WNM1",
b"WNM2",
b"WNM3",
b"WNM4",
b"WNM5",
b"WNM6",
b"WNM7",
b"XNAM",
b"YNAM",
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 Destruction.members:
yield Destruction.parse_member(record_type, raw_bytes)
else:
raise NotImplementedError(record_type)