Changes:
- Added tool to convert Blue Shift BSP files to standard Half-Life BSP format
- Fixed game potentially crashing when saving and/or loading the game in ba_outro
attachmentIndex
with the number matching the attachment.WRITE_SHORT(entindex() + (attachmentIndex << 12));
Where attachmentIndex
is the attachment index + 1 (0 means use entity origin).HEV_AAx common/null
HEV_A0 common/null
It can be changed in the source code by removing the lines that play it: https://github.com/ValveSoftware/halflife/blob/c7240b965743a53a29491dd49320c88eecf6257b/dlls/items.cpp#L191-L194cl: client fired mp5 1 times
server fired mp5 1 times
client event fired mp5 1 times
cl: client fired mp5 2 times
server fired mp5 2 times
client event fired mp5 2 times
cl: client fired mp5 3 times
client event fired mp5 3 times
cl: client fired mp5 4 times
server fired mp5 3 times
client event fired mp5 4 times
The third shot only happens on the client due to prediction code running it, but never on the server side. I ended up with 4 shots too many after a full 50 rounds fired.This is really interesting stuff! Does this fix the fast crowbar gib bug and the double shot bug from the glock's secondary fire?I can fix the crowbar bug, but what's the double shot bug exactly? Just so i know what to look for.
I know the guy.. He helped me with my own code till we both discovered Solokillers work. Then he ceased development. The public code is even less sophisticated than the private one I own and even that it a total dev build. For instance many NPC actions spawn a spark on top their heads for verification that things worked as intended coding wise. Unfortunately I'm to stupid and limited in time to figure things out myself.
If you got the coding know how let me know and I'll send you that internal code. He fixed dozens of spirit related bugs and cleaned up the whole code of it big time. At least so he claimed but I believe that he did.HLEnhanced was never really planned that well which is why i ceased development. I am planning to make a successor that does stuff properly (and with a unique name so it doesn't get confused for another mod).
Another guess is CMake and dependencies. You need to build the XercesC library, and use CMake to generate project files for Visual Studio, if I recall correctly. That probably pushes away many people, especially beginners. But then again, it's 2021, programmers are supposed to be familiar with at least one or two build systems. :/Yeah Xerces was a bad choice. There are XML parsing libraries that are lightweight, header only and don't have the separate dependency requirement. The only downside is they don't support the full XML spec, but i doubt anybody would ever use namespaces, entities and doctypes.
struct TITLECOMMENT
{
const char* pBSPName;
const char* pTitleName;
};
TITLECOMMENT gTitleComments[] =
{
{"c1a1", "#C1A1TITLE"},
{"ba_yard", "#BA_YARDTITLE"}
//More title entries here
};
extern "C" void DLLEXPORT SV_SaveGameComment(char* pszBuffer, int iSizeBuffer);
extern "C" void SV_SaveGameComment(char* pszBuffer, int iSizeBuffer)
{
const char* mapName = STRING(gpGlobals->mapname);
const char* titleName = nullptr;
for (int i = 0; i < ARRAYSIZE(gTitleComments); ++i)
{
const size_t length = strlen(gTitleComments[i].pBSPName);
if (!strnicmp(mapName, gTitleComments[i].pBSPName, length))
{
titleName = gTitleComments[i].pTitleName;
if (titleName)
{
break;
}
}
}
if (!titleName)
{
titleName = mapName;
if (!titleName || !(*titleName))
{
edict_t* world = ENT(0);
//Use the full map name (e.g. "maps/c1a0.bsp").
//The engine uses the value it sends to clients (aka a variable used by client code) but this should always be valid here
//The engine sets the model field on worldspawn when it loads the map
titleName = STRING(world->v.model);
if (!strlen(titleName))
{
//The engine assigns mapName here again, but that's known to be either null or empty so to be sure this always assigns empty,
//since strncpy has undefined behavior for null src pointers
titleName = "";
}
}
}
strncpy(pszBuffer, titleName, iSizeBuffer - 1);
pszBuffer[iSizeBuffer - 1] = '\0';
}
This is basically the same as the engine implementation. The function SV_SaveGameComment is exported and looked for by the engine. It passes in a buffer that is to be filled with a localizable string that will be looked up in the game's resource/<game>_<language>.txt
file.ba_yard
for example is used for ba_yard1
, ba_yard2
, ba_yard3
, ba_yard3a
, ba_yard3b
, ba_yard4
, ba_yard4a
, ba_yard5
and , ba_yard5a
. You can use this pattern but it's unlikely to be very helpful for most custom maps.resource/valve_english.txt
.cbase.h
and cbase.cpp
. It can be made more flexible so as to allow any map to specify a non-localized name, or to map the name to some localizable string.const int MaxRandomTargets = 16;
const char TargetKeyValuePrefix[] = "target";
class CTriggerRandom : public CBaseDelay
{
public:
void KeyValue(KeyValueData* pkvd);
void Spawn();
void EXPORT RandomUse(CBaseEntity* pActivator, CBaseEntity* pCaller, USE_TYPE useType, float value);
int ObjectCaps() { return CBaseDelay::ObjectCaps() & ~FCAP_ACROSS_TRANSITION; }
virtual int Save(CSave& save);
virtual int Restore(CRestore& restore);
static TYPEDESCRIPTION m_SaveData[];
string_t m_iszTargets[MaxRandomTargets];
private:
int BuildMap(std::array<std::uint8_t, MaxRandomTargets>& map);
};
TYPEDESCRIPTION CTriggerRandom::m_SaveData[] =
{
DEFINE_ARRAY(CTriggerRandom, m_iszTargets, FIELD_STRING, MaxRandomTargets),
};
IMPLEMENT_SAVERESTORE(CTriggerRandom, CBaseDelay);
LINK_ENTITY_TO_CLASS(trigger_random, CTriggerRandom);
void CTriggerRandom::KeyValue(KeyValueData* pkvd)
{
const size_t prefixLength = ARRAYSIZE(TargetKeyValuePrefix) - 1;
if (!strncmp(TargetKeyValuePrefix, pkvd->szKeyName, prefixLength))
{
const char* indexString = pkvd->szKeyName + prefixLength;
char* end;
int index = strtol(indexString, &end, 10);
//Must be a number and end after the number, e.g. "target1"
//index is 1 based [1, MaxRandomTargets]
if (end != indexString && *end == '\0')
{
--index;
if (index >= 0 && index < MaxRandomTargets)
{
if (FStringNull(m_iszTargets[index]))
{
m_iszTargets[index] = ALLOC_STRING(pkvd->szValue);
}
else
{
ALERT(at_error, "CTriggerRandom::KeyValue: target \"%s\" already set to \"%s\"", pkvd->szKeyName, STRING(m_iszTargets[index]));
}
}
else
{
ALERT(at_error, "CTriggerRandom::KeyValue: invalid target index \"%s\", must be in range [1, %d]", pkvd->szKeyName, MaxRandomTargets);
}
}
else
{
ALERT(at_error, "CTriggerRandom::KeyValue: invalid target format \"%s\"", pkvd->szKeyName);
}
}
else
{
CBaseDelay::KeyValue(pkvd);
}
}
void CTriggerRandom::Spawn()
{
SetUse(&CTriggerRandom::RandomUse);
}
void CTriggerRandom::RandomUse(CBaseEntity* pActivator, CBaseEntity* pCaller, USE_TYPE useType, float value)
{
std::array<std::uint8_t, MaxRandomTargets> map;
const int count = BuildMap(map);
if (count > 0)
{
const int index = RANDOM_LONG(0, count - 1);
const char* target = STRING(m_iszTargets[index]);
FireTargets(target, pActivator, pCaller, useType, 0);
}
}
int CTriggerRandom::BuildMap(std::array<std::uint8_t, MaxRandomTargets>& map)
{
int targetCount = 0;
for (int i = 0; i < MaxRandomTargets; ++i)
{
if (!FStringNull(m_iszTargets[i]))
{
++targetCount;
map[i] = i;
}
}
return targetCount;
}
Add it to the bottom of triggers.cpp.#include <array>
#include <cstdint>
FGD entry:
@PointClass base(Targetname) = trigger_random : "Random Trigger"
[
target1(target_destination) : "Target 1"
target2(target_destination) : "Target 2"
target3(target_destination) : "Target 3"
target4(target_destination) : "Target 4"
target5(target_destination) : "Target 5"
target6(target_destination) : "Target 6"
target7(target_destination) : "Target 7"
target8(target_destination) : "Target 8"
target9(target_destination) : "Target 9"
target10(target_destination) : "Target 10"
target11(target_destination) : "Target 11"
target12(target_destination) : "Target 12"
target13(target_destination) : "Target 13"
target14(target_destination) : "Target 14"
target15(target_destination) : "Target 15"
target16(target_destination) : "Target 16"
]
I haven't tested it but it should work fine. It logs invalid keyvalue inputs and will treat the "target" keyvalue as an invalid input since it isn't meant to be used with this entity.PickRandom
input. All target keyvalues that have been set will be considered and one is randomly chosen. You can turn it into a random do/do nothing setup by setting targets that don't exist, e.g. "dummytarget".