Created 3 years ago2021-01-13 21:05:33 UTC by Solokiller
ALLOC_STRING
engine function, and changes behavior to allocate memory once per string, so calling it multiple times with the same string doesn't allocate more memory. Very efficient, this change also frees up a little memory in the engine's available memory pool.game_text
to be changed to perform this parsing by itself. This behavior was inconsistent and could cause difficult to debug problems otherwise.
Con_Printf
which will unconditionally print to the console, and supports all of printf's format options (the engine's version is limited to C89 printf options).
sv_
or cl_
prefix automatically so you can have the same variables and commands on both sides.:command_name command_value
or :(sv_|cl_)command_name command_value
.g_ConCommands.CreateCommand("my_command", [this](const auto& args) { MyCommandHandler(args); });
void MyClass::MyCommandHandler(const CCommandArgs& args)
{
Con_Printf("%d arguments:\n", args.Count());
for (int i = 0; i < args.Count(); ++i)
{
Con_Printf("d\n", args.Argument(i)));
}
}
CCommandArgs
is a thin wrapper around engine functions that makes it easier to work with commands by indicating which functions are available in all libraries.
[startup] [error] Error validating JSON "/Defaults" with value "[{"LogLevel":"trace"}]": unexpected instance type
The JSON schemas can also be written to a file and used as input to tools that can generate JSON editors. This makes it easier to edit JSON and shows what kind of options you have.cfg/maps/<mapname>.json
will be loaded if the map with that name is started.{
"Includes": [
"cfg/shared.json"
],
"Sections": [
{
"Name": "Echo",
"Message": "Hello World!"
},
{
//Commands to configure multiplayer server
"Name": "Commands",
"Condition": "Multiplayer",
"Commands": [
"echo Hello World from command!",
// disable autoaim
"sv_aim 0",
// player bounding boxes (collisions, not clipping)
"sv_clienttrace 3.5",
// disable clients' ability to pause the server
"pausable 0",
// maximum client movement speed
"sv_maxspeed 270",
// load ban files"
"exec listip.cfg",
"exec banned.cfg"
]
}
]
}
The Includes
value is a list of files to include before the current one, and makes sharing settings between servers and maps very easy. You could for instance make a map series where each map includes a file that contains the shared configuration before changing some for specific maps.{
"Sections": [
{
"Name": "Echo",
"Message": "Hello World from shared.json!"
}
]
}
The Sections
value is a list of section objects that apply to the game. Each section can have a condition associated with it evaluated at load time to determine whether the section should be used or not.bool Evaluate()
{
return condition;
}
To prevent abuse this evaluation will time out after 1 second, so the server won't lock up due to some clever infinite loop trick.Singleplayer
and Multiplayer
. I plan to expand on this with gamemode detection (deathmatch, coop, teamplay, etc) as well as checking the map name and the value of cvars.Echo
section simply prints the message to the console, useful for debugging to see if your file is getting loaded properly.Commands
section provides the same functionality as the cfg files this system replaces. It lets you provide a list of console commands to execute.[
"sv_gravity"
]
So server operators can manage this whitelist themselves if needed.In regards to scripting, and assuming scripting allows for creating custom monsters and entities, I believe this could definitely be a plus.Yeah that's certainly a possibility. I implemented custom entity/NPC/weapon support in Sven Co-op using Angelscript before.
Condition
key.MapInit
, MapActivate
, MapStart
and PluginInit
. So if you then included multiple scripts those functions would conflict with each-other.trigger_script
would be loaded into its own Angelscript module. A module is basically a C++ library in terms of scope, so these API functions wouldn't conflict unless you mark them as shared.AFBRegister register(@MyAFBPluginClass());
The AFBRegister
class registers the plugin in its constructor and unregisters it in its destructor, thus tying the plugin lifetime to that of its containing module. You won't even need to write a PluginInit
function anymore. AFBRegister
would be a shared class.void ScriptInit()
{
//Called when the script is first loaded.
//Subscribe to the MapInit event. All events define a unique event type associated with it, even if they don't have any data or methods.
Events.GetEvent<MapInitEvent>().Subscribe(@MapInit);
}
void ScriptShutdown()
{
//Called when the script is about to be discarded/unloaded.
//Don't need to manually free event handlers because they'll be removed automatically, but it can be done manually like this.
Events.RemoveAllEventHandlersFromCallingModule();
}
//All event handlers follow the same structure: void return type, one parameter taking the event by handle.
void MapInit(MapInitEvent@ event)
{
//Called when the map is starting for any reason, map scripts could be in the process of being loaded right now, so inter-module communication isn't advised at this time.
//Equivalent to Source's logic_auto outputs: https://developer.valvesoftware.com/wiki/Logic_auto
switch (event.InitType)
{
case MapInitType::NewGame:
//The map is being loaded for the first time, like when the map or changelevel console commands are used.
break;
case MapInitType::LoadGame:
//The map is being loaded from a save game, which means the player is loading a save game or a map is being returned to through a trigger_changelevel.
break;
case MapInitType::NewRound:
//For game modes that use round-based gameplay, a new round is starting.
break;
//More types if needed.
}
}
The event system replaces hooks and allows for compile-time compatibility checking of events. The previous system had to rely on variable parameters because hooks could have any number of parameters, so you could technically pass in an integer into a function that's expecting a function handle. You'd get an error at runtime, so it's not as obvious that you've made a mistake.Events.GetEvent<MapInitEvent>().Publish(MapInitType::NewGame);
This would then internally create the event object:
//Curiously Recurring Template Pattern-based base class
template<typename TEvent>
class Event
{
public:
//Reference counting code omitted for brevity.
template<typename... Args>
void Publish(Args&&... args)
{
//Event is created with reference count 1, ownership transferred to smart pointer.
//Event constructor is invoked with template parameters.
as::SmartPtr event{new TEvent{std::forward<Args>(args)...}};
//Event handlers are tracked by the Events global and will invoke all handlers.
Events.PublishEvent(event.Get());
//Smart pointer releases event, usually destroys it if no script holds on to it.
}
};
class EventSystem
{
public:
template<typename TEvent>
void PublishEvent(TEvent* event)
{
//Probably a std::unordered_map<std::reference_wrapper<type_info>, as::SmartPtr<asITypeInfo>>.
asITypeInfo* type = GetScriptTypeFromCppType<TEvent>();
if (!type)
{
return;
}
auto context = ...;
//Finds or creates the list of handlers for this event type.
const std::vector<as::SmartPtr<asIScriptFunction>>& handlers = GetEventHandlersForType(type);
for (const auto& function : handlers)
{
//Context setup and error handling omitted for brevity.
context->SetArgObject(0, event);
context->Execute();
}
}
};
Custom entities require a fair bit of work to support. You need to expose the class you want custom entities to inherit from, expose the API to call base class implementations (e.g. calling CBaseMonster::Killed from your NPC's Killed method) and you need to write a wrapper that forwards calls to the script for any virtual functions it has.custom
function. I haven't quite figured out how to save and load them since the save game system doesn't use custom
. I could hack it by saving custom entities to have the custom
class, and then saving the actual classname separately. That solves the problem, but there is still the issue of scripts changing the custom entity implementation.A.as
that defines a custom entity foo
, and the next map could have a script B.as
that also defines a custom entity foo
. When such an entity is transitioned between maps its composition can change dramatically, including changing base classes.CBasePlayerWeapon*
isn't compatible with CBaseMonster*
. There are no safeguards in the save game system against this and it would take some doing to support it, so the initial version of the scripting system wouldn't be able to support it.On that note: to use the CMake version of Half-Life Updated you currently have to build the INSTALL target to deploy the libraries, which might be a bit cumbersome and annoying.Not a problem for me, though I imagine it'd be a bit confusing/frustrating to beginners.
trigger_sequence
entity. If Angelscript by itself is not good enough to manage such scripting functionality then providing a means of doing so through both entity and scripting means would be useful. It is likely that scripting will be sufficient given how the entity is implemented: https://github.com/SamVanheer/czeror-sdk/blob/1e732141e5823fa69596de388a269c1ba34a33b7/dlls/CTriggerSequence.cpp#L270-L371Vector
type that changed how the GCC compiler optimized passing vectors by value. This change broke compatibility with the particle manager library, which ships as part of the game.#sharplife
has been renamed to #unified-sdk
. It serves as a channel to discuss anything related to Half-Life Updated, Half-Life Unified SDK and Half-Life Asset Manager.
extdll.h
and util.h
before including cbase.h
and many other common headers are also included in cbase.h
which means you'll get access to common entity classes like CSprite
right off the bat.SetThink
, SetUse
, SetTouch
and SetBlocked
. If you try to set one of these to a function pointer that does not belong to the class hierarchy of the entity that you're setting the function on you'll get an error message in the console warning you about it. Setting a function incorrectly can cause strange bugs and crashes so this will help a lot.I've also added a new debug-only feature to help diagnose incorrect usage of SetThink, SetUse, SetTouch and SetBlocked. If you try to set one of these to a function pointer that does not belong to the class hierarchy of the entity that you're setting the function on you'll get an error message in the console warning you about it. Setting a function incorrectly can cause strange bugs and crashes so this will help a lot.Where is that change? I couldn't see anything obvious in the most recent commit messages.
HalfLife.UnifiedSdk.Utilities
library contains utility functionality for opening, analyzing, modifying, converting and upgrading Half-Life 1 maps made for the GoldSource engine..rmf
, .map
, .bsp
and .ent
files. Many thanks to Penguinboy for creating these libraries and updating them to support the loading of Blue Shift BSP files.BlueShiftBSPConverter
and ripent
tools currently used to install content. Along with the standard ZipFile
API provided as part of the .NET runtime replacing the use of the 7zip
tool this eliminates the use of platform-specific tools in all scripts..jmf
files (J.A.C.K. map source files) but hopefully full support can be added to Sledge.Formats.Map
someday.var halfLifeDirectory = SteamUtilities.TryGetModInstallPath() ?? throw new InvalidOperationException("Steam or Half-Life isn't installed");
On Windows Steam writes a few registry keys, one of which contains the install location of Half-Life. On Linux it's up to the user to provide the location, so the install scripts will use the script's location as a starting point.#r "nuget: HalfLife.UnifiedSdk.Utilities, 0.1.0"
#nullable enable
using HalfLife.UnifiedSdk.Utilities.Entities;
using HalfLife.UnifiedSdk.Utilities.Games;
using HalfLife.UnifiedSdk.Utilities.Maps;
using HalfLife.UnifiedSdk.Utilities.Tools;
var counts = MapFormats.EnumerateMapsWithExtension("bsp",
"C:/Program Files (x86)/Steam/steamapps/common/Half-Life/valve/maps",
"C:/Program Files (x86)/Steam/steamapps/common/Half-Life/gearbox/maps",
"C:/Program Files (x86)/Steam/steamapps/common/Half-Life/bshift/maps")
.WhereIsCampaignMap()
.GroupBy(m => m.FileName, m => m.Entities.OfClass("monster_human_grunt").Count()
+ m.Entities
.OfClass("monstermaker")
.WhereString("monstertype", "monster_human_grunt")
.Select(e => e.GetInteger("monstercount"))
.Sum())
.ToList();
var countsByGame = counts.GroupBy(c => Path.GetFileName(Path.GetDirectoryName(Path.GetDirectoryName(c.Key))));
foreach (var game in countsByGame)
{
Console.WriteLine("{0} human grunts in {1}", game.Sum(g => g.Sum()), game.Key);
}
Output:
174 human grunts in valveThis script counts both the actual grunt entities as well as monstermaker entities that spawn them, and counts the number of them it spawns. It doesn't account for infinitely spawning makers, but that's an easy thing to add. (Opposing Force has none because it uses a different entity)
0 human grunts in gearbox
81 human grunts in bshift
static void ReplaceWorldItems(Map map, Entity entity)
{
//Convert world_items entities to their respective entities
if (entity.ClassName == "world_items")
{
switch (entity.GetInteger("type"))
{
case 42:
entity.ClassName = "item_antidote";
entity.Remove("type");
break;
case 43:
//Remove security items (no purpose, and has been removed from the codebase)
map.Entities.Remove(entity);
break;
case 44:
entity.ClassName = "item_battery";
entity.Remove("type");
break;
case 45:
entity.ClassName = "item_suit";
entity.Remove("type");
break;
}
}
}
static void UpgradeWorldItems(MapUpgradeContext context)
{
foreach (var entity in context.Map.Entities)
{
ReplaceWorldItems(context.Map, entity);
}
}
static readonly string BaseDirectory = "./upgrade";
var unifiedSdk100UpgradeAction = new MapUpgradeAction(new SemVersion(1, 0, 0));
unifiedSdk100UpgradeAction.Upgrading += UpgradeWorldItems;
var upgradeTool = new MapUpgradeTool(unifiedSdk100UpgradeAction);
var map = MapFormats.Deserialize(Path.Combine(BaseDirectory, "c1a0d.bsp"));
upgradeTool.Upgrade(new MapUpgrade(map));
Console.WriteLine($"Upgraded map to {upgradeTool.GetVersion(map)}");
using (var file = File.Open(Path.Combine(BaseDirectory, "c1a0d_new.bsp"), FileMode.Create, FileAccess.Write))
{
map.Serialize(file);
}
This will upgrade maps to replace world_items
with the entity it normally spawns, while removing item_security
altogether since it's an obsolete entity.Half-Life
directory:
foreach (var modDirectory in ModUtilities.EnumerateMods(HalfLifeDirectory).Except(ValveGames.GoldSourceGames.Select(g => g.ModDirectory)))
{
Console.WriteLine($"Found mod {modDirectory}");
if (ModUtilities.TryLoadLiblist(HalfLifeDirectory, modDirectory) is { } liblist)
{
Console.WriteLine($"Mod is called {liblist.Game ?? "not listed"}");
}
}
In my case it outputs this:
Found mod czeror-sdkContinued in next post --->
Mod is called Condition-Zero: Deleted Scenes SDK
Found mod ehl
Mod is called Enhanced Half-Life
Found mod halflife-pr
Mod is called Half-Life PR
Found mod halflife-updated-cmake
Mod is called Half-Life Updated CMake
Found mod halflife_bs_updated
Mod is called Half-Life: Blue Shift Updated
Found mod halflife_op4_updated
Mod is called Half-Life: Opposing Force Updated
Found mod halflife_updated
Mod is called Half-Life Updated
Found mod hlenhanced
Mod is called Half-Life Enhanced
Found mod hlu
Mod is called Half-Life Unified SDK
Found mod scriptablemod
Mod is called Half-Life Scriptable Mod
LINK_ENTITY_TO_CLASS
and allows you to use Go to Definition
on Save
and Restore
methods and gets rid of the visual noise those warnings show in the editor, making it easier to find code that is actually in need of attention.modname_hd
, modname_lv
and modname_language
directories if they exist (where language
is a language listed here in the API language code column, except for english
since that's the default).Sledge.Formats.Bsp
library, as well as made sure all maps are written in the standard BSP format rather than the Blue Shift BSP format. I've also added utility functionality to get the list of languages supported by Steam for use in automating the management of language-specific mod directories, as well as constants to manage content directories such as modname_hd
.In WON, there was a hit sound when you shoot humans / aliens with a bullet, but in steam that is no longer there. Sven Coop actually added this back. Is this something you've added in this unified SDK? If not, do you know anything about it?In SDK 2.2 the code to handle bullets was split into a monster-only and a player-only version. The monster-only version still has the hit sound code:
As well as making the crosshair similar to the WON crosshairs?What's different about the crosshairs? Are you talking about sv_aim or something else?
And so for the sound then it would require a big rework or something. I wonder how Sven Coop achieved this. I'm just looking for the most nostalgia way to play half life for myself lolThey probably added the hit sound on the server sound, or they added a new variable sent to the client to let it pick the right sound (they have engine access, so they can do that).
If you switch to the D3D renderer in WON it'll do the same thing.I was rendering the WON version just now in OpenGL, which is what I use on Steam too. Hmm. Sven Coop also uses the WON style crosshair lol.
In SDK 2.2 the code to handle bullets was split into a monster-only and a player-only version. The monster-only version still has the hit sound code:I was reading the code for this Solo, and in both cases for monster and player, they both have this line inside of the
https://github.com/SamVanheer/Half-Life-Tools-Backup/blame/10b7ac15af91e50dad170af48bf77dcdf18dadf6/src/dlls/combat.cpp#L1456
The player-only one does not:
https://github.com/SamVanheer/Half-Life-Tools-Backup/blame/10b7ac15af91e50dad170af48bf77dcdf18dadf6/src/dlls/combat.cpp#L1551-L1553
The sounds are played on the client side for players:
https://github.com/SamVanheer/Half-Life-Tools-Backup/blob/10b7ac15af91e50dad170af48bf77dcdf18dadf6/src/cl_dll/ev_hldm.cpp#L413-L418
But since the client doesn't know what exactly is getting hit it can only handle sounds properly for players and the world:
https://github.com/SamVanheer/Half-Life-Tools-Backup/blob/10b7ac15af91e50dad170af48bf77dcdf18dadf6/src/cl_dll/ev_hldm.cpp#L115-L151
For reference, the server's version:
https://github.com/SamVanheer/Half-Life-Tools-Backup/blob/10b7ac15af91e50dad170af48bf77dcdf18dadf6/src/dlls/sound.cpp#L1646-L1651
if (iDamage)
statementTEXTURETYPE_PlaySound(&tr, vecSrc, vecEnd, iBulletType);
TEXTURETYPE_PlaySound(&tr, vecSrc, vecEnd, iBulletType);
in the if (iDamage)
statement, but not in the switch statement in the case of BULLET_PLAYER_9MM
?iDamage
is used to pass a custom damage value to FireBulletsPlayer
. None of the weapons do that, so that code never gets executed. As for why it plays sounds, that's probably a leftover because nobody realized it was still there. Since nothing actually causes that code to execute nobody noticed.tr
variable?What happens with the traceresult at this line https://github.com/SamVanheer/Half-Life-Tools-Backup/blame/10b7ac15af91e50dad170af48bf77dcdf18dadf6/src/dlls/combat.cpp#L1534 ? That doesn't figure out what was hit? Does it not modify the tr variable?The function does modify the trace result. The last parameter passed in is a pointer to the result variable which the trace result is stored in. It's used to determine which entity to deal damage to.
TEXTURETYPE_PlaySound(&tr, vecSrc, vecEnd, iBulletType);
inside the switch statement, no? All the appropriate variables are needed for the call, the traceresult knows the entity that was hitI did see that it was a pointer, and so then if it correctly does the traceresult, you can call TEXTURETYPE_PlaySound(&tr, vecSrc, vecEnd, iBulletType); inside the switch statement, no? All the appropriate variables are needed for the call, the traceresult knows the entity that was hitYou can, but the client is playing sounds as well so you'll end up playing them twice.
I know there's something I'm not understanding about how this engine works... is EV_HLDM.CPP where client processing happens? Why is it called HLDM if so?It's called that because the client side code was added for multiplayer. It was originally server only, but to compensate for lag it was moved to the client. Half-Life multiplayer is Half-Life Deathmatch, hence HLDM.
You can, but the client is playing sounds as well so you'll end up playing them twice.But the bullet hit flesh sound doesn't play either way, it's never called, so if you call TEXTURETYPE_PlaySound it will play the correct flesh hit sound?
But the bullet hit flesh sound doesn't play either way, it's never called, so if you call TEXTURETYPE_PlaySound it will play the correct flesh hit sound?The client is playing a sound, it's just not the flesh sound. If it can't identify the surface type it defaults to concrete:
skill.cfg
entirely and removes the hard-coded values used in multiplayer. It eliminates the need to define, register and synchronize cvars and variables in the skilldata_t
struct while also allowing you to change the values in real-time (except in those cases where the values are cached, like an NPC's health for example).sk_
prefix from all skill variables. This prefix was only needed because the values were stored in cvars. Removing this removes the visual noise and repetition, and also reduces memory usage a bit (not by any significant amount). Variables that now start with a number have been renamed, in all cases this meant swapping the words in the name from numbermm_bullet
to bullet_numbermm
.sk_reload
after changing the skill level) so storing this information was pointless and wasteful.sk_set
command no longer takes a skill level parameter and the sk_find
command now prints the value for each variable it finds.Skill2Json
. This tool converts original Half-Life skill.cfg
files to the Unified SDK's skill.json
format.Tokenizer
struct that can extract tokens out of text just like the game's COM_Parse
function. I haven't published a new version on Nuget yet since i'm probably adding more to the library soon.IMPLEMENT_CUSTOM_SCHEDULES
and DECLARE_COMMAND
macros to Visual Studio hint file so Intellisense stops showing green squiggles under usagesweapon_*
command was interpreted as a weapon selection command)CGameRules
so the destructor runs properlyspectator
client command to function outside of the CTF gamemode. This command was made unavailable due to Opposing Force using an older version of the SDK as a base which didn't support spectator mode in all game modes. Opposing Force Updated does this for consistency with the original game, the Unified SDK allows the use of this mode in all game modes just like the Half-Life SDK does (even in singleplayer)ScoreInfo
messages with incorrect contents causing a fatal error (Opposing Force doesn't send the player class and team index values because it uses a different method of team-based gameplay)OnCreate
, if a mapper-provided value exists it will be overridden in DispatchKeyValue
. All code uses pev->model
now to support custom models, although some edge cases may existOnCreate
now)PRECACHE_SOUND
macro has been removed, either CBaseEntity::PrecacheSound
(for entities) or UTIL_PrecacheSound
(for global precaching) should be used instead, or g_engfuncs.PfnPrecacheSound
if you need to precache a sound unaffected by global model replacementSET_MODEL
macro has been removed, CBaseEntity::SetModel
should be used insteadweapon_eagle
saving and restoring booleans as integers{
"Sections": [
{
"Name": "HudColor",
"Color": "255 160 0"
},
{
"Name": "SuitLightType",
"Type": "flashlight"
}
]
}
These are the default values as well.{
"Sections": [
{
"Name": "HudColor",
"Color": "0 160 0"
},
{
"Name": "SuitLightType",
"Type": "nightvision"
},
{
"Name": "GlobalModelReplacement",
"FileName": "cfg/maps/Op4ModelReplacement.json"
}
]
}
This configures the HUD color to Opposing Force green, the suit light type to night vision and it uses the Opposing Force model replacement file, which looks like this:
{
"models/v_9mmar.mdl": "models/op4/v_9mmar.mdl",
"models/v_9mmhandgun.mdl": "models/op4/v_9mmhandgun.mdl",
"models/v_357.mdl": "models/op4/v_357.mdl",
"models/v_chub.mdl": "models/op4/v_chub.mdl",
"models/v_crossbow.mdl": "models/op4/v_crossbow.mdl",
"models/v_crowbar.mdl": "models/op4/v_crowbar.mdl",
"models/v_desert_eagle.mdl": "models/op4/v_desert_eagle.mdl",
"models/v_displacer.mdl": "models/op4/v_displacer.mdl",
"models/v_egon.mdl": "models/op4/v_egon.mdl",
"models/v_gauss.mdl": "models/op4/v_gauss.mdl",
"models/v_grenade.mdl": "models/op4/v_grenade.mdl",
"models/v_hgun.mdl": "models/op4/v_hgun.mdl",
"models/v_knife.mdl": "models/op4/v_knife.mdl",
"models/v_m40a1.mdl": "models/op4/v_m40a1.mdl",
"models/v_penguin.mdl": "models/op4/v_penguin.mdl",
"models/v_pipe_wrench.mdl": "models/op4/v_pipe_wrench.mdl",
"models/v_rpg.mdl": "models/op4/v_rpg.mdl",
"models/v_satchel.mdl": "models/op4/v_satchel.mdl",
"models/v_satchel_radio.mdl": "models/op4/v_satchel_radio.mdl",
"models/v_saw.mdl": "models/op4/v_saw.mdl",
"models/v_shock.mdl": "models/op4/v_shock.mdl",
"models/v_shotgun.mdl": "models/op4/v_shotgun.mdl",
"models/v_spore_launcher.mdl": "models/op4/v_spore_launcher.mdl",
"models/v_squeak.mdl": "models/op4/v_squeak.mdl",
"models/v_tripmine.mdl": "models/op4/v_tripmine.mdl"
}
The third map uses this:
{
"Sections": [
{
"Name": "HudColor",
"Color": "95 95 255"
},
{
"Name": "SuitLightType",
"Type": "flashlight"
},
{
"Name": "GlobalModelReplacement",
"FileName": "cfg/maps/BlueShiftModelReplacement.json"
}
]
}
Same thing as Opposing Force.PackageName-YYYY-MM-DD-Revision.zip
where Revision is the N'th package that's been made) to make it easier to distribute