From 0a7120b6220a2efad0564dd1dac7961a7c11e639 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?eray=20or=C3=A7unus?= Date: Fri, 21 Jun 2019 21:16:51 +0300 Subject: Shotgun fix, CPed, CWeaponInfo MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: eray orçunus --- src/entities/Ped.cpp | 144 +++++++++++++++++++++++++++++--- src/entities/Ped.h | 29 ++++--- src/weapons/Weapon.h | 6 +- src/weapons/WeaponInfo.cpp | 202 ++++++++++++++++++++++++++++++++++++++++++++- src/weapons/WeaponInfo.h | 9 +- 5 files changed, 361 insertions(+), 29 deletions(-) (limited to 'src') diff --git a/src/entities/Ped.cpp b/src/entities/Ped.cpp index 81534915..35e8e957 100644 --- a/src/entities/Ped.cpp +++ b/src/entities/Ped.cpp @@ -9,6 +9,7 @@ #include "Ped.h" #include "PlayerPed.h" #include "General.h" +#include "VisibilityPlugins.h" bool &CPed::bNastyLimbsCheat = *(bool*)0x95CD44; bool &CPed::bPedCheat2 = *(bool*)0x95CD5A; @@ -23,8 +24,6 @@ WRAPPER void CPed::SetDie(AnimationId anim, float arg1, float arg2) { EAXJMP(0x4 WRAPPER void CPed::SpawnFlyingComponent(int, int8) { EAXJMP(0x4EB060); } WRAPPER void CPed::RestorePreviousState(void) { EAXJMP(0x4C5E30); } WRAPPER void CPed::ClearAttack(void) { EAXJMP(0x4E6790); } -WRAPPER void CPed::SelectGunIfArmed(void) { EAXJMP(0x4DD920); } -WRAPPER void CPed::RemoveWeaponModel(int) { EAXJMP(0x4CF980); } static char ObjectiveText[34][28] = { "No Obj", @@ -181,6 +180,18 @@ static char WaitStateText[21][16] = { "Finish Flee", }; +static RwObject* +RemoveAllModelCB(RwObject *object, void *data) +{ + RpAtomic* atomic = (RpAtomic*)object; + if (CVisibilityPlugins::GetAtomicModelInfo(atomic)) + { + RpClumpRemoveAtomic(atomic->clump, atomic); + RpAtomicDestroy(atomic); + } + return object; +} + static PedOnGroundState CheckForPedsOnGroundToAttack(CPlayerPed *player, CPed **pedOnGround) { @@ -718,7 +729,7 @@ CPed::Attack(void) } else { firePos = GetMatrix() * firePos; } - + GetWeapon()->Fire(this, &firePos); if (ourWeaponType == WEAPONTYPE_MOLOTOV || ourWeaponType == WEAPONTYPE_GRENADE) { @@ -752,29 +763,32 @@ CPed::Attack(void) if (m_ped_flagA4 || CTimer::GetTimeInMilliseconds() < m_lastHitTime) { weaponAnimAssoc->callbackType = 0; } - - lastReloadWasInFuture = false; } + + lastReloadWasInFuture = false; } if (ourWeaponType == WEAPONTYPE_SHOTGUN) { weaponAnimTime = weaponAnimAssoc->currentTime; + firePos = ourWeapon->m_vecFireOffset; + if (weaponAnimTime > 1.0f && weaponAnimTime - weaponAnimAssoc->timeStep <= 1.0f && weaponAnimAssoc->IsRunning()) { for (i = GetNodeFrame(PED_HANDR); i; i = RwFrameGetParent(i)) - RwV3dTransformPoints((RwV3d*)ourWeapon->m_vecFireOffset, (RwV3d*)ourWeapon->m_vecFireOffset, 1, &i->modelling); + RwV3dTransformPoints((RwV3d*)firePos, (RwV3d*)firePos, 1, &i->modelling); CVector gunshellPos( - ourWeapon->m_vecFireOffset.x - 0.6f * GetForward().x, - ourWeapon->m_vecFireOffset.y - 0.6f * GetForward().y, - ourWeapon->m_vecFireOffset.z - 0.15f * GetUp().z + firePos.x - 0.6f * GetForward().x, + firePos.y - 0.6f * GetForward().y, + firePos.z - 0.15f * GetUp().z ); + CVector2D gunshellRot( GetRight().x, GetRight().y ); gunshellRot.Normalise(); - CWeapon::AddGunshell(this, gunshellPos, gunshellRot, 0.025f); + GetWeapon()->AddGunshell(this, gunshellPos, gunshellRot, 0.025f); } } animEnd = ourWeapon->m_fAnimLoopEnd; @@ -787,7 +801,6 @@ CPed::Attack(void) if (weaponAnimTime > animEnd || !weaponAnimAssoc->IsRunning() && ourWeaponFire != WEAPON_FIRE_PROJECTILE) { if (weaponAnimTime - 2.0f * weaponAnimAssoc->timeStep <= animEnd - // BUG: We currently don't know any situation this cond. could be true. && (m_ped_flagA4 || CTimer::GetTimeInMilliseconds() < m_lastHitTime) && GetWeapon()->m_eWeaponState != WEAPONSTATE_RELOADING) { @@ -830,7 +843,7 @@ CPed::Attack(void) if (ourWeaponType == WEAPONTYPE_FLAMETHROWER && weaponAnimAssoc->IsRunning()) { weaponAnimAssoc->flags |= ASSOC_DELETEFADEDOUT; weaponAnimAssoc->flags &= ~ASSOC_RUNNING; - weaponAnimAssoc->blendDelta = -4.0; + weaponAnimAssoc->blendDelta = -4.0f; } } } @@ -865,6 +878,107 @@ CPed::Attack(void) CPed::FinishedAttackCB(0, this); } +void +CPed::RemoveWeaponModel(int modelId) +{ + // modelId is not used!! This function just removes the current weapon. + RwFrameForAllObjects(GetNodeFrame(PED_HANDR),RemoveAllModelCB,0); + m_wepModelID = -1; +} + +void +CPed::SetCurrentWeapon(eWeaponType weaponType) +{ + CWeaponInfo* weaponInfo; + + if (HasWeapon(weaponType)) { + weaponInfo = CWeaponInfo::GetWeaponInfo(GetWeapon()->m_eWeaponType); + RemoveWeaponModel(weaponInfo->m_nModelId); + + m_currentWeapon = weaponType; + + weaponInfo = CWeaponInfo::GetWeaponInfo(GetWeapon()->m_eWeaponType); + AddWeaponModel(weaponInfo->m_nModelId); + } +} + +// Only used while deciding which gun ped should switch to, if no ammo left. +bool +CPed::SelectGunIfArmed(void) +{ + eWeaponType weaponType; + + for (int i = 0; i < m_maxWeaponTypeAllowed; i++) { + + if (m_weapons[i].m_nAmmoTotal > 0) { + weaponType = m_weapons[i].m_eWeaponType; + + // Original condition was ridiculous + // if (weaponType == WEAPONTYPE_COLT45 || weaponType < WEAPONTYPE_M16 || weaponType < WEAPONTYPE_FLAMETHROWER || weaponType == WEAPONTYPE_FLAMETHROWER) + if (weaponType < WEAPONTYPE_MOLOTOV) { + SetCurrentWeapon(weaponType); + return true; + } + } + } + SetCurrentWeapon(WEAPONTYPE_UNARMED); + return false; +} + +void +CPed::Duck(void) +{ + if (CTimer::GetTimeInMilliseconds() > m_duckTimer) + ClearDuck(); +} + +void +CPed::ClearDuck(void) +{ + CAnimBlendAssociation *animAssoc; + + animAssoc = RpAnimBlendClumpGetAssociation((RpClump*) m_rwObject, ANIM_DUCK_DOWN); + if (!animAssoc) + animAssoc = RpAnimBlendClumpGetAssociation((RpClump*) m_rwObject, ANIM_DUCK_LOW); + + if (animAssoc) { + + if (m_ped_flagE8) { + + if (m_nPedState == PED_ATTACK || m_nPedState == PED_AIM_GUN) { + animAssoc = RpAnimBlendClumpGetAssociation((RpClump*) m_rwObject, ANIM_RBLOCK_CSHOOT); + if (!animAssoc || animAssoc->blendDelta < 0.0f) { + CAnimManager::BlendAnimation((RpClump*) m_rwObject, ASSOCGRP_STD, ANIM_RBLOCK_CSHOOT, 4.0f); + } + } + } + } else + m_ped_flagE10 = false; +} + +void +CPed::ClearPointGunAt(void) +{ + CAnimBlendAssociation *animAssoc; + CWeaponInfo *weaponInfo; + + ClearLookFlag(); + ClearAimFlag(); + m_ped_flagA8 = false; + if (m_nPedState == PED_AIM_GUN) { + RestorePreviousState(); + weaponInfo = CWeaponInfo::GetWeaponInfo(GetWeapon()->m_eWeaponType); + animAssoc = RpAnimBlendClumpGetAssociation((RpClump*) m_rwObject, weaponInfo->m_AnimToPlay); + if (!animAssoc || animAssoc->blendDelta < 0.0f) { + animAssoc = RpAnimBlendClumpGetAssociation((RpClump*) m_rwObject, weaponInfo->m_Anim2ToPlay); + } + if (animAssoc) { + animAssoc->flags |= ASSOC_DELETEFADEDOUT; + animAssoc->blendDelta = -4.0; + } + } +} + STARTPATCHES InjectHook(0x4CF8F0, &CPed::AddWeaponModel, PATCH_JUMP); InjectHook(0x4C6AA0, &CPed::AimGun, PATCH_JUMP); @@ -881,4 +995,10 @@ STARTPATCHES InjectHook(0x4E68A0, &CPed::FinishedAttackCB, PATCH_JUMP); InjectHook(0x4E5BD0, &CheckForPedsOnGroundToAttack, PATCH_JUMP); InjectHook(0x4E6BA0, &CPed::Attack, PATCH_JUMP); + InjectHook(0x4CF980, &CPed::RemoveWeaponModel, PATCH_JUMP); + InjectHook(0x4CFA60, &CPed::SetCurrentWeapon, PATCH_JUMP); + InjectHook(0x4DD920, &CPed::SelectGunIfArmed, PATCH_JUMP); + InjectHook(0x4E4A10, &CPed::Duck, PATCH_JUMP); + InjectHook(0x4E4A30, &CPed::ClearDuck, PATCH_JUMP); + InjectHook(0x4E6180, &CPed::ClearPointGunAt, PATCH_JUMP); ENDPATCHES diff --git a/src/entities/Ped.h b/src/entities/Ped.h index 45251d46..660f3462 100644 --- a/src/entities/Ped.h +++ b/src/entities/Ped.h @@ -12,10 +12,6 @@ struct CPathNode; -enum { - PED_MAX_WEAPONS = 13 -}; - enum PedOnGroundState { NO_PED, PED_BELOW_PLAYER, @@ -138,7 +134,7 @@ public: uint8 m_ped_flagE1 : 1; uint8 m_ped_flagE2 : 1; uint8 m_ped_flagE4 : 1; - uint8 m_ped_flagE8 : 1; + uint8 m_ped_flagE8 : 1; // can duck? uint8 m_ped_flagE10 : 1; // can't attack if it's set uint8 m_ped_flagE20 : 1; uint8 m_ped_flagE40 : 1; @@ -225,10 +221,11 @@ public: uint8 stuff5[24]; CEntity *m_pCollidingEntity; uint8 stuff6[12]; - CWeapon m_weapons[PED_MAX_WEAPONS]; + CWeapon m_weapons[NUM_PED_WEAPONTYPES]; int32 stuff7; - uint8 m_currentWeapon; - uint8 stuff[3]; + uint8 m_currentWeapon; // eWeaponType + uint8 m_maxWeaponTypeAllowed; // eWeaponType + uint8 stuff[2]; int32 m_pPointGunAt; CVector m_vecHitLastPos; uint8 stuff8[12]; @@ -241,8 +238,11 @@ public: uint32 m_standardTimer; uint32 m_attackTimer; uint32 m_lastHitTime; - uint8 stuff9[22]; - uint8 m_bodyPartBleeding; + uint32 m_hitRecoverTimer; + uint32 field_4E0; + uint32 m_duckTimer; + uint8 stuff9[10]; + uint8 m_bodyPartBleeding; // PedNode uint8 m_field_4F3; CPed *m_nearPeds[10]; uint16 m_numNearPeds; @@ -272,12 +272,17 @@ public: void RestorePreviousState(void); void ClearAttack(void); bool IsPedHeadAbovePos(float zOffset); - void RemoveWeaponModel(int); - void SelectGunIfArmed(void); + void RemoveWeaponModel(int modelId); + void SetCurrentWeapon(eWeaponType weaponType); + bool SelectGunIfArmed(void); + void Duck(void); + void ClearDuck(void); + void ClearPointGunAt(void); static RwObject *SetPedAtomicVisibilityCB(RwObject *object, void *data); static RwFrame *RecurseFrameChildrenVisibilityCB(RwFrame *frame, void *data); static void FinishedAttackCB(CAnimBlendAssociation *attackAssoc, void *arg); + bool HasWeapon(eWeaponType weaponType) { return m_weapons[weaponType].m_eWeaponType == weaponType; } CWeapon *GetWeapon(void) { return &m_weapons[m_currentWeapon]; } RwFrame *GetNodeFrame(int nodeId) { return m_pFrames[nodeId]->frame; } diff --git a/src/weapons/Weapon.h b/src/weapons/Weapon.h index 87134929..6009a549 100644 --- a/src/weapons/Weapon.h +++ b/src/weapons/Weapon.h @@ -16,7 +16,9 @@ enum eWeaponType WEAPONTYPE_MOLOTOV, WEAPONTYPE_GRENADE, WEAPONTYPE_DETONATOR, - WEAPONTYPE_HELICANNON + NUM_PED_WEAPONTYPES = 13, + WEAPONTYPE_HELICANNON = 13, + NUM_WEAPONTYPES }; enum eWeaponFire { @@ -48,6 +50,6 @@ public: bool m_bAddRotOffset; bool Fire(CEntity*, CVector*); - static void AddGunshell(CEntity*, CVector const&, CVector2D const&, float); + void AddGunshell(CEntity*, CVector const&, CVector2D const&, float); }; static_assert(sizeof(CWeapon) == 0x18, "CWeapon: error"); diff --git a/src/weapons/WeaponInfo.cpp b/src/weapons/WeaponInfo.cpp index 155425b5..46ecfb54 100644 --- a/src/weapons/WeaponInfo.cpp +++ b/src/weapons/WeaponInfo.cpp @@ -1,14 +1,214 @@ #include "common.h" #include "patcher.h" +#include "main.h" +#include "FileMgr.h" #include "WeaponInfo.h" +#include "AnimBlendAssociation.h" -CWeaponInfo (&CWeaponInfo::ms_apWeaponInfos)[14] = * (CWeaponInfo(*)[14]) * (uintptr*)0x6503EC; +//CWeaponInfo (&CWeaponInfo::ms_apWeaponInfos)[14] = * (CWeaponInfo(*)[14]) * (uintptr*)0x6503EC; +CWeaponInfo CWeaponInfo::ms_apWeaponInfos[NUM_WEAPONTYPES]; + +static char ms_aWeaponNames[][32] = { + "Unarmed", + "BaseballBat", + "Colt45", + "Uzi", + "Shotgun", + "AK47", + "M16", + "SniperRifle", + "RocketLauncher", + "FlameThrower", + "Molotov", + "Grenade", + "Detonator", + "HeliCannon" +}; CWeaponInfo* CWeaponInfo::GetWeaponInfo(eWeaponType weaponType) { return &CWeaponInfo::ms_apWeaponInfos[weaponType]; } +void +CWeaponInfo::Initialise(void) +{ + debug("Initialising CWeaponInfo...\n"); + for (int i = 0; i < NUM_WEAPONTYPES; i++) { + ms_apWeaponInfos[i].m_eWeaponFire = WEAPON_FIRE_INSTANT_HIT; + ms_apWeaponInfos[i].m_AnimToPlay = ANIM_PUNCH_R; + ms_apWeaponInfos[i].m_Anim2ToPlay = NUM_ANIMS; + ms_apWeaponInfos[i].m_bUseGravity = 1; + ms_apWeaponInfos[i].m_bSlowsDown = 1; + ms_apWeaponInfos[i].m_bRandSpeed = 1; + ms_apWeaponInfos[i].m_bExpands = 1; + ms_apWeaponInfos[i].m_bExplodes = 1; + } + debug("Loading weapon data...\n"); + LoadWeaponData(); + debug("CWeaponInfo ready\n"); +} + +void +CWeaponInfo::LoadWeaponData(void) +{ + float spread, speed, lifeSpan, radius; + float range, fireOffsetX, fireOffsetY, fireOffsetZ; + float delayBetweenAnimAndFire, delayBetweenAnim2AndFire, animLoopStart, animLoopEnd; + int flags, ammoAmount, damage, reload, weaponType; + int firingRate, modelId; + char line[256], weaponName[32], fireType[32]; + char animToPlay[32], anim2ToPlay[32]; + + CAnimBlendAssociation *animAssoc; + AnimationId animId, anim2Id; + + int bp, buflen; + int lp, linelen; + + CFileMgr::SetDir("DATA"); + buflen = CFileMgr::LoadFile("WEAPON.DAT", work_buff, sizeof(work_buff), "r"); + CFileMgr::SetDir(""); + + for (bp = 0; bp < buflen; ) { + // read file line by line + for (linelen = 0; work_buff[bp] != '\n' && bp < buflen; bp++) { + line[linelen++] = work_buff[bp]; + } + bp++; + line[linelen] = '\0'; + + // skip white space + for (lp = 0; line[lp] <= ' '; lp++); + + if (lp >= linelen || // FIX: game uses == here, but this is safer if we have empty lines + line[lp] == '#') + continue; + + spread = 0.0f; + flags = 0; + speed = 0.0f; + ammoAmount = 0; + lifeSpan = 0.0f; + radius = 0.0f; + range = 0.0f; + damage = 0; + reload = 0; + firingRate = 0; + fireOffsetX = 0.0f; + weaponName[0] = '\0'; + fireType[0] = '\0'; + fireOffsetY = 0.0f; + fireOffsetZ = 0.0f; + animId = ANIM_WALK; + anim2Id = ANIM_WALK; + sscanf( + &line[lp], + "%s %s %f %d %d %d %d %f %f %f %f %f %f %f %s %s %f %f %f %f %d %d", + &weaponName, + &fireType, + &range, + &firingRate, + &reload, + &ammoAmount, + &damage, + &speed, + &radius, + &lifeSpan, + &spread, + &fireOffsetX, + &fireOffsetY, + &fireOffsetZ, + &animToPlay, + &anim2ToPlay, + &animLoopStart, + &animLoopEnd, + &delayBetweenAnimAndFire, + &delayBetweenAnim2AndFire, + &modelId, + &flags); + + if (strncmp(weaponName, "ENDWEAPONDATA", 13) == 0) + return; + + weaponType = FindWeaponType(weaponName); + + animAssoc = CAnimManager::GetAnimAssociation(ASSOCGRP_STD, animToPlay); + animId = static_cast(animAssoc->animId); + + if (strncmp(anim2ToPlay, "null", 5) != 0) { + animAssoc = CAnimManager::GetAnimAssociation(ASSOCGRP_STD, anim2ToPlay); + anim2Id = static_cast(animAssoc->animId); + } + + CVector vecFireOffset(fireOffsetX, fireOffsetY, fireOffsetZ); + + ms_apWeaponInfos[weaponType].m_eWeaponFire = FindWeaponFireType(fireType); + ms_apWeaponInfos[weaponType].m_fRange = range; + ms_apWeaponInfos[weaponType].m_nFiringRate = firingRate; + ms_apWeaponInfos[weaponType].m_nReload = reload; + ms_apWeaponInfos[weaponType].m_nAmountofAmmunition = ammoAmount; + ms_apWeaponInfos[weaponType].m_nDamage = damage; + ms_apWeaponInfos[weaponType].m_fSpeed = speed; + ms_apWeaponInfos[weaponType].m_fRadius = radius; + ms_apWeaponInfos[weaponType].m_fLifespan = lifeSpan; + ms_apWeaponInfos[weaponType].m_fSpread = spread; + ms_apWeaponInfos[weaponType].m_vecFireOffset = vecFireOffset; + ms_apWeaponInfos[weaponType].m_AnimToPlay = animId; + ms_apWeaponInfos[weaponType].m_Anim2ToPlay = anim2Id; + ms_apWeaponInfos[weaponType].m_fAnimLoopStart = animLoopStart * 0.03f; + ms_apWeaponInfos[weaponType].m_fAnimLoopEnd = animLoopEnd * 0.03f; + ms_apWeaponInfos[weaponType].m_fAnimFrameFire = delayBetweenAnimAndFire * 0.03f; + ms_apWeaponInfos[weaponType].m_fAnim2FrameFire = delayBetweenAnim2AndFire * 0.03f; + ms_apWeaponInfos[weaponType].m_nModelId = modelId; + ms_apWeaponInfos[weaponType].m_bUseGravity = flags; + ms_apWeaponInfos[weaponType].m_bSlowsDown = flags >> 1; + ms_apWeaponInfos[weaponType].m_bDissipates = flags >> 2; + ms_apWeaponInfos[weaponType].m_bRandSpeed = flags >> 3; + ms_apWeaponInfos[weaponType].m_bExpands = flags >> 4; + ms_apWeaponInfos[weaponType].m_bExplodes = flags >> 5; + ms_apWeaponInfos[weaponType].m_bCanAim = flags >> 6; + ms_apWeaponInfos[weaponType].m_bCanAimWithArm = flags >> 7; + ms_apWeaponInfos[weaponType].m_b1stPerson = flags >> 8; + ms_apWeaponInfos[weaponType].m_bHeavy = flags >> 9; + ms_apWeaponInfos[weaponType].m_bThrow = flags >> 10; + } +} + +eWeaponType +CWeaponInfo::FindWeaponType(char *name) +{ + for (int i = 0; i < NUM_WEAPONTYPES; i++) { + if (strcmp(ms_aWeaponNames[i], name) == 0) { + return static_cast(i); + } + } + return WEAPONTYPE_UNARMED; +} + +eWeaponFire +CWeaponInfo::FindWeaponFireType(char *name) +{ + if (strcmp(name, "MELEE") == 0) return WEAPON_FIRE_MELEE; + if (strcmp(name, "INSTANT_HIT") == 0) return WEAPON_FIRE_INSTANT_HIT; + if (strcmp(name, "PROJECTILE") == 0) return WEAPON_FIRE_PROJECTILE; + if (strcmp(name, "AREA_EFFECT") == 0) return WEAPON_FIRE_AREA_EFFECT; + Error("Unknown weapon fire type, WeaponInfo.cpp"); + return WEAPON_FIRE_INSTANT_HIT; +} + +void +CWeaponInfo::Shutdown(void) +{ + debug("Shutting down CWeaponInfo...\n"); + debug("CWeaponInfo shut down\n"); +} + STARTPATCHES + InjectHook(0x564EA0, &CWeaponInfo::Initialise, PATCH_JUMP); InjectHook(0x564FD0, &CWeaponInfo::GetWeaponInfo, PATCH_JUMP); + InjectHook(0x5653E0, &CWeaponInfo::FindWeaponType, PATCH_JUMP); + InjectHook(0x5653B0, &CWeaponInfo::FindWeaponFireType, PATCH_JUMP); + InjectHook(0x564FE0, &CWeaponInfo::LoadWeaponData, PATCH_JUMP); + InjectHook(0x564FB0, &CWeaponInfo::Shutdown, PATCH_JUMP); ENDPATCHES \ No newline at end of file diff --git a/src/weapons/WeaponInfo.h b/src/weapons/WeaponInfo.h index 34790565..faa8bf7b 100644 --- a/src/weapons/WeaponInfo.h +++ b/src/weapons/WeaponInfo.h @@ -4,6 +4,8 @@ #include "AnimManager.h" class CWeaponInfo { +// static CWeaponInfo(&ms_apWeaponInfos)[14]; + static CWeaponInfo ms_apWeaponInfos[14]; public: eWeaponFire m_eWeaponFire; float m_fRange; @@ -37,9 +39,12 @@ public: uint8 m_bThrow : 1; uint8 stuff; - static CWeaponInfo (&ms_apWeaponInfos)[14]; - + static void Initialise(void); + static void LoadWeaponData(void); static CWeaponInfo *GetWeaponInfo(eWeaponType weaponType); + static eWeaponFire FindWeaponFireType(char *name); + static eWeaponType FindWeaponType(char *name); + static void Shutdown(void); }; static_assert(sizeof(CWeaponInfo) == 0x54, "CWeaponInfo: error"); \ No newline at end of file -- cgit v1.2.3