diff --git a/Directory.Build.targets b/Directory.Build.targets
new file mode 100644
index 000000000..bbf1d9fc0
--- /dev/null
+++ b/Directory.Build.targets
@@ -0,0 +1,7 @@
+
+
+
+
+
+
diff --git a/Game.Client/IClientLeafSystem.cs b/Game.Client/IClientLeafSystem.cs
index 631425008..ad0a4b1e8 100644
--- a/Game.Client/IClientLeafSystem.cs
+++ b/Game.Client/IClientLeafSystem.cs
@@ -5,6 +5,7 @@
using Source.Common.Engine;
using System.Numerics;
+using System.Runtime.InteropServices;
namespace Game.Client;
@@ -32,7 +33,11 @@ public struct Entry
public readonly int[] RenderGroupCounts = new int[RENDER_GROUP_COUNT];
public void Init() {
- Array.Clear(RenderGroups);
+ for (int i = 0; i < RENDER_GROUP_COUNT; i++) {
+ int count = RenderGroupCounts[i];
+ if (count > 0)
+ MemoryMarshal.CreateSpan(ref RenderGroups[i, 0], count).Clear();
+ }
Array.Clear(RenderGroupCounts);
}
diff --git a/Game.Server/AI_BaseNPC.cs b/Game.Server/AI_BaseNPC.cs
index f5c5c6ba8..09b151355 100644
--- a/Game.Server/AI_BaseNPC.cs
+++ b/Game.Server/AI_BaseNPC.cs
@@ -6674,11 +6674,11 @@ public void CheckTarget(BaseEntity target) {
UpdateTargetPos();
}
- public void RememberUnreachable(BaseEntity entity, float duration = -1) {
+ public void RememberUnreachable(BaseEntity entity, TimeUnit_t duration = -1) {
if (entity == GetEnemy())
ForceChooseNewEnemy();
- float NPC_UNREACHABLE_TIMEOUT = (duration > 0.0) ? duration : 3;
+ TimeUnit_t NPC_UNREACHABLE_TIMEOUT = (duration > 0.0) ? duration : 3;
for (int i = UnreachableEnts.Count - 1; i >= 0; i--) {
if (entity == UnreachableEnts[i].UnreachableEntity.Get()) {
UnreachableEnt existing = UnreachableEnts[i];
diff --git a/Game.Server/BaseFlex.cs b/Game.Server/BaseFlex.cs
index f5cccaad5..49b4a48fb 100644
--- a/Game.Server/BaseFlex.cs
+++ b/Game.Server/BaseFlex.cs
@@ -13,12 +13,14 @@
using System.Numerics;
namespace Game.Server;
+
using FIELD = Source.FIELD;
using EventType = Game.Shared.ChoreoEvent.EventType;
[LinkEntityToClass("funCBaseFlex")]
[NetworkName("CBaseFlex")]
-public class BaseFlex : BaseAnimatingOverlay {
+public class BaseFlex : BaseAnimatingOverlay
+{
public static readonly SendTable DT_BaseFlex = new(DT_BaseAnimatingOverlay, [
SendPropArray3 (FIELD.OF_ARRAY(nameof(FlexWeight)), SendPropFloat(FIELD.OF_ARRAY(nameof(FlexWeight)), 12, PropFlags.RoundDown, 0.0f, 1.0f )),
SendPropInt (FIELD.OF(nameof(BlinkToggle)), 1, PropFlags.Unsigned ),
@@ -286,7 +288,7 @@ public void AddSceneEvent(ChoreoScene? scene, ChoreoEvent? ev, BaseEntity? targe
ChoreoActor? actor = ev.GetActor();
if (actor == null) {
- Msg("CBaseFlex::AddSceneEvent: event->GetActor() was NULL!!!\n");
+ Msg("CBaseFlex::AddSceneEvent: event.GetActor() was NULL!!!\n");
return;
}
@@ -434,7 +436,7 @@ bool HandleStartGestureSceneEvent(SceneEventInfo info, ChoreoScene scene, Choreo
EventAbsoluteTag? origTag = ev.FindAbsoluteTag(ChoreoEvent.AbsTagType.Original, kvTags.Name);
if (origTag != null) {
if (MathF.Abs(origTag.GetPercentage() - percentage) > 0.05) {
- DevWarning($"{scene.GetFilename()} repositioned tag: {kvTags.Name} : {origTag.GetPercentage():F3} -> {percentage:F3} ({scene.GetFilename()}:{actor.GetName()}:{ev.GetParameters()})\n");
+ DevWarning($"{scene.GetFilename()} repositioned tag: {kvTags.Name} : {origTag.GetPercentage():F3} . {percentage:F3} ({scene.GetFilename()}:{actor.GetName()}:{ev.GetParameters()})\n");
origTag.SetPercentage(percentage);
}
}
@@ -830,10 +832,31 @@ bool ProcessMoveToSceneEvent(SceneEventInfo info, ChoreoScene scene, ChoreoEvent
if (ev.GetForceShortMovement())
distTolerance = 0.1f;
+ AI_NavGoal goal = new AI_NavGoal(NavGoalType.TargetEnt, moveActivity, distTolerance, AI_NavGoalFlags.UpdateTargetPos);
float dist = (info.Target.Get()!.EyePosition() - GetAbsOrigin()).Length2D();
- if (dist > Math.Max(Math.Max(distTolerance, 0.1f), ev.GetDistanceToTarget()))
- throw new NotImplementedException();
+ if (dist > Math.Max(Math.Max(distTolerance, 0.1f), ev.GetDistanceToTarget())) {
+ bool result = false;
+
+ if (!myNpc.IsUnreachable(info.Target.Get()!)) {
+ result = myNpc.GetNavigator()!.SetGoal(in goal, AI_NavSetGoalFlags.ClearTarget);
+ if (!result)
+ myNpc.RememberUnreachable(info.Target.Get()!, 1.5);
+ }
+
+ if (result) {
+ myNpc.GetNavigator()!.SetMovementActivity(moveActivity);
+ myNpc.GetNavigator()!.SetArrivalDistance(ev.GetDistanceToTarget());
+ info.IsMoving = true;
+ }
+ else {
+ if (developer.GetInt() > 0 && scene_showmoveto.GetBool()) {
+ myNpc.GetMoveProbe()!.FloorPoint(info.Target.Get()!.EyePosition(), Mask.NPCSolid, 0, -64, out Vector3 testPoint);
+ DebugOverlay.HorzArrow(GetAbsOrigin() + new Vector3(0, 0, 1), testPoint + new Vector3(0, 0, 1), 4, 255, 0, 255, 0, false, 0.12f);
+ DebugOverlay.Box(testPoint, myNpc.GetHullMins(), myNpc.GetHullMaxs(), 255, 0, 255, 0, 0.12f);
+ }
+ }
+ }
else
info.HasArrived = true;
}
diff --git a/Source.CodeAnalysis.ConVars/ConVarRegistrationGenerator.cs b/Source.CodeAnalysis.ConVars/ConVarRegistrationGenerator.cs
new file mode 100644
index 000000000..e3b205d2d
--- /dev/null
+++ b/Source.CodeAnalysis.ConVars/ConVarRegistrationGenerator.cs
@@ -0,0 +1,528 @@
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using Microsoft.CodeAnalysis.Text;
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+
+namespace Source.CodeAnalysis.ConVars
+{
+ ///
+ /// Emits Source.Generated.CvarRegistration for every [SourceDll] assembly. The engine uses it in place of the
+ /// reflection sweep in EngineBuilder.ConVar_Register: one registrar per type that declares ConVar fields/properties
+ /// or [ConCommand] methods. Private members are reached with [UnsafeAccessor], so the declaring code needs no changes.
+ /// Anything the generator cannot express exactly is listed for the engine to handle through reflection instead.
+ ///
+ [Generator(LanguageNames.CSharp)]
+ public sealed class ConVarRegistrationGenerator : IIncrementalGenerator
+ {
+ const string Commands = "Source.Common.Commands.";
+ const string G = "global::";
+ const string CompilerServices = "global::System.Runtime.CompilerServices.";
+ const string RegistrarType = "global::System.Action>";
+
+ public void Initialize(IncrementalGeneratorInitializationContext context) {
+ context.RegisterSourceOutput(context.CompilationProvider, static (spc, compilation) => Execute(spc, compilation));
+ }
+
+ sealed class Symbols
+ {
+ public Compilation Compilation = null!;
+ public INamedTypeSymbol ConVar = null!;
+ public INamedTypeSymbol? ConVarRef;
+ public INamedTypeSymbol ConCommandAttribute = null!;
+ public INamedTypeSymbol? CvarIgnoreAttribute;
+ public INamedTypeSymbol TokenizedCommand = null!;
+ public INamedTypeSymbol CommandSource = null!;
+ public INamedTypeSymbol? EnumerableOfString;
+ public bool HasUnsafeAccessorType;
+ public string AssemblyName = "";
+ }
+
+ enum MemberKind { Property, Field, Method }
+
+ sealed class Completion
+ {
+ public string Name = "";
+ public bool Direct;
+ public string ReturnType = "";
+ }
+
+ sealed class Member
+ {
+ public MemberKind Kind;
+ public string Name = "";
+ public bool IsStatic;
+ public bool Inheritable;
+ public bool Direct;
+ public string ValueType = "";
+ public int Callback;
+ public string CommandName = "";
+ public string HelpText = "null";
+ public string Flags = "0";
+ public Completion? Completion;
+ public int Id;
+ }
+
+ sealed class TypeModel
+ {
+ public string MetadataName = "";
+ public string FullyQualified = "";
+ public readonly List Members = new List();
+ }
+
+ static void Execute(SourceProductionContext spc, Compilation compilation) {
+ if (!compilation.Assembly.GetAttributes().Any(a => a.AttributeClass?.ToDisplayString() == "Source.Common.SourceDllAttribute"))
+ return;
+
+ INamedTypeSymbol? conVar = compilation.GetTypeByMetadataName(Commands + "ConVar");
+ INamedTypeSymbol? conCommandAttribute = compilation.GetTypeByMetadataName(Commands + "ConCommandAttribute");
+ INamedTypeSymbol? tokenizedCommand = compilation.GetTypeByMetadataName(Commands + "TokenizedCommand");
+ INamedTypeSymbol? commandSource = compilation.GetTypeByMetadataName(Commands + "CommandSource");
+ if (conVar == null || conCommandAttribute == null || tokenizedCommand == null || commandSource == null || compilation.GetTypeByMetadataName(Commands + "ICvar") == null)
+ return;
+
+ Symbols s = new Symbols {
+ Compilation = compilation,
+ ConVar = conVar,
+ ConVarRef = compilation.GetTypeByMetadataName(Commands + "ConVarRef"),
+ ConCommandAttribute = conCommandAttribute,
+ CvarIgnoreAttribute = compilation.GetTypeByMetadataName(Commands + "CvarIgnoreAttribute"),
+ TokenizedCommand = tokenizedCommand,
+ CommandSource = commandSource,
+ EnumerableOfString = compilation.GetTypeByMetadataName("System.Collections.Generic.IEnumerable`1")?.Construct(compilation.GetSpecialType(SpecialType.System_String)),
+ HasUnsafeAccessorType = compilation.GetTypeByMetadataName("System.Runtime.CompilerServices.UnsafeAccessorTypeAttribute") != null,
+ AssemblyName = compilation.AssemblyName ?? "",
+ };
+
+ List models = new List();
+ List reflected = new List();
+ int nextId = 0;
+ foreach (INamedTypeSymbol type in AllTypes(compilation.Assembly.GlobalNamespace)) {
+ spc.CancellationToken.ThrowIfCancellationRequested();
+ Analyze(s, type, models, reflected, ref nextId);
+ }
+
+ spc.AddSource("CvarRegistration.g.cs", SourceText.From(Emit(s, models, reflected), Encoding.UTF8));
+ }
+
+ static IEnumerable AllTypes(INamespaceSymbol ns) {
+ foreach (INamedTypeSymbol type in ns.GetTypeMembers())
+ foreach (INamedTypeSymbol t in SelfAndNested(type))
+ yield return t;
+
+ foreach (INamespaceSymbol child in ns.GetNamespaceMembers())
+ foreach (INamedTypeSymbol t in AllTypes(child))
+ yield return t;
+ }
+
+ static IEnumerable SelfAndNested(INamedTypeSymbol type) {
+ yield return type;
+ foreach (INamedTypeSymbol nested in type.GetTypeMembers())
+ foreach (INamedTypeSymbol t in SelfAndNested(nested))
+ yield return t;
+ }
+
+ static bool DerivesFrom(ITypeSymbol? type, INamedTypeSymbol target) {
+ for (ITypeSymbol? t = type; t != null; t = t.BaseType)
+ if (SymbolEqualityComparer.Default.Equals(t, target))
+ return true;
+ return false;
+ }
+
+ static bool IsConVarType(Symbols s, ITypeSymbol type) {
+ if (type is ITypeParameterSymbol tp)
+ return tp.ConstraintTypes.Any(c => IsConVarType(s, c));
+ return DerivesFrom(type, s.ConVar);
+ }
+
+ static bool HasAttribute(ISymbol symbol, INamedTypeSymbol? attribute)
+ => attribute != null && symbol.GetAttributes().Any(a => DerivesFrom(a.AttributeClass, attribute));
+
+ static AttributeData? FindAttribute(ISymbol symbol, INamedTypeSymbol attribute)
+ => symbol.GetAttributes().FirstOrDefault(a => DerivesFrom(a.AttributeClass, attribute));
+
+ static string MetadataName(INamedTypeSymbol type) {
+ if (type.ContainingType != null)
+ return MetadataName(type.ContainingType) + "+" + type.MetadataName;
+ if (type.ContainingNamespace == null || type.ContainingNamespace.IsGlobalNamespace)
+ return type.MetadataName;
+ return type.ContainingNamespace.ToDisplayString() + "." + type.MetadataName;
+ }
+
+ static string Fq(ITypeSymbol type) => type.WithNullableAnnotation(NullableAnnotation.NotAnnotated).ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
+ static string Literal(string value) => SymbolDisplay.FormatLiteral(value, true);
+
+ static bool IsGeneric(INamedTypeSymbol type) {
+ for (INamedTypeSymbol? t = type; t != null; t = t.ContainingType)
+ if (t.Arity > 0)
+ return true;
+ return false;
+ }
+
+ static bool IsNameable(Symbols s, ITypeSymbol type) {
+ switch (type) {
+ case IArrayTypeSymbol array:
+ return IsNameable(s, array.ElementType);
+ case INamedTypeSymbol named:
+ for (INamedTypeSymbol? t = named; t != null; t = t.ContainingType)
+ if (t.IsFileLocal || t.IsAnonymousType)
+ return false;
+ if (!s.Compilation.IsSymbolAccessibleWithin(named, s.Compilation.Assembly))
+ return false;
+ return named.TypeArguments.All(a => IsNameable(s, a));
+ default:
+ return false;
+ }
+ }
+
+ static bool CapturesConVarInPrimaryConstructor(Symbols s, INamedTypeSymbol type) {
+ foreach (IMethodSymbol ctor in type.InstanceConstructors) {
+ if (!ctor.DeclaringSyntaxReferences.Any(r => r.GetSyntax() is TypeDeclarationSyntax))
+ continue;
+ if (ctor.Parameters.Any(p => IsConVarType(s, p.Type)))
+ return true;
+ }
+ return false;
+ }
+
+ static void Analyze(Symbols s, INamedTypeSymbol type, List models, List reflected, ref int nextId) {
+ if (type.TypeKind != TypeKind.Class && type.TypeKind != TypeKind.Struct && type.TypeKind != TypeKind.Interface)
+ return;
+ if (DerivesFrom(type, s.ConVar) || (s.ConVarRef != null && SymbolEqualityComparer.Default.Equals(type, s.ConVarRef)))
+ return;
+
+ bool nameable = IsNameable(s, type);
+ bool isStruct = type.TypeKind == TypeKind.Struct;
+ bool isInterface = type.TypeKind == TypeKind.Interface;
+ bool candidates = false;
+ bool reflect = false;
+ TypeModel model = new TypeModel { MetadataName = MetadataName(type), FullyQualified = Fq(type) };
+
+ if (type.TypeKind != TypeKind.Interface && CapturesConVarInPrimaryConstructor(s, type)) {
+ candidates = true;
+ reflect = true;
+ }
+
+ foreach (ISymbol symbol in type.GetMembers()) {
+ switch (symbol) {
+ case IPropertySymbol property: {
+ if (property.IsIndexer || !IsConVarType(s, property.Type) || HasAttribute(property, s.CvarIgnoreAttribute))
+ break;
+ if (property.GetMethod == null || property.GetMethod.DeclaredAccessibility != Accessibility.Public)
+ break;
+
+ candidates = true;
+ if (isInterface ? property.IsStatic : (property.IsVirtual || property.IsAbstract || property.IsOverride))
+ reflect = true;
+ if (isStruct && !property.IsStatic)
+ reflect = true;
+ if (!nameable && !CanUseAccessor(s, type, property.Type))
+ reflect = true;
+
+ model.Members.Add(new Member {
+ Kind = MemberKind.Property,
+ Name = property.Name,
+ IsStatic = property.IsStatic,
+ Inheritable = !property.IsStatic && !isInterface,
+ Direct = nameable,
+ ValueType = Fq(property.Type),
+ Id = nextId++,
+ });
+ break;
+ }
+ case IFieldSymbol field: {
+ if (!IsConVarType(s, field.Type) || HasAttribute(field, s.CvarIgnoreAttribute))
+ break;
+
+ candidates = true;
+ if (isStruct && !field.IsStatic)
+ reflect = true;
+
+ bool direct = nameable && field.AssociatedSymbol == null && field.CanBeReferencedByName && s.Compilation.IsSymbolAccessibleWithin(field, s.Compilation.Assembly);
+ if (!direct && !CanUseAccessor(s, type, field.Type))
+ reflect = true;
+
+ model.Members.Add(new Member {
+ Kind = MemberKind.Field,
+ Name = field.Name,
+ IsStatic = field.IsStatic,
+ Inheritable = !field.IsStatic && field.DeclaredAccessibility != Accessibility.Private,
+ Direct = direct,
+ ValueType = Fq(field.Type),
+ Id = nextId++,
+ });
+ break;
+ }
+ case IMethodSymbol method: {
+ AttributeData? attribute = FindAttribute(method, s.ConCommandAttribute);
+ if (attribute == null) {
+ for (IMethodSymbol? overridden = method.OverriddenMethod; overridden != null; overridden = overridden.OverriddenMethod) {
+ if (FindAttribute(overridden, s.ConCommandAttribute) != null) {
+ candidates = true;
+ reflect = true;
+ break;
+ }
+ }
+ break;
+ }
+
+ candidates = true;
+ Member? member = AnalyzeCommand(s, type, method, attribute, nameable, isStruct);
+ if (member == null)
+ reflect = true;
+ else {
+ member.Id = nextId++;
+ model.Members.Add(member);
+ }
+ break;
+ }
+ }
+ }
+
+ if (!candidates)
+ return;
+
+ if (reflect || IsGeneric(type))
+ reflected.Add(model.MetadataName);
+ else
+ models.Add(model);
+ }
+
+ static bool CanUseAccessor(Symbols s, INamedTypeSymbol owner, ITypeSymbol valueType)
+ => s.HasUnsafeAccessorType && owner.TypeKind == TypeKind.Class && !IsGeneric(owner) && IsNameable(s, valueType);
+
+ static Member? AnalyzeCommand(Symbols s, INamedTypeSymbol type, IMethodSymbol method, AttributeData attribute, bool nameable, bool isStruct) {
+ if (method.MethodKind != MethodKind.Ordinary || method.IsGenericMethod || !method.ReturnsVoid)
+ return null;
+ if (method.IsVirtual || method.IsAbstract || method.IsOverride || type.TypeKind == TypeKind.Interface)
+ return null;
+ if (isStruct && !method.IsStatic)
+ return null;
+ if (!SymbolEqualityComparer.Default.Equals(attribute.AttributeClass, s.ConCommandAttribute) || attribute.ConstructorArguments.Length != 4 || attribute.NamedArguments.Length != 0)
+ return null;
+
+ int callback;
+ var parameters = method.Parameters;
+ if (parameters.Length == 0)
+ callback = 0;
+ else if (parameters.Length == 1 && IsCommandParameter(s, parameters[0]))
+ callback = 1;
+ else if (parameters.Length == 3 && IsCommandParameter(s, parameters[0])
+ && parameters[1].RefKind == RefKind.None && SymbolEqualityComparer.Default.Equals(parameters[1].Type, s.CommandSource)
+ && parameters[2].RefKind == RefKind.None && parameters[2].Type.SpecialType == SpecialType.System_Int32)
+ callback = 2;
+ else
+ return null;
+
+ TypedConstant name = attribute.ConstructorArguments[0];
+ TypedConstant help = attribute.ConstructorArguments[1];
+ TypedConstant flags = attribute.ConstructorArguments[2];
+ TypedConstant complete = attribute.ConstructorArguments[3];
+ if (name.Kind == TypedConstantKind.Error || help.Kind == TypedConstantKind.Error || flags.Kind == TypedConstantKind.Error || complete.Kind == TypedConstantKind.Error || flags.Type == null)
+ return null;
+
+ bool direct = nameable && s.Compilation.IsSymbolAccessibleWithin(method, s.Compilation.Assembly);
+ if (!direct && !CanUseAccessor(s, type, s.TokenizedCommand))
+ return null;
+
+ Member member = new Member {
+ Kind = MemberKind.Method,
+ Name = method.Name,
+ IsStatic = method.IsStatic,
+ Inheritable = !method.IsStatic && method.DeclaredAccessibility != Accessibility.Private,
+ Direct = direct,
+ Callback = callback,
+ CommandName = Literal(name.Value as string ?? method.Name),
+ HelpText = help.Value is string helpText ? Literal(helpText) : "null",
+ Flags = "unchecked((" + Fq(flags.Type) + ")(" + (flags.Value is ulong u ? u.ToString() + "UL" : Convert.ToInt64(flags.Value ?? 0).ToString() + "L") + "))",
+ };
+
+ if (complete.Value is string completeName) {
+ IMethodSymbol[] found = type.GetMembers(completeName).OfType().ToArray();
+ if (found.Length != 1)
+ return null;
+
+ IMethodSymbol completion = found[0];
+ if (completion.MethodKind != MethodKind.Ordinary || completion.IsGenericMethod)
+ return null;
+
+ bool bindable = completion.IsStatic == method.IsStatic && s.EnumerableOfString != null && !completion.ReturnsVoid
+ && completion.Parameters.Length == 1 && completion.Parameters[0].RefKind == RefKind.None;
+ if (bindable && completion.Parameters[0].Type.SpecialType != SpecialType.System_String)
+ return null;
+
+ if (bindable) {
+ Conversion conversion = ((CSharpCompilation)s.Compilation).ClassifyConversion(completion.ReturnType, s.EnumerableOfString!);
+ if (!conversion.IsIdentity && !(conversion.IsImplicit && conversion.IsReference))
+ bindable = false;
+ }
+
+ if (bindable) {
+ bool completionDirect = nameable && s.Compilation.IsSymbolAccessibleWithin(completion, s.Compilation.Assembly);
+ if (!completionDirect && !CanUseAccessor(s, type, completion.ReturnType))
+ return null;
+
+ member.Completion = new Completion { Name = completion.Name, Direct = completionDirect, ReturnType = Fq(completion.ReturnType) };
+ }
+ }
+
+ return member;
+ }
+
+ static bool IsCommandParameter(Symbols s, IParameterSymbol parameter)
+ => parameter.RefKind == RefKind.In && SymbolEqualityComparer.Default.Equals(parameter.Type, s.TokenizedCommand);
+
+ static string Emit(Symbols s, List models, List reflected) {
+ StringBuilder sb = new StringBuilder();
+ StringBuilder accessors = new StringBuilder();
+ sb.Append("// \n#nullable disable\n#pragma warning disable\n\nnamespace Source.Generated\n{\n");
+ sb.Append("\tinternal static class CvarRegistration\n\t{\n");
+ sb.Append("\t\tpublic static void Populate(global::System.Collections.Generic.Dictionary declared, global::System.Collections.Generic.Dictionary inherited, global::System.Collections.Generic.List reflected) {\n");
+ for (int i = 0; i < models.Count; i++) {
+ sb.Append("\t\t\tdeclared.Add(").Append(Literal(models[i].MetadataName)).Append(", D").Append(i).Append(");\n");
+ if (models[i].Members.Any(m => m.Inheritable))
+ sb.Append("\t\t\tinherited.Add(").Append(Literal(models[i].MetadataName)).Append(", I").Append(i).Append(");\n");
+ }
+ foreach (string name in reflected)
+ sb.Append("\t\t\treflected.Add(").Append(Literal(name)).Append(");\n");
+ sb.Append("\t\t}\n\n");
+
+ sb.Append("\t\tstatic object Inst(global::System.Func resolve, global::System.Type declaring, global::System.Type type, bool concommand, string name) {\n");
+ sb.Append("\t\t\tobject instance = resolve(type, concommand, name);\n");
+ sb.Append("\t\t\tif (!declaring.IsInstanceOfType(instance))\n");
+ sb.Append("\t\t\t\tthrow new global::System.InvalidCastException($\"The instance resolved for '{type}' is not a '{declaring}', which declares '{name}'.\");\n");
+ sb.Append("\t\t\treturn instance;\n\t\t}\n\n");
+ sb.Append("\t\tstatic void Reg(global::Source.Common.Commands.ICvar cvar, global::Source.Common.Commands.ConVar cv, string name) {\n");
+ sb.Append("\t\t\tif (cv == null)\n\t\t\t\treturn;\n");
+ sb.Append("\t\t\tif (cv.GetName() == null)\n\t\t\t\tcv.SetName(name);\n");
+ sb.Append("\t\t\tcvar.RegisterConCommand(cv);\n\t\t}\n");
+
+ for (int i = 0; i < models.Count; i++) {
+ EmitRegistrar(s, sb, accessors, models[i], "D" + i, inheritedOnly: false);
+ if (models[i].Members.Any(m => m.Inheritable))
+ EmitRegistrar(s, sb, accessors, models[i], "I" + i, inheritedOnly: true);
+ }
+
+ sb.Append(accessors);
+ sb.Append("\t}\n}\n");
+ return sb.ToString();
+ }
+
+ static void EmitRegistrar(Symbols s, StringBuilder sb, StringBuilder accessors, TypeModel model, string name, bool inheritedOnly) {
+ sb.Append("\n\t\t// ").Append(model.MetadataName).Append(inheritedOnly ? " (members inherited by derived types)" : "").Append('\n');
+ sb.Append("\t\tstatic void ").Append(name).Append("(int phase, global::System.Type declaring, global::System.Type type, global::Source.Common.Commands.ICvar cvar, global::System.Func resolve) {\n");
+ sb.Append("\t\t\tswitch (phase) {\n");
+ foreach (MemberKind kind in new[] { MemberKind.Property, MemberKind.Field, MemberKind.Method }) {
+ // The runtime lists a type's instance fields before its static fields; keep that order so that
+ // duplicate names resolve to the same parent ConVar as they do through reflection.
+ Member[] members = model.Members.Where(m => m.Kind == kind && (!inheritedOnly || m.Inheritable))
+ .OrderBy(m => kind == MemberKind.Field && m.IsStatic ? 1 : 0).ToArray();
+ if (members.Length == 0)
+ continue;
+
+ sb.Append("\t\t\t\tcase ").Append((int)kind).Append(":\n");
+ foreach (Member member in members) {
+ if (!inheritedOnly)
+ EmitAccessors(s, accessors, model, member);
+ if (kind == MemberKind.Method)
+ EmitCommand(sb, model, member);
+ else
+ EmitConVar(sb, model, member);
+ }
+ sb.Append("\t\t\t\t\tbreak;\n");
+ }
+ sb.Append("\t\t\t}\n\t\t}\n");
+ }
+
+ static string Id(string name) => "@" + name;
+
+ static void EmitConVar(StringBuilder sb, TypeModel model, Member member) {
+ string nameLiteral = Literal(member.Name);
+ string value;
+ if (member.Direct)
+ value = (member.IsStatic ? model.FullyQualified : "((" + model.FullyQualified + ")i)") + "." + Id(member.Name);
+ else
+ value = "A" + member.Id + "(" + (member.IsStatic ? "null" : "i") + ")";
+
+ if (member.IsStatic)
+ sb.Append("\t\t\t\t\tReg(cvar, ").Append(value).Append(", ").Append(nameLiteral).Append(");\n");
+ else
+ sb.Append("\t\t\t\t\t{ object i = Inst(resolve, declaring, type, false, ").Append(nameLiteral).Append("); Reg(cvar, ").Append(value).Append(", ").Append(nameLiteral).Append("); }\n");
+ }
+
+ static void EmitCommand(StringBuilder sb, TypeModel model, Member member) {
+ string target = member.IsStatic ? model.FullyQualified : "((" + model.FullyQualified + ")i)";
+ string arg = member.IsStatic ? "null" : "i";
+ string callbackType = G + Commands + (member.Callback == 0 ? "FnCommandCallbackVoid" : member.Callback == 1 ? "FnCommandCallback" : "FnCommandCallbackSourced");
+ string tokenized = G + Commands + "TokenizedCommand";
+ string source = G + Commands + "CommandSource";
+
+ string callback;
+ if (member.Direct)
+ callback = target + "." + Id(member.Name);
+ else if (member.Callback == 0)
+ callback = "() => A" + member.Id + "(" + arg + ")";
+ else if (member.Callback == 1)
+ callback = "(in " + tokenized + " a) => A" + member.Id + "(" + arg + ", in a)";
+ else
+ callback = "(in " + tokenized + " a, " + source + " b, int c) => A" + member.Id + "(" + arg + ", in a, b, c)";
+
+ string completion = "null";
+ if (member.Completion != null) {
+ completion = "(" + G + Commands + "FnCommandCompletionCallback)(" + (member.Completion.Direct
+ ? target + "." + Id(member.Completion.Name)
+ : "(string a) => C" + member.Id + "(" + arg + ", a)") + ")";
+ }
+
+ sb.Append("\t\t\t\t\t{ ");
+ if (!member.IsStatic)
+ sb.Append("object i = Inst(resolve, declaring, type, true, ").Append(Literal(member.Name)).Append("); ");
+ sb.Append("cvar.RegisterConCommand(new ").Append(G).Append(Commands).Append("ConCommand(").Append(member.CommandName)
+ .Append(", (").Append(callbackType).Append(")(").Append(callback).Append("), ")
+ .Append(member.HelpText).Append(", ").Append(member.Flags).Append(", ").Append(completion).Append(")); }\n");
+ }
+
+ static void EmitAccessors(Symbols s, StringBuilder accessors, TypeModel model, Member member) {
+ string owner = "[" + CompilerServices + "UnsafeAccessorType(" + Literal(model.MetadataName + ", " + s.AssemblyName) + ")] object _";
+ string tokenized = G + Commands + "TokenizedCommand";
+
+ if (!member.Direct) {
+ accessors.Append('\n');
+ switch (member.Kind) {
+ case MemberKind.Field:
+ AppendAccessorAttribute(accessors, member.IsStatic ? "StaticField" : "Field", member.Name);
+ accessors.Append("\t\tstatic extern ref ").Append(member.ValueType).Append(" A").Append(member.Id).Append('(').Append(owner).Append(");\n");
+ break;
+ case MemberKind.Property:
+ AppendAccessorAttribute(accessors, member.IsStatic ? "StaticMethod" : "Method", "get_" + member.Name);
+ accessors.Append("\t\tstatic extern ").Append(member.ValueType).Append(" A").Append(member.Id).Append('(').Append(owner).Append(");\n");
+ break;
+ case MemberKind.Method:
+ AppendAccessorAttribute(accessors, member.IsStatic ? "StaticMethod" : "Method", member.Name);
+ accessors.Append("\t\tstatic extern void A").Append(member.Id).Append('(').Append(owner);
+ if (member.Callback >= 1)
+ accessors.Append(", in ").Append(tokenized).Append(" a");
+ if (member.Callback == 2)
+ accessors.Append(", ").Append(G).Append(Commands).Append("CommandSource b, int c");
+ accessors.Append(");\n");
+ break;
+ }
+ }
+
+ if (member.Completion != null && !member.Completion.Direct) {
+ accessors.Append('\n');
+ AppendAccessorAttribute(accessors, member.IsStatic ? "StaticMethod" : "Method", member.Completion.Name);
+ accessors.Append("\t\tstatic extern ").Append(member.Completion.ReturnType).Append(" C").Append(member.Id).Append('(').Append(owner).Append(", string a);\n");
+ }
+ }
+
+ static void AppendAccessorAttribute(StringBuilder accessors, string kind, string name) {
+ accessors.Append("\t\t[").Append(CompilerServices).Append("UnsafeAccessor(").Append(CompilerServices).Append("UnsafeAccessorKind.").Append(kind)
+ .Append(", Name = ").Append(Literal(name)).Append(")]\n");
+ }
+ }
+}
diff --git a/Source.CodeAnalysis.ConVars/Source.CodeAnalysis.ConVars.csproj b/Source.CodeAnalysis.ConVars/Source.CodeAnalysis.ConVars.csproj
new file mode 100644
index 000000000..5fe2175b2
--- /dev/null
+++ b/Source.CodeAnalysis.ConVars/Source.CodeAnalysis.ConVars.csproj
@@ -0,0 +1,19 @@
+
+
+
+ netstandard2.0
+ false
+ true
+ true
+ latest
+ enable
+ false
+ Debug;Release;Debug (Dedicated);Release (Dedicated)
+
+
+
+
+
+
+
+
diff --git a/Source.Common.Tests/KeyValuesTests.cs b/Source.Common.Tests/KeyValuesTests.cs
new file mode 100644
index 000000000..ba54ca3e7
--- /dev/null
+++ b/Source.Common.Tests/KeyValuesTests.cs
@@ -0,0 +1,590 @@
+using Source.Common.Formats.Keyvalues;
+
+using System.Text;
+
+namespace Source.Common.Tests;
+
+public class KeyValuesTests
+{
+ static KeyValues Parse(string text, bool escapeSequences = false) {
+ KeyValues kv = new();
+ kv.UsesEscapeSequences(escapeSequences);
+ Assert.True(kv.LoadFromBuffer("test", text));
+ return kv;
+ }
+
+ static KeyValues Value(string rawValue, bool escapeSequences = false)
+ => Parse("\"Root\"\n{\n\t\"v\"\t" + rawValue + "\n}\n", escapeSequences).FindKey("v")!;
+
+ static string[] ChildNames(KeyValues kv) => [.. kv.Select(x => x.Name)];
+
+ [Fact]
+ public void SkipWhitespaceMatchesCharIsWhiteSpace() {
+ for (int c = char.MinValue; c <= char.MaxValue; c++) {
+ KeyValues.KeyValuesReader reader = new((char)c + "x");
+
+ Assert.Equal(char.IsWhiteSpace((char)c), KeyValues.SkipWhitespace(reader));
+ Assert.Equal(char.IsWhiteSpace((char)c) ? 'x' : c, reader.Peek());
+ }
+ }
+
+ [Fact]
+ public void QuotedRootWithPairs() {
+ KeyValues kv = Parse("\"Root\"\n{\n\t\"a\"\t\"1\"\n\t\"b\"\t\"two\"\n}\n");
+
+ Assert.Equal("Root", kv.Name);
+ Assert.Equal(KeyValues.Types.None, kv.Type);
+ Assert.Equal(2, kv.Count);
+ Assert.Equal(["a", "b"], ChildNames(kv));
+ Assert.Equal(1, kv.GetInt("a"));
+ Assert.Equal("two", kv.GetString("b").ToString());
+ }
+
+ [Fact]
+ public void UnquotedTokens() {
+ KeyValues kv = Parse("Root\n{\n\tkey value\n\tnum 42\n}\n");
+
+ Assert.Equal("Root", kv.Name);
+ Assert.Equal(["key", "num"], ChildNames(kv));
+ Assert.Equal("value", kv.GetString("key").ToString());
+ Assert.Equal(42, kv.GetInt("num"));
+ }
+
+ [Fact]
+ public void MixedQuotedAndUnquotedTokens() {
+ KeyValues kv = Parse("\"Root\"\n{\n\tkey \"quoted value\"\n\t\"quoted key\" value\n}\n");
+
+ Assert.Equal("quoted value", kv.GetString("key").ToString());
+ Assert.Equal("value", kv.GetString("quoted key").ToString());
+ }
+
+ [Fact]
+ public void NestedSectionsKeepOrder() {
+ KeyValues kv = Parse("""
+ "Root"
+ {
+ "first" "1"
+ "Section"
+ {
+ "inner" "x"
+ "Deeper"
+ {
+ "leaf" "y"
+ }
+ }
+ "last" "2"
+ }
+ """);
+
+ Assert.Equal(["first", "Section", "last"], ChildNames(kv));
+
+ KeyValues section = kv.FindKey("Section")!;
+ Assert.Equal(KeyValues.Types.None, section.Type);
+ Assert.Equal(["inner", "Deeper"], ChildNames(section));
+ Assert.Equal("x", section.GetString("inner").ToString());
+ Assert.Equal("y", section.FindKey("Deeper")!.GetString("leaf").ToString());
+ Assert.Equal(2, kv.GetInt("last"));
+ }
+
+ [Fact]
+ public void EmptySection() {
+ KeyValues kv = Parse("\"Root\"\n{\n\t\"Empty\"\n\t{\n\t}\n\t\"after\"\t\"1\"\n}\n");
+
+ KeyValues empty = kv.FindKey("Empty")!;
+ Assert.Equal(KeyValues.Types.None, empty.Type);
+ Assert.Equal(0, empty.Count);
+ Assert.Equal(1, kv.GetInt("after"));
+ }
+
+ [Fact]
+ public void CompactSingleLine() {
+ KeyValues kv = Parse("\"Root\"{\"a\" \"1\" \"Sub\"{\"b\" \"2\"}\"c\" \"3\"}");
+
+ Assert.Equal(["a", "Sub", "c"], ChildNames(kv));
+ Assert.Equal(2, kv.FindKey("Sub")!.GetInt("b"));
+ Assert.Equal(3, kv.GetInt("c"));
+ }
+
+ [Fact]
+ public void NoTrailingNewline() {
+ KeyValues kv = Parse("\"Root\"\n{\n\t\"a\"\t\"1\"\n}");
+
+ Assert.Equal(1, kv.Count);
+ Assert.Equal(1, kv.GetInt("a"));
+ }
+
+ [Fact]
+ public void WindowsLineEndingsAndTabs() {
+ KeyValues kv = Parse("\"Root\"\r\n{\r\n\t\"a\"\t\t\"1\"\r\n\t\"b\"\t\t\"two words\"\r\n}\r\n");
+
+ Assert.Equal(["a", "b"], ChildNames(kv));
+ Assert.Equal("two words", kv.GetString("b").ToString());
+ }
+
+ [Fact]
+ public void CommentsAreSkipped() {
+ KeyValues kv = Parse("""
+ // leading comment
+ "Root" // after the root name
+ {
+ // a full line comment
+ "a" "1" // trailing comment
+ //"commented" "out"
+ "b" "2"
+ }
+ // comment at the end with no newline
+ """);
+
+ Assert.Equal("Root", kv.Name);
+ Assert.Equal(["a", "b"], ChildNames(kv));
+ Assert.Equal(1, kv.GetInt("a"));
+ Assert.Equal(2, kv.GetInt("b"));
+ }
+
+ [Fact]
+ public void QuotedValueKeepsSpecialCharacters() {
+ KeyValues kv = Parse("\"Root\"\n{\n\t\"k\"\t\"a b { } // not a comment [$X360]\"\n\t\"after\"\t\"1\"\n}\n");
+
+ Assert.Equal("a b { } // not a comment [$X360]", kv.GetString("k").ToString());
+ Assert.Equal(1, kv.GetInt("after"));
+ }
+
+ [Fact]
+ public void UnquotedValueMayContainSlash() {
+ KeyValues kv = Parse("\"Root\"\n{\n\tpath sound/weapons/shot.wav\n\tabs /root/file\n}\n");
+
+ Assert.Equal("sound/weapons/shot.wav", kv.GetString("path").ToString());
+ Assert.Equal("/root/file", kv.GetString("abs").ToString());
+ }
+
+ [Fact]
+ public void DuplicateKeysAreKeptInOrder() {
+ KeyValues kv = Parse("\"Root\"\n{\n\t\"wave\"\t\"a.wav\"\n\t\"wave\"\t\"b.wav\"\n\t\"wave\"\t\"c.wav\"\n}\n");
+
+ Assert.Equal(3, kv.Count);
+ Assert.Equal(["a.wav", "b.wav", "c.wav"], kv.Select(x => x.GetString().ToString()).ToArray());
+ Assert.Equal("a.wav", kv.GetString("wave").ToString());
+ }
+
+ [Fact]
+ public void FindKeyIsCaseInsensitive() {
+ KeyValues kv = Parse("\"Root\"\n{\n\t\"MixedCase\"\t\"1\"\n}\n");
+
+ Assert.NotNull(kv.FindKey("mixedcase"));
+ Assert.NotNull(kv.FindKey("MIXEDCASE"));
+ Assert.Equal("MixedCase", kv.FindKey("mixedcase")!.Name);
+ Assert.Null(kv.FindKey("missing"));
+ }
+
+ [Theory]
+ [InlineData("\"5\"", 5)]
+ [InlineData("\"-12\"", -12)]
+ [InlineData("\"+7\"", 7)]
+ [InlineData("\"0\"", 0)]
+ [InlineData("42", 42)]
+ [InlineData("\"2147483647\"", int.MaxValue)]
+ public void IntegerValues(string raw, int expected) {
+ KeyValues v = Value(raw);
+
+ Assert.Equal(KeyValues.Types.Int, v.Type);
+ Assert.Equal(expected, Assert.IsType(v.Value));
+ }
+
+ [Theory]
+ [InlineData("\"1.5\"", 1.5)]
+ [InlineData("\"-0.25\"", -0.25)]
+ [InlineData("\".5\"", 0.5)]
+ [InlineData("\"1e3\"", 1000.0)]
+ [InlineData("0.75", 0.75)]
+ public void FloatValues(string raw, double expected) {
+ KeyValues v = Value(raw);
+
+ Assert.Equal(KeyValues.Types.Double, v.Type);
+ Assert.Equal(expected, Assert.IsType(v.Value));
+ }
+
+ [Fact]
+ public void FloatValuesAreSinglePrecision() {
+ KeyValues v = Value("\"1.1\"");
+
+ Assert.Equal((double)1.1f, Assert.IsType(v.Value));
+ }
+
+ [Theory]
+ [InlineData("\"abc\"", "abc")]
+ [InlineData("\"12abc\"", "12abc")]
+ [InlineData("\"1.5.2\"", "1.5.2")]
+ [InlineData("\"\"", "")]
+ [InlineData("\"99999999999\"", "99999999999")]
+ [InlineData("\"0x10\"", "0x10")]
+ [InlineData("\"255 128 0 255\"", "255 128 0 255")]
+ [InlineData("word", "word")]
+ public void StringValues(string raw, string expected) {
+ KeyValues v = Value(raw);
+
+ Assert.Equal(KeyValues.Types.String, v.Type);
+ Assert.Equal(expected, Assert.IsType(v.Value));
+ }
+
+ [Theory]
+ [InlineData("\"0x00000000000000FF\"", 0xFFul)]
+ [InlineData("\"0x0123456789abcdef\"", 0x0123456789ABCDEFul)]
+ [InlineData("\"0xFFFFFFFFFFFFFFFF\"", ulong.MaxValue)]
+ public void Uint64Values(string raw, ulong expected) {
+ KeyValues v = Value(raw);
+
+ Assert.Equal(KeyValues.Types.Uint64, v.Type);
+ Assert.Equal(expected, Assert.IsType(v.Value));
+ }
+
+ [Fact]
+ public void BackslashesAreLiteralWithoutEscapeSequences() {
+ KeyValues v = Value("\"C:\\dir\\new\"", escapeSequences: false);
+
+ Assert.Equal("C:\\dir\\new", v.GetString().ToString());
+ }
+
+ [Fact]
+ public void EscapeSequencesAreTranslated() {
+ KeyValues v = Value("\"line1\\nline2 q:\\\"x\\\"\"", escapeSequences: true);
+
+ Assert.Equal("line1\nline2 q:\"x\"", v.GetString().ToString());
+ }
+
+ [Fact]
+ public void ConditionalsOnValues() {
+ KeyValues kv = Parse("""
+ "Root"
+ {
+ "pc" "1" [$WIN32]
+ "notpc" "1" [!$WIN32]
+ "x360" "1" [$X360]
+ "notx360" "1" [!$X360]
+ "unknown" "1" [$SOMETHINGELSE]
+ "notunknown" "1" [!$SOMETHINGELSE]
+ "plain" "1"
+ }
+ """);
+
+ Assert.Equal(["pc", "notx360", "notunknown", "plain"], ChildNames(kv));
+ }
+
+ [Fact]
+ public void ConditionalsOnSections() {
+ KeyValues kv = Parse("""
+ "Root"
+ {
+ "Console" [$X360]
+ {
+ "a" "1"
+ }
+ "Desktop" [$WIN32]
+ {
+ "b" "2"
+ }
+ "after" "3"
+ }
+ """);
+
+ Assert.Equal(["Desktop", "after"], ChildNames(kv));
+ Assert.Equal(2, kv.FindKey("Desktop")!.GetInt("b"));
+ Assert.Equal(3, kv.GetInt("after"));
+ }
+
+ [Fact]
+ public void PlatformConditionalsMatchTheBuildPlatform() {
+ KeyValues kv = Parse("""
+ "Root"
+ {
+ "windows" "1" [$WINDOWS]
+ "linux" "1" [$LINUX]
+ "osx" "1" [$OSX]
+ "posix" "1" [$POSIX]
+ }
+ """);
+
+ Assert.Equal(OperatingSystem.IsWindows(), kv.FindKey("windows") != null);
+ Assert.Equal(OperatingSystem.IsLinux(), kv.FindKey("linux") != null);
+ Assert.Equal(OperatingSystem.IsMacOS(), kv.FindKey("osx") != null);
+ Assert.Equal(OperatingSystem.IsLinux(), kv.FindKey("posix") != null);
+ }
+
+ [Fact]
+ public void MultipleRootsBecomePeers() {
+ KeyValues kv = Parse("\"A\"\n{\n\t\"x\"\t\"1\"\n}\n\"B\"\n{\n\t\"y\"\t\"2\"\n}\n");
+
+ Assert.Equal("A", kv.Name);
+ Assert.Equal(1, kv.GetInt("x"));
+
+ KeyValues? b = kv.GetNextKey();
+ Assert.NotNull(b);
+ Assert.Equal("B", b.Name);
+ Assert.Equal(2, b.GetInt("y"));
+ Assert.Null(b.GetNextKey());
+ }
+
+ [Theory]
+ [InlineData("")]
+ [InlineData(" \r\n\t\n")]
+ [InlineData("// only a comment\n")]
+ public void NothingToParseFails(string text) {
+ KeyValues kv = new();
+
+ Assert.False(kv.LoadFromBuffer("test", text));
+ }
+
+ [Fact]
+ public void NullStreamFails() {
+ Assert.False(new KeyValues().LoadFromStream(null));
+ }
+
+ [Fact]
+ public void StreamWithUtf8ByteOrderMark() {
+ byte[] bytes = [.. Encoding.UTF8.GetPreamble(), .. Encoding.UTF8.GetBytes("\"Root\"\n{\n\t\"name\"\t\"caf\u00e9\"\n}\n")];
+ KeyValues kv = new();
+
+ Assert.True(kv.LoadFromStream(new MemoryStream(bytes)));
+ Assert.Equal("Root", kv.Name);
+ Assert.Equal("caf\u00e9", kv.GetString("name").ToString());
+ }
+
+ [Fact]
+ public void SoundScriptShapedFile() {
+ KeyValues kv = Parse("""
+ // Channels
+ // CHAN_AUTO = 0,
+
+ "Weapon_Pistol.Single"
+ {
+ "channel" "CHAN_WEAPON"
+ "volume" "0.55"
+ "soundlevel" "SNDLVL_GUNFIRE"
+ "pitch" "98,102"
+
+ "rndwave"
+ {
+ "wave" ")weapons/pistol/pistol_fire2.wav"
+ "wave" ")weapons/pistol/pistol_fire3.wav"
+ }
+ }
+
+ "Weapon_Pistol.Empty"
+ {
+ "channel" "CHAN_ITEM"
+ "volume" "0.8"
+ "soundlevel" "SNDLVL_NORM"
+ "pitch" "PITCH_NORM"
+ "wave" "weapons/pistol/pistol_empty.wav"
+ }
+ """);
+
+ Assert.Equal("Weapon_Pistol.Single", kv.Name);
+ Assert.Equal("CHAN_WEAPON", kv.GetString("channel").ToString());
+ Assert.Equal(0.55f, kv.GetFloat("volume"));
+ Assert.Equal("98,102", kv.GetString("pitch").ToString());
+
+ KeyValues rndwave = kv.FindKey("rndwave")!;
+ Assert.Equal([")weapons/pistol/pistol_fire2.wav", ")weapons/pistol/pistol_fire3.wav"], rndwave.Select(x => x.GetString().ToString()).ToArray());
+
+ KeyValues empty = kv.GetNextKey()!;
+ Assert.Equal("Weapon_Pistol.Empty", empty.Name);
+ Assert.Equal("weapons/pistol/pistol_empty.wav", empty.GetString("wave").ToString());
+ Assert.Null(empty.GetNextKey());
+ }
+
+ [Fact]
+ public void MissingKeysReturnDefaults() {
+ KeyValues kv = Parse("\"Root\"\n{\n\t\"a\"\t\"1\"\n}\n");
+
+ Assert.Equal("fallback", kv.GetString("missing", "fallback").ToString());
+ Assert.True(kv.GetString("missing").IsEmpty);
+ Assert.Equal(7, kv.GetInt("missing", 7));
+ Assert.Equal(0, kv.GetInt("missing"));
+ Assert.Equal(1.5f, kv.GetFloat("missing", 1.5f));
+ Assert.Equal(2.5, kv.GetDouble("missing", 2.5));
+ Assert.True(kv.GetBool("missing", true));
+ Assert.False(kv.GetBool("missing", false));
+ Assert.Equal(9ul, kv.GetUint64("missing", 9ul));
+ Assert.Equal(new Color(1, 2, 3, 4), kv.GetColor("missing", new Color(1, 2, 3, 4)));
+ Assert.Null(kv.GetPtr("missing"));
+ }
+
+ [Fact]
+ public void NumericConversionsByKey() {
+ KeyValues kv = Parse("\"Root\"\n{\n\t\"i\"\t\"5\"\n\t\"f\"\t\"2.75\"\n\t\"s\"\t\"text\"\n\t\"zero\"\t\"0\"\n}\n");
+
+ Assert.Equal(5, kv.GetInt("i"));
+ Assert.Equal(5f, kv.GetFloat("i"));
+ Assert.Equal(5.0, kv.GetDouble("i"));
+ Assert.True(kv.GetBool("i", false));
+
+ Assert.Equal(2, kv.GetInt("f"));
+ Assert.Equal(2.75f, kv.GetFloat("f"));
+ Assert.Equal(2.75, kv.GetDouble("f"));
+
+ Assert.Equal(3, kv.GetInt("s", 3));
+ Assert.Equal(3f, kv.GetFloat("s", 3f));
+ Assert.False(kv.GetBool("zero", true));
+ }
+
+ [Fact]
+ public void GetStringFormatsNumbers() {
+ KeyValues kv = Parse("\"Root\"\n{\n\t\"i\"\t\"5\"\n\t\"f\"\t\"1.5\"\n}\n");
+
+ Assert.Equal("5", kv.GetString("i").ToString());
+ Assert.Equal("1.500000", kv.GetString("f").ToString());
+ }
+
+ [Fact]
+ public void GetColorParsesComponents() {
+ KeyValues kv = Parse("\"Root\"\n{\n\t\"c\"\t\"255 128 0 64\"\n}\n");
+
+ Assert.Equal(new Color(255, 128, 0, 64), kv.GetColor("c"));
+ }
+
+ [Fact]
+ public void SettersCreateAndOverwriteKeys() {
+ KeyValues kv = new("Root");
+
+ kv.SetString("s", "one");
+ kv.SetInt("i", 3);
+ kv.SetFloat("f", 0.5f);
+ kv.SetUint64("u", 0x1_0000_0000ul);
+ kv.SetColor("c", new Color(1, 2, 3, 4));
+ kv.SetString("s", "two");
+ kv.SetInt("I", 4);
+
+ Assert.Equal(["s", "i", "f", "u", "c"], ChildNames(kv));
+ Assert.Equal("two", kv.GetString("s").ToString());
+ Assert.Equal(4, kv.GetInt("i"));
+ Assert.Equal(0.5f, kv.GetFloat("f"));
+ Assert.Equal(0x1_0000_0000ul, kv.GetUint64("u"));
+ Assert.Equal(new Color(1, 2, 3, 4), kv.GetColor("c"));
+ }
+
+ [Fact]
+ public void FindKeyCanCreate() {
+ KeyValues kv = new("Root");
+
+ Assert.Null(kv.FindKey("new"));
+ KeyValues created = kv.FindKey("new", create: true)!;
+
+ Assert.Equal("new", created.Name);
+ Assert.Same(created, kv.FindKey("NEW"));
+ Assert.Equal(1, kv.Count);
+ }
+
+ [Fact]
+ public void SubKeyIterationHelpers() {
+ KeyValues kv = Parse("""
+ "Root"
+ {
+ "v1" "1"
+ "S1"
+ {
+ "a" "1"
+ }
+ "v2" "2"
+ "S2"
+ {
+ "b" "2"
+ }
+ }
+ """);
+
+ List all = [];
+ for (KeyValues? k = kv.GetFirstSubKey(); k != null; k = k.GetNextKey())
+ all.Add(k.Name);
+ Assert.Equal(["v1", "S1", "v2", "S2"], all);
+
+ List sections = [];
+ for (KeyValues? k = kv.GetFirstTrueSubKey(); k != null; k = k.GetNextTrueSubKey())
+ sections.Add(k.Name);
+ Assert.Equal(["S1", "S2"], sections);
+
+ List values = [];
+ for (KeyValues? k = kv.GetFirstValue(); k != null; k = k.GetNextValue())
+ values.Add(k.Name);
+ Assert.Equal(["v1", "v2"], values);
+ }
+
+ [Fact]
+ public void IsEmptyReportsMissingAndEmptySections() {
+ KeyValues kv = Parse("\"Root\"\n{\n\t\"Empty\"\n\t{\n\t}\n\t\"Full\"\n\t{\n\t\t\"a\"\t\"1\"\n\t}\n\t\"value\"\t\"1\"\n}\n");
+
+ Assert.True(kv.IsEmpty("missing"));
+ Assert.True(kv.IsEmpty("Empty"));
+ Assert.False(kv.IsEmpty("Full"));
+ Assert.False(kv.IsEmpty("value"));
+ }
+
+ [Fact]
+ public void MakeCopyIsDeep() {
+ KeyValues kv = Parse("\"Root\"\n{\n\t\"a\"\t\"1\"\n\t\"Sub\"\n\t{\n\t\t\"b\"\t\"text\"\n\t}\n}\n");
+
+ KeyValues copy = kv.MakeCopy();
+ copy.SetInt("a", 99);
+ copy.FindKey("Sub")!.SetString("b", "changed");
+ copy.SetString("added", "x");
+
+ Assert.Equal("Root", copy.Name);
+ Assert.Equal(1, kv.GetInt("a"));
+ Assert.Equal("text", kv.FindKey("Sub")!.GetString("b").ToString());
+ Assert.Null(kv.FindKey("added"));
+ Assert.Equal(99, copy.GetInt("a"));
+ Assert.Equal("changed", copy.FindKey("Sub")!.GetString("b").ToString());
+ }
+
+ [Fact]
+ public void RemoveSubKey() {
+ KeyValues kv = Parse("\"Root\"\n{\n\t\"a\"\t\"1\"\n\t\"b\"\t\"2\"\n\t\"c\"\t\"3\"\n}\n");
+
+ kv.RemoveSubKey(kv.FindKey("b"));
+ Assert.Equal(["a", "c"], ChildNames(kv));
+
+ kv.RemoveSubKey(kv.FindKey("a"));
+ Assert.Equal(["c"], ChildNames(kv));
+
+ kv.RemoveSubKey(null);
+ Assert.Equal(["c"], ChildNames(kv));
+ }
+
+ [Fact]
+ public void CreateNewKeyUsesNextNumber() {
+ KeyValues kv = new("Root");
+
+ Assert.Equal("1", kv.CreateNewKey().Name);
+ Assert.Equal("2", kv.CreateNewKey().Name);
+ kv.CreateKey("10");
+ Assert.Equal("11", kv.CreateNewKey().Name);
+ Assert.Equal(4, kv.Count);
+ }
+
+ [Fact]
+ public void ChainedKeyValuesAreSearchedAfterOwnKeys() {
+ KeyValues baseKv = Parse("\"Base\"\n{\n\t\"shared\"\t\"base\"\n\t\"onlybase\"\t\"1\"\n}\n");
+ KeyValues kv = Parse("\"Derived\"\n{\n\t\"shared\"\t\"derived\"\n}\n");
+ kv.ChainKeyValue(baseKv);
+
+ Assert.Equal("derived", kv.GetString("shared").ToString());
+ Assert.Equal(1, kv.GetInt("onlybase"));
+ Assert.Null(kv.FindKey("nowhere"));
+ }
+
+ [Fact]
+ public void DirectValueSetters() {
+ KeyValues kv = new("k");
+
+ kv.SetStringValue("text");
+ Assert.Equal(KeyValues.Types.String, kv.Type);
+ Assert.Equal("text", kv.GetString().ToString());
+
+ kv.SetInt(12);
+ Assert.Equal(KeyValues.Types.Int, kv.Type);
+ Assert.Equal(12, kv.GetInt());
+ Assert.True(kv.GetBool());
+
+ kv.SetName("renamed");
+ Assert.Equal("renamed", kv.Name);
+ }
+}
diff --git a/Source.Common/Engine/IModelRender.cs b/Source.Common/Engine/IModelRender.cs
index b57acfdd4..d660efcd6 100644
--- a/Source.Common/Engine/IModelRender.cs
+++ b/Source.Common/Engine/IModelRender.cs
@@ -51,12 +51,23 @@ public struct ModelRenderInfo
public ModelInstanceHandle_t Instance;
}
+public struct StaticPropRenderInfo
+{
+ public Matrix3x4 ModelToWorld;
+ public Model? Model;
+ public IClientRenderable? Renderable;
+ public Vector3 LightingOrigin;
+ public short Skin;
+ public ModelInstanceHandle_t Instance;
+}
+
public interface IModelRender
{
ModelInstanceHandle_t CreateInstance(IClientRenderable renderable, LightCacheHandle_t? cache = null);
void DestroyInstance(ModelInstanceHandle_t modelInstance);
int DrawModel(StudioFlags flags, IClientRenderable? renderable, ModelInstanceHandle_t instance, int entityIndex, Model? model, in Vector3 origin, in QAngle angles, int skin, int body, int hitboxset, Matrix3x4? modelToWorld = null, Matrix3x4? lightingOffset = null);
int DrawModelEx(ref ModelRenderInfo info);
+ int DrawStaticPropArrayFast(Span props, int count, bool shadowDepth);
void SetStaticLighting(ModelInstanceHandle_t handle, LightCacheHandle_t? cache);
void SetViewTarget(StudioHdr studioHdr, int bodyIndex, in Vector3 target);
void DrawModelExecute(ref DrawModelState state, ref ModelRenderInfo info, Span boneToWorldArray);
diff --git a/Source.Common/Formats/Keyvalues/KeyValues.cs b/Source.Common/Formats/Keyvalues/KeyValues.cs
index bcee945be..cc1282ac6 100644
--- a/Source.Common/Formats/Keyvalues/KeyValues.cs
+++ b/Source.Common/Formats/Keyvalues/KeyValues.cs
@@ -102,26 +102,53 @@ public bool LoadFromStream(Stream? stream) {
// Clear();
if (stream == null) return false;
- using KeyValuesReader reader = new KeyValuesReader(stream);
- return LoadFromBuffer(reader);
+ // Size the buffer to the stream when we can, so the text is decoded in one piece.
+ int bufferSize = stream.CanSeek ? (int)Math.Clamp(stream.Length - stream.Position, 1024, 1 << 20) : 1024;
+ using StreamReader streamReader = new(stream, System.Text.Encoding.UTF8, true, bufferSize);
+ return LoadFromBuffer(new KeyValuesReader(streamReader.ReadToEnd()));
}
- public sealed class KeyValuesReader(Stream stream) : StreamReader(stream)
+ public sealed class KeyValuesReader(string text)
{
- int Pushback = -1;
+ readonly string text = text;
+ int position;
- public void Unread(char c) => Pushback = c;
+ public ReadOnlySpan Remaining => text.AsSpan(position);
- public override int Peek() => Pushback != -1 ? Pushback : base.Peek();
+ public void Advance(int count) => position += count;
- public override int Read() {
- if (Pushback != -1) {
- int c = Pushback;
- Pushback = -1;
- return c;
+ public string ReadString(int count) {
+ string str = text.Substring(position, count);
+ position += count;
+ return str;
+ }
+
+ public void Unread(char c) {
+ Debug.Assert(position > 0 && text[position - 1] == c, "can only unread the character that was just read");
+ position--;
+ }
+
+ public unsafe bool SkipWhitespace() {
+ int start = position;
+ fixed (char* chars = text) {
+ int i = position, length = text.Length;
+ while (i < length) {
+ char c = chars[i];
+ if (c != ' ' && (c < '\t' || c > '\r') && (c < 0x80 || !char.IsWhiteSpace(c)))
+ break;
+ i++;
+ }
+ position = i;
}
- return base.Read();
+
+ return position != start;
}
+
+ public int Position => position;
+
+ public int Peek() => position < text.Length ? text[position] : -1;
+
+ public int Read() => position < text.Length ? text[position++] : -1;
}
public bool WriteToStream(Stream? stream) {
@@ -199,18 +226,7 @@ private bool LoadFromBuffer(KeyValuesReader reader) {
// Returns true if we did anything at all to skip whitespace.
public static bool SkipWhitespace(KeyValuesReader reader) {
- bool didAnything = false;
- while (true) {
- int c = reader.Peek();
- if (c == -1)
- break;
- if (!char.IsWhiteSpace((char)c))
- break;
- reader.Read();
- didAnything = true;
- }
-
- return didAnything;
+ return reader.SkipWhitespace();
}
public override string ToString() {
@@ -224,8 +240,12 @@ private bool SkipUntilParseableTextOrEOF(KeyValuesReader reader) {
// 2. Anything else, we read until whitespace
while (true) {
- if (reader.Peek() == -1)
+ int c = reader.Peek();
+ if (c == -1)
return false;
+ // Most calls land directly on a token: no whitespace or comment can start with these characters.
+ if (c > ' ' && c < 0x7F && c != '/')
+ return true;
// If no whitespace was skipped and no comments were skipped, continue
if (!SkipWhitespace(reader) && !SkipComments(reader))
return true;
@@ -281,15 +301,14 @@ public static bool EvaluateConditional(Span str) {
return false;
}
public static bool ReadConditional(KeyValuesReader reader, Span condition, out bool match) {
- // Zero out if it's existing memory
- for (int si = 0; si < condition.Length; si++)
- condition[si] = '\0';
-
if (reader.Peek() != '[') {
match = false;
return false;
}
+ // Zero out if it's existing memory
+ condition.Clear();
+
reader.Read();
// Determine if we're inverted.
char c = (char)reader.Read();
@@ -356,9 +375,8 @@ public static bool HandleConditional(ReadOnlySpan condition, bool supporte
return notSupported;
}
+ // The reader must already be positioned on the key; both callers skip to parseable text first.
private bool ReadKV(KeyValuesReader reader) {
- SkipUntilParseableTextOrEOF(reader);
-
bool quoteTerminated = (char)reader.Peek() == '"';
string key = quoteTerminated ? ReadQuoteTerminatedString(reader, useEscapeSequences) : ReadWhitespaceTerminatedString(reader);
@@ -367,10 +385,13 @@ private bool ReadKV(KeyValuesReader reader) {
SkipUntilParseableTextOrEOF(reader);
Span conditional = stackalloc char[16];
+ int beforeConditional = reader.Position;
bool isBlockConditional = ReadConditional(reader, conditional, out bool mustMatch);
bool matches = isBlockConditional ? HandleConditional(conditional, mustMatch) : true;
- SkipUntilParseableTextOrEOF(reader);
+ // Nothing was consumed unless there was a conditional, so there is nothing new to skip.
+ if (reader.Position != beforeConditional)
+ SkipUntilParseableTextOrEOF(reader);
// Determine what we're reading next.
// If we run into a {, we read another KVObject and set our value to that.
@@ -433,25 +454,16 @@ private void ReadKVPairs(KeyValuesReader reader, bool matches) {
// Returns true if we did anything at all to skip comments.
public static bool SkipComments(KeyValuesReader reader) {
- bool didAnything = false;
- if (reader.Peek() == '/') {
- // We need to check the stream for another /
- reader.Read();
- if (reader.Peek() == '/') { // We got //, its a comment
- // We read until the end of the line.
- didAnything = true;
- int val;
- while ((val = reader.Read()) != -1) {
- char c = (char)val;
- if (c == '\n')
- break;
- }
- }
- else
- reader.Unread('/');
- }
+ ReadOnlySpan text = reader.Remaining;
+ // We need to check the stream for another /
+ if (text.Length < 2 || text[0] != '/' || text[1] != '/')
+ return false;
- return didAnything;
+ // We got //, its a comment
+ // We read until the end of the line.
+ int newline = text.IndexOf('\n');
+ reader.Advance(newline == -1 ? text.Length : newline + 1);
+ return true;
}
private void DetermineValueType(string input) {
@@ -516,29 +528,39 @@ static bool IsIntegerSyntax(ReadOnlySpan input) {
}
public static string ReadWhitespaceTerminatedString(KeyValuesReader reader) {
- Span work = stackalloc char[1024];
- int i, len;
- for (i = 0, len = work.Length; i < len; i++) {
- int c = reader.Peek();
- if (c == -1) break;
- char ch = (char)c;
- if (char.IsWhiteSpace(ch)) break;
- work[i] = ch;
- reader.Read();
- }
+ ReadOnlySpan text = reader.Remaining;
+ int len = Math.Min(text.Length, MAX_TOKEN_LENGTH);
+ int i = 0;
+ while (i < len && !char.IsWhiteSpace(text[i]))
+ i++;
- if (i >= len) {
+ if (i >= MAX_TOKEN_LENGTH) {
Warning("KeyValues: string overflow, ignoring (should we allocate more space?)\n");
AssertMsg(false, "KeyValues string overflow");
}
- return new(work[..i]);
+ return reader.ReadString(i);
}
+ const int MAX_TOKEN_LENGTH = 1024;
+
public static string ReadQuoteTerminatedString(KeyValuesReader reader, bool useEscapeSequences) {
int rd = reader.Read();
Debug.Assert(rd == '"', "invalid quote-terminated string");
- Span work = stackalloc char[1024];
+
+ // Nearly every string closes within the token limit and has nothing to unescape
+ ReadOnlySpan text = reader.Remaining;
+ if (text.Length > MAX_TOKEN_LENGTH)
+ text = text[..MAX_TOKEN_LENGTH];
+
+ int end = useEscapeSequences ? text.IndexOfAny('"', '\\') : text.IndexOf('"');
+ if (end != -1 && text[end] == '"') {
+ string str = reader.ReadString(end);
+ reader.Advance(1);
+ return str;
+ }
+
+ Span work = stackalloc char[MAX_TOKEN_LENGTH];
int i, len;
bool lastCharacterWasEscape = false;
int charI = 0;
@@ -574,7 +596,7 @@ public static string ReadQuoteTerminatedString(KeyValuesReader reader, bool useE
AssertMsg(false, "KeyValues string overflow");
}
- return new(work[..i]);
+ return new(work[..charI]);
}
public bool LoadFromFile(string? filepath) {
@@ -646,7 +668,7 @@ public KeyValues CreateNewKey() {
Span buf = stackalloc char[12];
sprintf(buf, $"{newID}");
- return CreateKey(buf);
+ return CreateKey(((ReadOnlySpan)buf).SliceNullTerminatedString());
}
public KeyValues CreateKey(ReadOnlySpan keyName) {
diff --git a/Source.Common/IStudioRender.cs b/Source.Common/IStudioRender.cs
index a01e3cf5e..d7fd8d065 100644
--- a/Source.Common/IStudioRender.cs
+++ b/Source.Common/IStudioRender.cs
@@ -108,6 +108,7 @@ public interface IStudioRender {
void LockFlexWeights(int weightCount, out Span flexWeights, out Span flexDelayedWeights);
void UnlockFlexWeights();
void DrawModel(ref DrawModelResults results, ref DrawModelInfo info, Span boneToWorld, Span flexWeights, Span flexDelayedWeights, in Vector3 modelOrigin, StudioRenderFlags flags = StudioRenderFlags.DrawEntireModel);
+ void DrawModelStaticProp(ref DrawModelInfo drawInfo, in Matrix3x4 modelToWorld, StudioRenderFlags flags = StudioRenderFlags.DrawEntireModel);
void SetViewState(in Vector3 currentViewOrigin, in Vector3 currentViewRight, in Vector3 currentViewUp, in Vector3 currentViewForward);
void SetColorModulation(Vector3 r_colormod);
void SetAlphaModulation(float r_blend);
diff --git a/Source.Common/SoundEmitterSystem/ISoundEmitterSystemBase.cs b/Source.Common/SoundEmitterSystem/ISoundEmitterSystemBase.cs
index 2147b76a8..1f80b0be7 100644
--- a/Source.Common/SoundEmitterSystem/ISoundEmitterSystemBase.cs
+++ b/Source.Common/SoundEmitterSystem/ISoundEmitterSystemBase.cs
@@ -5,7 +5,6 @@
using Source.Common.Utilities;
using System;
-using System.Collections.Frozen;
using System.Collections.Generic;
using System.Reflection.Emit;
using System.Numerics;
@@ -137,141 +136,197 @@ public bool Equals(SoundParametersInternal other) {
}
- // NOTE: Needs to reflect the soundlevel_t enum defined in soundflags.h
- static readonly FrozenDictionary g_pSoundLevels = (new Dictionary {
- { Audio.SoundLevel.LvlNone, "SNDLVL_NONE" },
- { Audio.SoundLevel.Lvl20dB, "SNDLVL_20dB" },
- { Audio.SoundLevel.Lvl25dB, "SNDLVL_25dB" },
- { Audio.SoundLevel.Lvl30dB, "SNDLVL_30dB" },
- { Audio.SoundLevel.Lvl35dB, "SNDLVL_35dB" },
- { Audio.SoundLevel.Lvl40dB, "SNDLVL_40dB" },
- { Audio.SoundLevel.Lvl45dB, "SNDLVL_45dB" },
- { Audio.SoundLevel.Lvl50dB, "SNDLVL_50dB" },
- { Audio.SoundLevel.Lvl55dB, "SNDLVL_55dB" },
- { Audio.SoundLevel.Lvl60dB, "SNDLVL_60dB" },
- { Audio.SoundLevel.Lvl65dB, "SNDLVL_65dB" },
- { Audio.SoundLevel.Lvl70dB, "SNDLVL_70dB" } ,
- { Audio.SoundLevel.Lvl75dB, "SNDLVL_75dB" } ,
- { Audio.SoundLevel.Lvl80dB, "SNDLVL_80dB" } ,
- { Audio.SoundLevel.Lvl85dB, "SNDLVL_85dB" } ,
- { Audio.SoundLevel.Lvl90dB, "SNDLVL_90dB" } ,
- { Audio.SoundLevel.Lvl95dB, "SNDLVL_95dB" } ,
- { Audio.SoundLevel.Lvl100dB, "SNDLVL_100dB" },
- { Audio.SoundLevel.Lvl105dB, "SNDLVL_105dB" },
- { Audio.SoundLevel.Lvl110dB, "SNDLVL_110dB" },
- { Audio.SoundLevel.Lvl120dB, "SNDLVL_120dB" },
- { Audio.SoundLevel.Lvl130dB, "SNDLVL_130dB" },
- { Audio.SoundLevel.Lvl140dB, "SNDLVL_140dB" },
- { Audio.SoundLevel.Lvl150dB, "SNDLVL_150dB" },
- { Audio.SoundLevel.Lvl180dB, "SNDLVL_180dB" },
- }).ToFrozenDictionary();
-
- static readonly FrozenDictionary g_pSoundLevelsRev = (new Dictionary {
- { "SNDLVL_NONE".Hash(invariant: true) , Audio.SoundLevel.LvlNone},
- { "SNDLVL_20dB".Hash(invariant: true) , Audio.SoundLevel.Lvl20dB},
- { "SNDLVL_25dB".Hash(invariant: true) , Audio.SoundLevel.Lvl25dB},
- { "SNDLVL_30dB".Hash(invariant: true) , Audio.SoundLevel.Lvl30dB},
- { "SNDLVL_35dB".Hash(invariant: true) , Audio.SoundLevel.Lvl35dB},
- { "SNDLVL_40dB".Hash(invariant: true) , Audio.SoundLevel.Lvl40dB},
- { "SNDLVL_45dB".Hash(invariant: true) , Audio.SoundLevel.Lvl45dB},
- { "SNDLVL_50dB".Hash(invariant: true) , Audio.SoundLevel.Lvl50dB},
- { "SNDLVL_55dB".Hash(invariant: true) , Audio.SoundLevel.Lvl55dB},
- { "SNDLVL_IDLE".Hash(invariant: true) , Audio.SoundLevel.LvlIdle},
- { "SNDLVL_TALKING".Hash(invariant: true) , Audio.SoundLevel.LvlTalking},
- { "SNDLVL_60dB".Hash(invariant: true) , Audio.SoundLevel.Lvl60dB},
- { "SNDLVL_65dB".Hash(invariant: true) , Audio.SoundLevel.Lvl65dB},
- { "SNDLVL_STATIC" .Hash(invariant: true) , Audio.SoundLevel.LvlStatic},
- { "SNDLVL_70dB" .Hash(invariant: true) , Audio.SoundLevel.Lvl70dB} ,
- { "SNDLVL_NORM" .Hash(invariant: true) , Audio.SoundLevel.LvlNorm} ,
- { "SNDLVL_75dB" .Hash(invariant: true) , Audio.SoundLevel.Lvl75dB} ,
- { "SNDLVL_80dB" .Hash(invariant: true) , Audio.SoundLevel.Lvl80dB} ,
- { "SNDLVL_85dB" .Hash(invariant: true) , Audio.SoundLevel.Lvl85dB} ,
- { "SNDLVL_90dB" .Hash(invariant: true) , Audio.SoundLevel.Lvl90dB} ,
- { "SNDLVL_95dB" .Hash(invariant: true) , Audio.SoundLevel.Lvl95dB} ,
- { "SNDLVL_100dB" .Hash(invariant: true) , Audio.SoundLevel.Lvl100dB},
- { "SNDLVL_105dB" .Hash(invariant: true) , Audio.SoundLevel.Lvl105dB},
- { "SNDLVL_110dB" .Hash(invariant: true) , Audio.SoundLevel.Lvl110dB},
- { "SNDLVL_120dB" .Hash(invariant: true) , Audio.SoundLevel.Lvl120dB},
- { "SNDLVL_130dB" .Hash(invariant: true) , Audio.SoundLevel.Lvl130dB},
- { "SNDLVL_GUNFIRE".Hash(invariant: true) , Audio.SoundLevel.LvlGunfire},
- { "SNDLVL_140dB".Hash(invariant: true) , Audio.SoundLevel.Lvl140dB},
- { "SNDLVL_150dB".Hash(invariant: true) , Audio.SoundLevel.Lvl150dB},
- { "SNDLVL_180dB" .Hash(invariant: true) , Audio.SoundLevel.Lvl180dB},
- }).ToFrozenDictionary();
-
- static readonly FrozenDictionary g_pChannelNames = (new Dictionary {
- { SoundEntityChannel.Auto, "CHAN_AUTO" },
- { SoundEntityChannel.Weapon, "CHAN_WEAPON" },
- { SoundEntityChannel.Voice, "CHAN_VOICE" },
- { SoundEntityChannel.Item, "CHAN_ITEM" },
- { SoundEntityChannel.Body, "CHAN_BODY" },
- { SoundEntityChannel.Stream, "CHAN_STREAM" },
- { SoundEntityChannel.Static, "CHAN_STATIC" },
- { SoundEntityChannel.Voice2, "CHAN_VOICE2" },
- }).ToFrozenDictionary();
-
- static readonly FrozenDictionary g_pChannelNamesRev =
- g_pChannelNames
- .Select(x => new KeyValuePair(x.Value.Hash(invariant: true), x.Key))
- .ToFrozenDictionary();
-
- static readonly FrozenDictionary g_pVolumeLevels = (new Dictionary {
- { VOL_NORM, "VOL_NORM" },
- }).ToFrozenDictionary();
-
- static readonly FrozenDictionary g_pVolumeLevelsRev =
- g_pVolumeLevels
- .Select(x => new KeyValuePair(x.Value.Hash(invariant: false), x.Key))
- .ToFrozenDictionary();
-
- static readonly FrozenDictionary g_pPitchLookup = (new Dictionary {
- { PITCH_NORM, "PITCH_NORM" },
- { PITCH_LOW, "PITCH_LOW" },
- { PITCH_HIGH, "PITCH_HIGH" },
- }).ToFrozenDictionary();
-
- static readonly FrozenDictionary g_pPitchLookupRev =
- g_pPitchLookup
- .Select(x => new KeyValuePair(x.Value.Hash(invariant: false), x.Key))
- .ToFrozenDictionary();
-
public static ReadOnlySpan _SoundLevelToString(SoundLevel level) {
- if (g_pSoundLevels.TryGetValue(level, out string? v))
- return v;
+ switch (level) {
+ case Audio.SoundLevel.LvlNone: return "SNDLVL_NONE";
+ case Audio.SoundLevel.Lvl20dB: return "SNDLVL_20dB";
+ case Audio.SoundLevel.Lvl25dB: return "SNDLVL_25dB";
+ case Audio.SoundLevel.Lvl30dB: return "SNDLVL_30dB";
+ case Audio.SoundLevel.Lvl35dB: return "SNDLVL_35dB";
+ case Audio.SoundLevel.Lvl40dB: return "SNDLVL_40dB";
+ case Audio.SoundLevel.Lvl45dB: return "SNDLVL_45dB";
+ case Audio.SoundLevel.Lvl50dB: return "SNDLVL_50dB";
+ case Audio.SoundLevel.Lvl55dB: return "SNDLVL_55dB";
+ case Audio.SoundLevel.Lvl60dB: return "SNDLVL_60dB";
+ case Audio.SoundLevel.Lvl65dB: return "SNDLVL_65dB";
+ case Audio.SoundLevel.Lvl70dB: return "SNDLVL_70dB";
+ case Audio.SoundLevel.Lvl75dB: return "SNDLVL_75dB";
+ case Audio.SoundLevel.Lvl80dB: return "SNDLVL_80dB";
+ case Audio.SoundLevel.Lvl85dB: return "SNDLVL_85dB";
+ case Audio.SoundLevel.Lvl90dB: return "SNDLVL_90dB";
+ case Audio.SoundLevel.Lvl95dB: return "SNDLVL_95dB";
+ case Audio.SoundLevel.Lvl100dB: return "SNDLVL_100dB";
+ case Audio.SoundLevel.Lvl105dB: return "SNDLVL_105dB";
+ case Audio.SoundLevel.Lvl110dB: return "SNDLVL_110dB";
+ case Audio.SoundLevel.Lvl120dB: return "SNDLVL_120dB";
+ case Audio.SoundLevel.Lvl130dB: return "SNDLVL_130dB";
+ case Audio.SoundLevel.Lvl140dB: return "SNDLVL_140dB";
+ case Audio.SoundLevel.Lvl150dB: return "SNDLVL_150dB";
+ case Audio.SoundLevel.Lvl180dB: return "SNDLVL_180dB";
+ }
return $"{level}";
}
+ static bool NamedSoundLevel(ReadOnlySpan name, out SoundLevel level) {
+ level = default;
+ if (name.IsEmpty)
+ return false;
+
+ switch (name[0] | 0x20) {
+ case 'n':
+ if (0 == stricmp(name, "NORM")) {
+ level = Audio.SoundLevel.LvlNorm;
+ return true;
+ }
+ if (0 == stricmp(name, "NONE")) {
+ level = Audio.SoundLevel.LvlNone;
+ return true;
+ }
+ break;
+ case 'i':
+ if (0 == stricmp(name, "IDLE")) {
+ level = Audio.SoundLevel.LvlIdle;
+ return true;
+ }
+ break;
+ case 't':
+ if (0 == stricmp(name, "TALKING")) {
+ level = Audio.SoundLevel.LvlTalking;
+ return true;
+ }
+ break;
+ case 's':
+ if (0 == stricmp(name, "STATIC")) {
+ level = Audio.SoundLevel.LvlStatic;
+ return true;
+ }
+ break;
+ case 'g':
+ if (0 == stricmp(name, "GUNFIRE")) {
+ level = Audio.SoundLevel.LvlGunfire;
+ return true;
+ }
+ break;
+ }
+
+ return false;
+ }
+
+ static string? ChannelName(SoundEntityChannel channel) {
+ switch (channel) {
+ case SoundEntityChannel.Auto: return "CHAN_AUTO";
+ case SoundEntityChannel.Weapon: return "CHAN_WEAPON";
+ case SoundEntityChannel.Voice: return "CHAN_VOICE";
+ case SoundEntityChannel.Item: return "CHAN_ITEM";
+ case SoundEntityChannel.Body: return "CHAN_BODY";
+ case SoundEntityChannel.Stream: return "CHAN_STREAM";
+ case SoundEntityChannel.Static: return "CHAN_STATIC";
+ case SoundEntityChannel.Voice2: return "CHAN_VOICE2";
+ }
+
+ return null;
+ }
+
+ static bool NamedChannel(ReadOnlySpan name, out SoundEntityChannel channel) {
+ channel = default;
+ if (name.IsEmpty)
+ return false;
+
+ switch (name[0] | 0x20) {
+ case 'a':
+ if (0 == stricmp(name, "AUTO")) {
+ channel = SoundEntityChannel.Auto;
+ return true;
+ }
+ break;
+ case 'w':
+ if (0 == stricmp(name, "WEAPON")) {
+ channel = SoundEntityChannel.Weapon;
+ return true;
+ }
+ break;
+ case 'v':
+ if (0 == stricmp(name, "VOICE")) {
+ channel = SoundEntityChannel.Voice;
+ return true;
+ }
+ if (0 == stricmp(name, "VOICE2")) {
+ channel = SoundEntityChannel.Voice2;
+ return true;
+ }
+ break;
+ case 'i':
+ if (0 == stricmp(name, "ITEM")) {
+ channel = SoundEntityChannel.Item;
+ return true;
+ }
+ break;
+ case 'b':
+ if (0 == stricmp(name, "BODY")) {
+ channel = SoundEntityChannel.Body;
+ return true;
+ }
+ break;
+ case 's':
+ if (0 == stricmp(name, "STREAM")) {
+ channel = SoundEntityChannel.Stream;
+ return true;
+ }
+ if (0 == stricmp(name, "STATIC")) {
+ channel = SoundEntityChannel.Static;
+ return true;
+ }
+ break;
+ }
+
+ return false;
+ }
+
public static ReadOnlySpan _ChannelToString(int channel) {
- if (g_pChannelNames.TryGetValue((SoundEntityChannel)channel, out string? v))
+ string? v = ChannelName((SoundEntityChannel)channel);
+ if (v != null)
return v;
return $"{channel}";
}
public static ReadOnlySpan _ChannelToString(SoundEntityChannel channel) {
- if (g_pChannelNames.TryGetValue(channel, out string? v))
+ string? v = ChannelName(channel);
+ if (v != null)
return v;
return $"{channel}";
}
public static ReadOnlySpan _VolumeToString(float volume) {
- if (g_pVolumeLevels.TryGetValue(volume, out string? v))
- return v;
+ if (volume == VOL_NORM)
+ return "VOL_NORM";
return $"{volume}";
}
+ static string? PitchName(byte pitch) {
+ switch (pitch) {
+ case PITCH_NORM: return "PITCH_NORM";
+ case PITCH_LOW: return "PITCH_LOW";
+ case PITCH_HIGH: return "PITCH_HIGH";
+ }
+
+ return null;
+ }
+
public static ReadOnlySpan _PitchToString(float pitch) {
- if (g_pPitchLookup.TryGetValue((byte)pitch, out string? v))
+ string? v = PitchName((byte)pitch);
+ if (v != null)
return v;
return $"{pitch}";
}
public static ReadOnlySpan _PitchToString(byte pitch) {
- if (g_pPitchLookup.TryGetValue(pitch, out string? v))
+ string? v = PitchName(pitch);
+ if (v != null)
return v;
return $"{pitch}";
@@ -315,7 +370,7 @@ public SoundEntityChannel TextToChannel(ReadOnlySpan name) {
if (strnicmp(name, "chan_", "chan_".Length) != 0)
return (SoundEntityChannel)atoi(name);
- if (g_pChannelNamesRev.TryGetValue(name.Hash(invariant: true), out SoundEntityChannel chan))
+ if (NamedChannel(name["chan_".Length..], out SoundEntityChannel chan))
return chan;
// At this point, it starts with chan_ but is not recognized
@@ -336,11 +391,11 @@ public static SoundLevel TextToSoundLevel(ReadOnlySpan key) {
return Audio.SoundLevel.LvlNorm;
}
- if (g_pSoundLevelsRev.TryGetValue(key.Hash(invariant: true), out SoundLevel level))
- return level;
-
if (0 == stricmp(key[..Math.Min((int)strlen(SNDLVL_PREFIX), key.Length)], SNDLVL_PREFIX)) {
ReadOnlySpan val = key[(int)strlen(SNDLVL_PREFIX)..];
+ if (NamedSoundLevel(val, out SoundLevel level))
+ return level;
+
int sndlvl = atoi(val);
if (sndlvl > 0 && sndlvl <= 180)
return (SoundLevel)sndlvl;
diff --git a/Source.Engine/EngineBuilder.cs b/Source.Engine/EngineBuilder.cs
index 4b33582f9..a10e45c0b 100644
--- a/Source.Engine/EngineBuilder.cs
+++ b/Source.Engine/EngineBuilder.cs
@@ -18,6 +18,8 @@
using System.Runtime.CompilerServices;
using System.Runtime.ExceptionServices;
+using CvarRegistrar = System.Action>;
+
namespace Source.Engine;
///
@@ -381,18 +383,105 @@ static IEnumerable safeTypeGet(Assembly assembly) {
public static void ConVar_Register() {
var engineAPI = Singleton();
ICvar cvar = engineAPI.GetRequiredService();
- var assemblies = AppDomain.CurrentDomain.GetAssemblies()
- .Where(ReflectionUtils.IsSourceEngineAssembly);
+ Assembly[] assemblies = [.. AppDomain.CurrentDomain.GetAssemblies().Where(ReflectionUtils.IsSourceEngineAssembly)];
+
+ // Source.CodeAnalysis.ConVars emits Source.Generated.CvarRegistration into each Source assembly. Types it lists
+ // are registered through generated code; anything else falls back to the reflection path below.
+ Dictionary declared = [];
+ Dictionary inherited = [];
+ HashSet reflected = [];
+ HashSet generated = [];
+ foreach (var assembly in assemblies) {
+ if (LoadGeneratedCvarRegistration(assembly, declared, inherited, reflected))
+ generated.Add(assembly);
+ }
- Type CVAR = typeof(ConVar);
- Type CCMD = typeof(ConCommand);
+ Func resolve = (type, concommand, name) => DetermineInstance(engineAPI, type, concommand, name)!;
+ List<(Type Declaring, CvarRegistrar Registrar)> registrars = [];
foreach (var assembly in assemblies) {
cvar.SetAssemblyIdentifier(assembly);
+ bool hasGenerated = generated.Contains(assembly);
foreach (var type in assembly.GetTypes()) {
if (type.IsAssignableTo(typeof(ConVar)) || type == typeof(ConVarRef))
continue;
+ if (!hasGenerated || !FindGeneratedCvarRegistrars(type, assemblies, generated, declared, inherited, reflected, registrars)) {
+ ConVar_RegisterByReflection(engineAPI, cvar, type);
+ continue;
+ }
+
+ RunStaticConstructor(type);
+
+ // Properties, then fields, then methods; the same order the reflection path uses.
+ for (int phase = 0; phase < 3 && registrars.Count > 0; phase++) {
+ foreach (var (declaring, registrar) in registrars)
+ registrar(phase, declaring, type, cvar, resolve);
+ }
+ }
+ }
+ }
+
+ static bool LoadGeneratedCvarRegistration(Assembly assembly, Dictionary declared, Dictionary inherited, HashSet reflected) {
+ MethodInfo? populate = assembly.GetType("Source.Generated.CvarRegistration")?.GetMethod("Populate", BindingFlags.Public | BindingFlags.Static);
+ if (populate == null)
+ return false;
+
+ Dictionary declaredNames = [];
+ Dictionary inheritedNames = [];
+ List reflectedNames = [];
+ populate.CreateDelegate, Dictionary, List>>()(declaredNames, inheritedNames, reflectedNames);
+
+ foreach (var (name, registrar) in declaredNames)
+ declared.Add(assembly.GetType(name, true)!, registrar);
+ foreach (var (name, registrar) in inheritedNames)
+ inherited.Add(assembly.GetType(name, true)!, registrar);
+ foreach (string name in reflectedNames)
+ reflected.Add(assembly.GetType(name, true)!);
+
+ return true;
+ }
+
+ ///
+ /// Collects the generated registrars that apply to : its own, then those of each base type
+ /// (reflection returns inherited non-private instance members, so derived types register them too).
+ /// Returns false if the type has to go through reflection instead.
+ ///
+ static bool FindGeneratedCvarRegistrars(Type type, Assembly[] sourceAssemblies, HashSet generated, Dictionary declared, Dictionary inherited, HashSet reflected, List<(Type Declaring, CvarRegistrar Registrar)> registrars) {
+ registrars.Clear();
+ if (reflected.Contains(type))
+ return false;
+
+ if (declared.TryGetValue(type, out CvarRegistrar? registrar))
+ registrars.Add((type, registrar));
+
+ for (Type? baseType = type.BaseType; baseType != null; baseType = baseType.BaseType) {
+ Type key = baseType.IsConstructedGenericType ? baseType.GetGenericTypeDefinition() : baseType;
+ if (reflected.Contains(key))
+ return false;
+ if (!generated.Contains(key.Assembly) && Array.IndexOf(sourceAssemblies, key.Assembly) != -1)
+ return false;
+ if (inherited.TryGetValue(key, out registrar))
+ registrars.Add((key, registrar));
+ }
+
+ return true;
+ }
+
+ static void RunStaticConstructor(Type type) {
+ try {
+ RuntimeHelpers.RunClassConstructor(type.TypeHandle);
+ }
+ catch (TypeInitializationException tie) when (tie.InnerException != null) {
+ ExceptionDispatchInfo.Capture(tie.InnerException).Throw();
+ throw;
+ }
+ }
+
+ static void ConVar_RegisterByReflection(IEngineAPI engineAPI, ICvar cvar, Type type) {
+ Type CVAR = typeof(ConVar);
+ {
+ {
var props = type.GetProperties(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static);
var fields = type.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static);
var methods = type.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static);
diff --git a/Source.Engine/ImplStudio.cs b/Source.Engine/ImplStudio.cs
index 335b60a75..e6e5a72ec 100644
--- a/Source.Engine/ImplStudio.cs
+++ b/Source.Engine/ImplStudio.cs
@@ -701,6 +701,289 @@ public void DrawModelExecute(ref DrawModelState state, ref ModelRenderInfo pInfo
#endif
}
+ struct RObject
+ {
+ public Matrix3x4 Matrix;
+ public IClientRenderable? Renderable;
+ public ColorMeshInfo[]? ColorMeshes;
+ public ITexture? EnvCubeMap;
+ public Vector3 LightingOrigin;
+ public short ModelIndex;
+ public short Lod;
+ public ModelInstanceHandle_t Instance;
+ public short Skin;
+ public short LightIndex;
+ }
+
+ struct RModel
+ {
+ public Model? Model;
+ public StudioHeader? StudioHdr;
+ public StudioHWData? StudioHWData;
+ public float MaxArea;
+ public short LodStart;
+ public byte LodCount;
+ public bool VertexLit;
+ public bool NeedsCubemap;
+ public bool StaticLighting;
+ }
+
+ int RObjectLess(RObject lhs, RObject rhs) {
+ Span models = CollectionsMarshal.AsSpan(ModelList);
+ if (lhs.ModelIndex == rhs.ModelIndex) {
+ if (lhs.Skin != rhs.Skin)
+ return lhs.Skin.CompareTo(rhs.Skin);
+ return lhs.Lod.CompareTo(rhs.Lod);
+ }
+ if (models[lhs.ModelIndex].MaxArea == models[rhs.ModelIndex].MaxArea)
+ return lhs.ModelIndex.CompareTo(rhs.ModelIndex);
+ return models[rhs.ModelIndex].MaxArea.CompareTo(models[lhs.ModelIndex].MaxArea);
+ }
+
+ static int FindModel(List list, Model? model) {
+ for (int j = list.Count; --j >= 0;) {
+ if (list[j].Model == model)
+ return j;
+ }
+ return -1;
+ }
+
+ readonly List ObjectList = new(1024);
+ readonly List ModelList = new(256);
+ readonly List LightObjects = new(256);
+ readonly List LightStates = new(256);
+ Comparison? ObjectLess;
+
+ static readonly ConVar r_staticprop_lod = new("r_staticprop_lod", "-1");
+ public int DrawStaticPropArrayFast(Span props, int count, bool shadowDepth) {
+#if !SWDS
+ using MatRenderContextPtr renderContext = new(materials);
+ ObjectList.Clear();
+ ModelList.Clear();
+ LightObjects.Clear();
+ LightStates.Clear();
+ bool forceCubemap = r_showenvcubemap.GetBool();
+ int drawnCount = 0;
+ int forcedLodSetting = r_lod.GetInt();
+ if (r_staticprop_lod.GetInt() >= 0)
+ forcedLodSetting = r_staticprop_lod.GetInt();
+
+ for (int i = 0; i < count; i++) {
+ drawnCount++;
+ int modelIndex = FindModel(ModelList, props[i].Model);
+ if (modelIndex < 0) {
+ modelIndex = ModelList.Count;
+ ModelList.Add(new RModel {
+ Model = props[i].Model,
+ StudioHWData = MDLCache.GetHardwareData(props[i].Model!.Studio)
+ });
+ }
+ if (ModelList[modelIndex].StudioHWData != null) {
+ RObject obj = default;
+ obj.Matrix = props[i].ModelToWorld;
+ obj.Renderable = props[i].Renderable;
+ obj.ModelIndex = (short)modelIndex;
+ obj.Instance = props[i].Instance;
+ obj.Skin = props[i].Skin;
+ obj.Lod = 0;
+ obj.ColorMeshes = null;
+ obj.EnvCubeMap = null;
+ obj.LightIndex = -1;
+ obj.LightingOrigin = props[i].LightingOrigin;
+ ObjectList.Add(obj);
+ }
+ }
+
+ Span modelList = CollectionsMarshal.AsSpan(ModelList);
+ Span objectList = CollectionsMarshal.AsSpan(ObjectList);
+
+ int lodStart = 0;
+ for (int i = 0; i < modelList.Length; i++) {
+ Model model = modelList[i].Model!;
+ ModelFlag flags = model.Flags;
+ modelList[i].StudioHdr = MDLCache.GetStudioHdr(model.Studio);
+ modelList[i].MaxArea = 1.0f;
+ modelList[i].LodStart = (short)lodStart;
+ modelList[i].LodCount = (byte)(modelList[i].StudioHWData != null ? modelList[i].StudioHWData!.NumLODs : 0);
+ bool bumpMapped = (flags & ModelFlag.UsesBumpMapping) != 0;
+ modelList[i].StaticLighting = ((modelList[i].StudioHdr!.Flags & StudioHdrFlags.StaticProp) != 0) && !bumpMapped;
+ modelList[i].VertexLit = (flags & ModelFlag.VertexLit) != 0;
+ modelList[i].NeedsCubemap = (flags & ModelFlag.UsesEnvCubemap) != 0;
+
+ lodStart += modelList[i].LodCount;
+ }
+
+ if (forcedLodSetting < 0) {
+ for (int i = 0; i < objectList.Length; i++) {
+ MathLib.MatrixGetColumn(in objectList[i].Matrix, 3, out Vector3 org);
+ float screenSize = renderContext.ComputePixelWidthOfSphere(org, 0.5f);
+ ref RModel model = ref modelList[objectList[i].ModelIndex];
+ float metric = model.StudioHWData!.LODMetric(screenSize);
+ objectList[i].Lod = (short)model.StudioHWData.GetLODForMetric(metric);
+ if (objectList[i].Lod < model.StudioHWData.RootLOD)
+ objectList[i].Lod = (short)model.StudioHWData.RootLOD;
+ model.MaxArea = Math.Max(model.MaxArea, screenSize);
+ }
+ }
+ else {
+ for (int i = 0; i < objectList.Length; i++) {
+ ref RModel model = ref modelList[objectList[i].ModelIndex];
+ objectList[i].Lod = (short)Math.Clamp(forcedLodSetting, model.StudioHWData!.RootLOD, model.LodCount - 1);
+ }
+ }
+ ObjectList.Sort(ObjectLess ??= RObjectLess);
+
+ if (!shadowDepth) {
+ for (int i = 0; i < objectList.Length; i++) {
+ ref RObject obj = ref objectList[i];
+ ref RModel model = ref modelList[obj.ModelIndex];
+ bool staticLighting = model.StaticLighting && obj.Instance != MODEL_INSTANCE_INVALID;
+ bool vertexLit = model.VertexLit;
+ bool needsEnvCubemap = forceCubemap || model.NeedsCubemap;
+ // todo: decal light states, shadow objects
+
+ DataCacheHandle_t colorMeshHandle = 0;
+ if (staticLighting) {
+ colorMeshHandle = ModelInstances[obj.Instance].ColorMeshHandle;
+ ColorMeshData? colorMeshData = CacheGet(colorMeshHandle);
+ if (colorMeshData == null || colorMeshData.NeedsRetry) {
+ if (UpdateStaticPropColorData(obj.Renderable!.GetIClientUnknown(), obj.Instance))
+ colorMeshData = CacheGet(colorMeshHandle);
+ else if (colorMeshData == null || !colorMeshData.NeedsRetry) {
+ continue;
+ }
+ }
+
+ if (colorMeshData != null && (colorMeshData.ColorMeshValid || colorMeshData.ColorTextureValid)) {
+ obj.ColorMeshes = colorMeshData.MeshInfos;
+ if (colorMeshData.ColorTextureValid && !colorMeshData.ColorTextureCreated)
+ CreateLightmapsFromData(colorMeshData);
+ }
+ else {
+ staticLighting = false;
+ }
+ }
+
+ if (vertexLit || needsEnvCubemap) {
+ ref LightCacheHandle_t lightCache = ref Unsafe.NullRef();
+ ITexture? envCubemapTexture = null;
+ if (obj.Instance != MODEL_INSTANCE_INVALID) {
+ if ((ModelInstances[obj.Instance].Flags & ModelInstanceFlags.HasStaticLighting) != 0 && ModelInstances[obj.Instance].LightCacheHandle != 0)
+ lightCache = ref ModelInstances[obj.Instance].LightCacheHandle;
+ }
+
+ Assert(!Unsafe.IsNullRef(ref lightCache));
+ LightingState lightingState = default;
+ ref LightingState state = ref lightingState;
+ if (!Unsafe.IsNullRef(ref lightCache)) {
+ if (!staticLighting || Render.StaticLightCacheAffectedByDynamicLight(lightCache)) {
+ state = ref Render.LightcacheGetStatic(lightCache, out envCubemapTexture, LightCacheFlags.Static | LightCacheFlags.Dynamic | LightCacheFlags.LightStyle);
+ Assert(state.NumLights >= 0 && state.NumLights <= Render.MAXLOCALLIGHTS);
+ }
+ else {
+ state = ref Render.LightcacheGetStatic(lightCache, out envCubemapTexture, LightCacheFlags.Dynamic | LightCacheFlags.LightStyle);
+ Assert(state.NumLights >= 0 && state.NumLights <= Render.MAXLOCALLIGHTS);
+ }
+ }
+ else {
+ Vector3 lightingOrigin = obj.LightingOrigin;
+ LightCacheFlags lightCacheFlags = staticLighting ? (LightCacheFlags.Dynamic | LightCacheFlags.LightStyle)
+ : (LightCacheFlags.Static | LightCacheFlags.Dynamic | LightCacheFlags.LightStyle | LightCacheFlags.AllowFast);
+ LightcacheGetDynamic_Stats stats = default;
+ envCubemapTexture = Render.LightcacheGetDynamic(lightingOrigin, ref lightingState,
+ ref stats, lightCacheFlags, false);
+ Assert(lightingState.NumLights >= 0 && lightingState.NumLights <= Render.MAXLOCALLIGHTS);
+ state = ref lightingState;
+ }
+
+ if (needsEnvCubemap && envCubemapTexture != null)
+ obj.EnvCubeMap = envCubemapTexture;
+
+ if (vertexLit) {
+ if (state.NumLights != 0 || state.HasAmbientColors()) {
+ obj.LightIndex = (short)LightStates.Count;
+ LightStates.Add(state);
+ LightObjects.Add((short)i);
+ }
+ }
+ }
+ }
+ }
+ StudioRender.SetColorModulation(new Vector3(1.0f, 1.0f, 1.0f));
+ StudioRender.SetAlphaModulation(1.0f);
+ StudioRender.SetViewState(in Render.CurrentViewOrigin, in Render.CurrentViewRight, in Render.CurrentViewUp, in Render.CurrentViewForward);
+
+ renderContext.MatrixMode(MaterialMatrixMode.Model);
+ renderContext.PushMatrix();
+ renderContext.LoadIdentity();
+ renderContext.DisableAllLocalLights();
+ DrawModelInfo info = default;
+ for (int i = 0; i < 6; i++)
+ info.AmbientCube[i].Init();
+ StudioRender.SetAmbientLightColors(info.AmbientCube);
+ renderContext.SetAmbientLight(0.0f, 0.0f, 0.0f);
+ info.NumLocalLights = 0;
+ info.StaticLighting = false;
+
+ StudioRenderFlags drawFlags = StudioRenderFlags.DrawEntireModel | StudioRenderFlags.DrawStaticLighting;
+ if (shadowDepth)
+ drawFlags |= StudioRenderFlags.ShadowDepthTexture;
+ info.Body = 0;
+ info.HitboxSet = 0;
+ for (int i = 0; i < objectList.Length; i++) {
+ ref RObject obj = ref objectList[i];
+ if (obj.LightIndex >= 0)
+ continue;
+ ref RModel model = ref modelList[obj.ModelIndex];
+ if (obj.EnvCubeMap != null)
+ renderContext.BindLocalCubemap(obj.EnvCubeMap);
+
+ info.StudioHdr = model.StudioHdr!;
+ info.HardwareData = model.StudioHWData!;
+ info.Skin = obj.Skin;
+ info.ClientEntity = obj.Renderable;
+ info.Lod = obj.Lod;
+ info.ColorMeshes = obj.ColorMeshes;
+ StudioRender.DrawModelStaticProp(ref info, in obj.Matrix, drawFlags);
+ }
+
+ int localLightCount = 0;
+ InlineArray4 localLightDescs = default;
+ drawFlags = StudioRenderFlags.DrawEntireModel | StudioRenderFlags.DrawStaticLighting;
+ if (LightObjects.Count != 0) {
+ Span lightStates = CollectionsMarshal.AsSpan(LightStates);
+ for (int i = 0; i < LightObjects.Count; i++) {
+ ref RObject obj = ref objectList[LightObjects[i]];
+ ref RModel model = ref modelList[obj.ModelIndex];
+
+ if (obj.EnvCubeMap != null)
+ renderContext.BindLocalCubemap(obj.EnvCubeMap);
+
+ ref LightingState state = ref lightStates[obj.LightIndex];
+ StudioRender.SetAmbientLightColors(state.BoxColor);
+ renderContext.SetLightingOrigin(obj.LightingOrigin);
+ R_SetNonAmbientLightingState(state.NumLights, state.LocalLight, out localLightCount, localLightDescs, true);
+ info.StudioHdr = model.StudioHdr!;
+ info.HardwareData = model.StudioHWData!;
+ info.Skin = obj.Skin;
+ info.ClientEntity = obj.Renderable;
+ info.Lod = obj.Lod;
+ info.ColorMeshes = obj.ColorMeshes;
+ StudioRender.DrawModelStaticProp(ref info, in obj.Matrix, drawFlags);
+ }
+ }
+
+ // todo: shadow pass (DrawStaticPropShadows)
+ // todo: decal pass (DrawStaticPropDecals)
+
+ renderContext.MatrixMode(MaterialMatrixMode.Model);
+ renderContext.PopMatrix();
+ return drawnCount;
+#else
+ return 0;
+#endif
+ }
+
bool EngineLightingSuppressed = false;
public void SuppressEngineLighting(bool suppress) => EngineLightingSuppressed = suppress;
@@ -715,7 +998,8 @@ private void StudioSetupLighting(DrawModelState state, in Vector3 absEntCenter,
#if !SWDS
ITexture? envCubemapTexture = null;
- LightingState lightingState = default;
+ LightingState lightingStateStorage = default;
+ ref LightingState lightingState = ref lightingStateStorage;
Span saveLightPos = stackalloc Vector3[Render.MAXLOCALLIGHTS];
Vector3? debugLightingOrigin = null;
@@ -747,9 +1031,9 @@ private void StudioSetupLighting(DrawModelState state, in Vector3 absEntCenter,
if (staticLighting) {
if (HardwareConfig.SupportsStaticPlusDynamicLighting()) {
if (Render.StaticLightCacheAffectedByDynamicLight(lightcache))
- lightingState = Render.LightcacheGetStatic(lightcache, out envCubemapTexture);
+ lightingState = ref Render.LightcacheGetStatic(lightcache, out envCubemapTexture);
else
- lightingState = Render.LightcacheGetStatic(lightcache, out envCubemapTexture, LightCacheFlags.Dynamic | LightCacheFlags.LightStyle);
+ lightingState = ref Render.LightcacheGetStatic(lightcache, out envCubemapTexture, LightCacheFlags.Dynamic | LightCacheFlags.LightStyle);
}
else {
if (Render.StaticLightCacheAffectedByDynamicLight(lightcache) ||
@@ -761,7 +1045,7 @@ private void StudioSetupLighting(DrawModelState state, in Vector3 absEntCenter,
}
if (!staticLighting)
- lightingState = Render.LightcacheGetStatic(lightcache, out envCubemapTexture);
+ lightingState = ref Render.LightcacheGetStatic(lightcache, out envCubemapTexture);
if (r_decalstaticprops.GetBool() && modelInst != null && drawInfo.StaticLighting && hasDecals) {
for (int iCube = 0; iCube < 6; ++iCube)
diff --git a/Source.Engine/Lightcache.cs b/Source.Engine/Lightcache.cs
index c207639d5..5bd9c4841 100644
--- a/Source.Engine/Lightcache.cs
+++ b/Source.Engine/Lightcache.cs
@@ -1246,7 +1246,8 @@ private ReadOnlySpan ComputeDynamicLighting(LightCache cache, scoped ref L
}
Assert(cache.DynamicLightingState.NumLights >= 0 && cache.DynamicLightingState.NumLights <= MAXLOCALLIGHTS);
- lightingState = cache.DynamicLightingState;
+ if (!Unsafe.AreSame(ref lightingState, ref cache.DynamicLightingState))
+ lightingState = cache.DynamicLightingState;
return vis;
}
diff --git a/Source.Engine/StaticPropMgr.cs b/Source.Engine/StaticPropMgr.cs
index af71fd46c..e2d4e8eb3 100644
--- a/Source.Engine/StaticPropMgr.cs
+++ b/Source.Engine/StaticPropMgr.cs
@@ -341,8 +341,17 @@ public void DrawStaticProps(ref InlineArray512 props, int cou
if (!r_drawstaticprops.GetBool())
return;
- // todo: fast pipeline
- DrawStaticProps_Slow(ref props, count, shadowDepth, drawVCollideWireframe);
+ if (IsUsingStaticPropDebugModes() || drawVCollideWireframe)
+ DrawStaticProps_Slow(ref props, count, shadowDepth, drawVCollideWireframe);
+ else {
+ if (pipeline_static_props.GetBool() &&
+ HardwareConfig.GetDXSupportLevel() >= 80 &&
+ HardwareConfig.SupportsColorOnSecondStream() &&
+ HardwareConfig.SupportsStaticPlusDynamicLighting())
+ DrawStaticProps_FastPipeline(props, count, shadowDepth);
+ else
+ DrawStaticProps_Fast(ref props, count, shadowDepth);
+ }
}
void DrawStaticProps_Slow(ref InlineArray512 props, int count, bool shadowDepth, bool drawVCollideWireframe) {
@@ -359,7 +368,32 @@ void DrawStaticProps_Slow(ref InlineArray512 props, int count
}
void DrawStaticProps_Fast(ref InlineArray512 props, int count, bool shadowDepth) => throw new NotImplementedException();
- void DrawStaticProps_FastPipeline(ref InlineArray512 props, int count, bool shadowDepth) => throw new NotImplementedException();
+
+ const int MAX_OBJECTS = 2048;
+ readonly StaticPropRenderInfo[] PropList = new StaticPropRenderInfo[MAX_OBJECTS];
+
+ void DrawStaticProps_FastPipeline(Span props, int count, bool shadowDepth) {
+ Span propList = PropList;
+ int listCount = 0;
+ if (count > MAX_OBJECTS)
+ DrawStaticProps_FastPipeline(props[MAX_OBJECTS..], count - MAX_OBJECTS, shadowDepth);
+
+ for (int i = 0; i < count; i++) {
+ StaticProp prop = (StaticProp)props[i];
+ if (prop.Model == null)
+ continue;
+ propList[listCount].ModelToWorld = prop.ModelToWorld;
+ propList[listCount].Model = prop.Model;
+ propList[listCount].Instance = prop.ModelInstance;
+ propList[listCount].Skin = prop.Skin;
+ propList[listCount].Renderable = prop;
+ propList[listCount].LightingOrigin = prop.LightingOrigin;
+ listCount++;
+ }
+ modelrender.DrawStaticPropArrayFast(propList, listCount, shadowDepth);
+ }
+
+ static readonly ConVar pipeline_static_props = new("pipeline_static_props", "1");
void OutputLevelStats() => throw new NotImplementedException();
void PrecacheLighting() { // FIXME: This is very slow...
@@ -593,12 +627,12 @@ public class StaticProp : IClientUnknown, IClientRenderable, ICollideable
Vector3 Origin;
QAngle Angles;
- Model? Model;
+ internal Model? Model;
SpatialPartitionHandle_t Partition;
- ModelInstanceHandle_t ModelInstance;
+ internal ModelInstanceHandle_t ModelInstance;
byte Alpha;
byte Solid;
- byte Skin;
+ internal byte Skin;
byte flags;
ushort firstLeaf;
ushort leafCount;
@@ -609,13 +643,13 @@ public class StaticProp : IClientUnknown, IClientRenderable, ICollideable
Vector3 RenderBBoxMin;
Vector3 RenderBBoxMax;
- Matrix3x4 ModelToWorld;
+ internal Matrix3x4 ModelToWorld;
float radius;
Vector3 WorldRenderBBoxMin;
Vector3 WorldRenderBBoxMax;
- Vector3 LightingOrigin;
+ internal Vector3 LightingOrigin;
public StaticProp() {
ModelInstance = MODEL_INSTANCE_INVALID;
diff --git a/Source.GUI.Controls/AnimationController.cs b/Source.GUI.Controls/AnimationController.cs
index bd84076b4..636779402 100644
--- a/Source.GUI.Controls/AnimationController.cs
+++ b/Source.GUI.Controls/AnimationController.cs
@@ -7,7 +7,6 @@
using Source.Common.Mathematics;
using Source.Common.Utilities;
-using System.Collections.Frozen;
using System.Text;
using static Source.GUI.Controls.AnimationController;
@@ -1192,30 +1191,26 @@ private void SetupPosition(ref AnimCmdAnimate cmd, ref float output, ReadOnlySpa
output = (float)pos;
}
- static readonly FrozenDictionary g_AlignmentLookup = new Dictionary(){
- { "northwest".Hash(), Alignment.Northwest },
- { "north".Hash(), Alignment.North },
- { "northeast".Hash(), Alignment.Northeast },
- { "west".Hash(), Alignment.West },
- { "center".Hash(), Alignment.Center },
- { "east".Hash(), Alignment.East },
- { "southwest".Hash(), Alignment.Southwest },
- { "south".Hash(), Alignment.South },
- { "southeast".Hash(), Alignment.Southeast },
- { "nw".Hash(), Alignment.Northwest },
- { "n".Hash(), Alignment.North },
- { "ne".Hash(), Alignment.Northeast },
- { "w".Hash(), Alignment.West },
- { "c".Hash(), Alignment.Center },
- { "e".Hash(), Alignment.East },
- { "sw".Hash(), Alignment.Southwest },
- { "s".Hash(), Alignment.South },
- { "se".Hash(), Alignment.Southeast },
- }.ToFrozenDictionary();
-
private static Alignment LookupAlignment(ReadOnlySpan sz) {
sz = sz.SliceNullTerminatedString();
- return g_AlignmentLookup.TryGetValue(sz.Hash(), out Alignment al) ? al : Alignment.Northwest;
+ if (sz.Length > "northwest".Length)
+ return Alignment.Northwest;
+
+ Span lower = stackalloc char[sz.Length];
+ sz.ToLowerInvariant(lower);
+ switch (lower) {
+ case "northwest": case "nw": return Alignment.Northwest;
+ case "north": case "n": return Alignment.North;
+ case "northeast": case "ne": return Alignment.Northeast;
+ case "west": case "w": return Alignment.West;
+ case "center": case "c": return Alignment.Center;
+ case "east": case "e": return Alignment.East;
+ case "southwest": case "sw": return Alignment.Southwest;
+ case "south": case "s": return Alignment.South;
+ case "southeast": case "se": return Alignment.Southeast;
+ }
+
+ return Alignment.Northwest;
}
private bool ParseScriptFile(ReadOnlySpan mem) {
diff --git a/Source.NET.slnx b/Source.NET.slnx
index ea8b4b413..b0328a0f5 100644
--- a/Source.NET.slnx
+++ b/Source.NET.slnx
@@ -22,6 +22,9 @@
+
+
+
diff --git a/Source.ShaderAPI.Gl46/PersistentBufferRing.cs b/Source.ShaderAPI.Gl46/PersistentBufferRing.cs
new file mode 100644
index 000000000..4fe3836a7
--- /dev/null
+++ b/Source.ShaderAPI.Gl46/PersistentBufferRing.cs
@@ -0,0 +1,84 @@
+namespace Source.ShaderAPI.Gl46;
+
+public unsafe class PersistentBufferRing : IDisposable
+{
+ const int MEMORY_BUDGET = 48 * 1024 * 1024;
+ const int MIN_SLOTS = 8;
+ const int MAX_SLOTS = 256;
+ const ulong WAIT_TIMEOUT_NS = 1_000_000_000;
+
+ sealed class Slot
+ {
+ public uint Buffer;
+ public byte* Memory;
+ public void* Fence;
+ }
+
+ readonly List Slots = [];
+ readonly int Size;
+ readonly int MaxSlots;
+ int Current;
+
+ public PersistentBufferRing(int size) {
+ Size = size;
+ MaxSlots = Math.Clamp(MEMORY_BUDGET / Math.Max(size, 1), MIN_SLOTS, MAX_SLOTS);
+ Slots.Add(CreateSlot());
+ }
+
+ public uint Buffer => Slots[Current].Buffer;
+ public byte* Memory => Slots[Current].Memory;
+
+ Slot CreateSlot() {
+ Slot slot = new();
+ slot.Buffer = glCreateBuffer();
+ glNamedBufferStorage(slot.Buffer, Size, null, GL_MAP_WRITE_BIT | GL_MAP_PERSISTENT_BIT | GL_MAP_COHERENT_BIT);
+ slot.Memory = (byte*)glMapNamedBufferRange(slot.Buffer, 0, Size, GL_MAP_WRITE_BIT | GL_MAP_PERSISTENT_BIT | GL_MAP_COHERENT_BIT);
+ return slot;
+ }
+
+ static bool IsSignaled(Slot slot, uint flags, ulong timeout) {
+ if (slot.Fence == null)
+ return true;
+
+ if (glClientWaitSync(slot.Fence, flags, timeout) == GL_TIMEOUT_EXPIRED)
+ return false;
+
+ glDeleteSync(slot.Fence);
+ slot.Fence = null;
+ return true;
+ }
+
+ public void Advance() {
+ Slot slot = Slots[Current];
+ if (slot.Fence != null)
+ glDeleteSync(slot.Fence);
+ slot.Fence = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0);
+
+ int next = (Current + 1) % Slots.Count;
+ if (next != Current && IsSignaled(Slots[next], 0, 0)) {
+ Current = next;
+ return;
+ }
+
+ if (Slots.Count < MaxSlots) {
+ Slots.Insert(Current + 1, CreateSlot());
+ Current++;
+ return;
+ }
+
+ while (!IsSignaled(Slots[next], GL_SYNC_FLUSH_COMMANDS_BIT, WAIT_TIMEOUT_NS)) ;
+ Current = next;
+ }
+
+ public void Dispose() {
+ foreach (Slot slot in Slots) {
+ if (slot.Fence != null)
+ glDeleteSync(slot.Fence);
+ glUnmapNamedBuffer(slot.Buffer);
+ uint buffer = slot.Buffer;
+ glDeleteBuffers(1, &buffer);
+ }
+ Slots.Clear();
+ Current = 0;
+ }
+}
diff --git a/Source.ShaderAPI.Gl46/ShaderAPIGl46.cs b/Source.ShaderAPI.Gl46/ShaderAPIGl46.cs
index ba723304e..acc23bdf8 100644
--- a/Source.ShaderAPI.Gl46/ShaderAPIGl46.cs
+++ b/Source.ShaderAPI.Gl46/ShaderAPIGl46.cs
@@ -148,12 +148,6 @@ public unsafe void ClearBuffers(bool clearColor, bool clearDepth, bool clearSten
glStencilMask(0xFF);
if (flags != 0) {
- bool* colorMask = stackalloc bool[4];
- glGetBooleanv(GL_COLOR_WRITEMASK, colorMask);
- bool scissorWasEnabled = glIsEnabled(GL_SCISSOR_TEST);
- if (clearColor)
- glColorMask(true, true, true, true);
-
bool renderTargetMatchesViewport =
(renderTargetWidth == -1 && renderTargetHeight == -1) ||
(renderTargetWidth == Viewport.Width && renderTargetHeight == Viewport.Height);
@@ -169,10 +163,8 @@ public unsafe void ClearBuffers(bool clearColor, bool clearDepth, bool clearSten
glDisable(GL_SCISSOR_TEST);
}
- if (scissorWasEnabled)
+ if (ScissorEnabled)
glEnable(GL_SCISSOR_TEST);
- if (clearColor)
- glColorMask(colorMask[0], colorMask[1], colorMask[2], colorMask[3]);
}
if (boardStateValid) {
@@ -578,8 +570,11 @@ public void SetStencilWriteMask(uint msk) {
void ApplyStencilFunc() => glStencilFunc(StencilCompareFunction.GLEnum(), StencilReferenceValue, StencilTestMask);
+ bool ScissorEnabled;
+
public void SetScissorRect(int left, int top, int right, int bottom, bool enableScissor) {
FlushBufferedPrimitives();
+ ScissorEnabled = enableScissor;
if (!enableScissor) {
glDisable(GL_SCISSOR_TEST);
return;
@@ -1934,7 +1929,7 @@ public void GMOD_ForceFilterMode(bool min, int mode) {
ForcedMagFilter = mode;
}
- unsafe void ApplyForcedFilter(Sampler sampler, ShaderAPITextureHandle_t textureHandle) {
+ void ApplyForcedFilter(Sampler sampler, ShaderAPITextureHandle_t textureHandle) {
int unit = (int)sampler;
if (ForcedMinFilter == 0 && ForcedMagFilter == 0) {
if (ForcedFilterSamplerBound[unit]) {
@@ -1947,20 +1942,19 @@ unsafe void ApplyForcedFilter(Sampler sampler, ShaderAPITextureHandle_t textureH
if (ForcedFilterSamplers[unit] == 0)
ForcedFilterSamplers[unit] = glCreateSamplers();
uint samplerObject = ForcedFilterSamplers[unit];
- uint texture = GetGL46Texture(textureHandle);
-
- int value;
- foreach (int pname in (ReadOnlySpan)[GL_TEXTURE_WRAP_S, GL_TEXTURE_WRAP_T, GL_TEXTURE_WRAP_R, GL_TEXTURE_MIN_FILTER, GL_TEXTURE_MAG_FILTER]) {
- glGetTextureParameteriv(texture, pname, &value);
- glSamplerParameteri(samplerObject, pname, value);
- }
- if (SupportsSRGBDecode()) {
- glGetTextureParameteriv(texture, GL_TEXTURE_SRGB_DECODE_EXT, &value);
- glSamplerParameteri(samplerObject, GL_TEXTURE_SRGB_DECODE_EXT, value);
- }
+ InternalTextureInfo info = GetTexture(textureHandle);
+ ref TextureSamplerParams samplerParams = ref info.GetSamplerParams();
+
+ glSamplerParameteri(samplerObject, GL_TEXTURE_WRAP_S, samplerParams.WrapS);
+ glSamplerParameteri(samplerObject, GL_TEXTURE_WRAP_T, samplerParams.WrapT);
+ glSamplerParameteri(samplerObject, GL_TEXTURE_WRAP_R, samplerParams.WrapR);
+ glSamplerParameteri(samplerObject, GL_TEXTURE_MIN_FILTER, samplerParams.MinFilter);
+ glSamplerParameteri(samplerObject, GL_TEXTURE_MAG_FILTER, samplerParams.MagFilter);
+ if (SupportsSRGBDecode())
+ glSamplerParameteri(samplerObject, GL_TEXTURE_SRGB_DECODE_EXT, info.LastSRGBDecode);
glSamplerParameterf(samplerObject, GL_TEXTURE_MAX_ANISOTROPY, 1.0f);
- bool mipped = GetTexture(textureHandle).Levels > 1;
+ bool mipped = info.Levels > 1;
switch (ForcedMinFilter) {
case 0:
break;
@@ -2214,6 +2208,7 @@ public unsafe void CreateTextures(
else {
texture.NumCopies = (byte)copies;
texture.TextureCopies = new uint[copies];
+ texture.SamplerParams = new TextureSamplerParams[copies];
for (int k = 0; k < copies; k++) {
uint glTex = glCreateTexture(texture.DetermineGLObjectType());
glTextureStorage2D(glTex, mipCount, ImageLoader.GetGLImageInternalFormat(dstFormat, isSRGB), width, height);
@@ -2275,9 +2270,27 @@ public ShaderAPITextureHandle_t CreateTextureHandle() {
return handle;
}
+ public struct TextureSamplerParams
+ {
+ public int WrapS;
+ public int WrapT;
+ public int WrapR;
+ public int MinFilter;
+ public int MagFilter;
+
+ public static TextureSamplerParams Default => new() {
+ WrapS = GL_REPEAT,
+ WrapT = GL_REPEAT,
+ WrapR = GL_REPEAT,
+ MinFilter = GL_NEAREST_MIPMAP_LINEAR,
+ MagFilter = GL_LINEAR
+ };
+ }
+
public class InternalTextureInfo
{
internal uint[] TextureCopies = [0];
+ internal TextureSamplerParams[] SamplerParams = [TextureSamplerParams.Default];
internal readonly uint DepthStencilSurface = 0;
internal readonly InlineArray2 RenderTargetSurface;
@@ -2327,12 +2340,16 @@ public uint[] GetTextureArray() {
public void SetTexture(uint tex) {
TextureCopies[0] = tex;
+ SamplerParams[0] = TextureSamplerParams.Default;
}
public void SetTexture(int copy, uint tex) {
TextureCopies[copy] = tex;
+ SamplerParams[copy] = TextureSamplerParams.Default;
}
+ public ref TextureSamplerParams GetSamplerParams() => ref SamplerParams[NumCopies == 1 ? 0 : CurrentCopy];
+
internal nuint GetMemUsage() {
return SizeBytes;
}
@@ -2832,58 +2849,73 @@ public void TexWrap(TexCoordComponent coord, TexWrapMode wrapMode) {
return;
}
+ int wrap;
switch (wrapMode) {
case TexWrapMode.Clamp:
- glTextureParameteri(GetModifyTexture(), coordinate, GL_CLAMP_TO_EDGE);
+ wrap = GL_CLAMP_TO_EDGE;
break;
case TexWrapMode.Repeat:
- glTextureParameteri(GetModifyTexture(), coordinate, GL_REPEAT);
+ wrap = GL_REPEAT;
break;
case TexWrapMode.Border:
- glTextureParameteri(GetModifyTexture(), coordinate, GL_CLAMP_TO_BORDER);
+ wrap = GL_CLAMP_TO_BORDER;
break;
default:
Warning("ShaderAPIGl46.TexWrap: unknown wrapMode\n");
- break;
+ return;
+ }
+
+ glTextureParameteri(GetModifyTexture(), coordinate, wrap);
+ ref TextureSamplerParams samplerParams = ref GetTexture(ModifyTextureHandle).GetSamplerParams();
+ switch (coord) {
+ case TexCoordComponent.S: samplerParams.WrapS = wrap; break;
+ case TexCoordComponent.T: samplerParams.WrapT = wrap; break;
+ case TexCoordComponent.U: samplerParams.WrapR = wrap; break;
}
}
IMaterialSystemHardwareConfig HardwareConfig = Singleton();
public void TexMinFilter(TexFilterMode mode) {
+ int filter;
switch (mode) {
case TexFilterMode.Nearest:
- glTextureParameteri(GetModifyTexture(), GL_TEXTURE_MIN_FILTER, GL_NEAREST);
+ filter = GL_NEAREST;
break;
case TexFilterMode.Linear:
- glTextureParameteri(GetModifyTexture(), GL_TEXTURE_MIN_FILTER, GL_LINEAR);
+ filter = GL_LINEAR;
break;
case TexFilterMode.NearestMipmapNearest:
- glTextureParameteri(GetModifyTexture(), GL_TEXTURE_MIN_FILTER, GL_NEAREST_MIPMAP_NEAREST);
+ filter = GL_NEAREST_MIPMAP_NEAREST;
break;
case TexFilterMode.LinearMipmapNearest:
- glTextureParameteri(GetModifyTexture(), GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_NEAREST);
+ filter = GL_LINEAR_MIPMAP_NEAREST;
break;
case TexFilterMode.NearestMipmapLinear:
- glTextureParameteri(GetModifyTexture(), GL_TEXTURE_MIN_FILTER, GL_NEAREST_MIPMAP_LINEAR);
+ filter = GL_NEAREST_MIPMAP_LINEAR;
break;
case TexFilterMode.LinearMipmapLinear:
- glTextureParameteri(GetModifyTexture(), GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
+ filter = GL_LINEAR_MIPMAP_LINEAR;
break;
case TexFilterMode.Anisotropic:
glTextureParameterf(GetModifyTexture(), GL_TEXTURE_MAX_ANISOTROPY, AnisotropicLevel);
- glTextureParameteri(GetModifyTexture(), GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
+ filter = GL_LINEAR_MIPMAP_LINEAR;
break;
default:
- break;
+ return;
}
+
+ glTextureParameteri(GetModifyTexture(), GL_TEXTURE_MIN_FILTER, filter);
+ GetTexture(ModifyTextureHandle).GetSamplerParams().MinFilter = filter;
}
public void TexMagFilter(TexFilterMode mode) {
switch (mode) {
case TexFilterMode.Nearest:
glTextureParameteri(GetModifyTexture(), GL_TEXTURE_MAG_FILTER, GL_NEAREST);
+ GetTexture(ModifyTextureHandle).GetSamplerParams().MagFilter = GL_NEAREST;
break;
case TexFilterMode.Linear:
glTextureParameteri(GetModifyTexture(), GL_TEXTURE_MAG_FILTER, GL_LINEAR);
+ GetTexture(ModifyTextureHandle).GetSamplerParams().MagFilter = GL_LINEAR;
break;
case TexFilterMode.NearestMipmapNearest:
Warning("ShaderAPIGl46.TexMagFilter: TexFilterMode.NearestMipmapNearest is invalid\n");
@@ -2899,6 +2931,7 @@ public void TexMagFilter(TexFilterMode mode) {
break;
case TexFilterMode.Anisotropic:
glTextureParameteri(GetModifyTexture(), GL_TEXTURE_MAG_FILTER, GL_LINEAR);
+ GetTexture(ModifyTextureHandle).GetSamplerParams().MagFilter = GL_LINEAR;
glTextureParameterf(GetModifyTexture(), GL_TEXTURE_MAX_ANISOTROPY_EXT, AnisotropicLevel);
break;
default:
diff --git a/Source.ShaderAPI.Gl46/VertexBufferGl46.cs b/Source.ShaderAPI.Gl46/VertexBufferGl46.cs
index 18ae5e86f..810940e0a 100644
--- a/Source.ShaderAPI.Gl46/VertexBufferGl46.cs
+++ b/Source.ShaderAPI.Gl46/VertexBufferGl46.cs
@@ -21,10 +21,12 @@ public unsafe class VertexBufferGl46 : IDisposable
internal bool ExternalMemory;
internal bool SoftwareVertexProcessing;
internal bool LateCreateShouldDiscard;
+ internal bool Written;
int vbo = -1;
+ PersistentBufferRing? ring;
- internal uint VBO() => vbo > 0 ? (uint)vbo : throw new NullReferenceException("Vertex Buffer Object was null");
+ internal uint VBO() => ring != null ? ring.Buffer : vbo > 0 ? (uint)vbo : throw new NullReferenceException("Vertex Buffer Object was null");
public VertexBufferGl46(bool dynamic) {
Dynamic = dynamic;
@@ -134,6 +136,16 @@ internal void ChangeConfiguration(VertexFormat format, int vertexSize, int total
int lastBufferSize = -1;
public void RecomputeVBO() {
+ if (Dynamic) {
+ if (BufferSize > lastBufferSize) {
+ lastBufferSize = BufferSize;
+ ring?.Dispose();
+ ring = new PersistentBufferRing(BufferSize);
+ Written = false;
+ }
+ return;
+ }
+
// Create the VBO if it doesn't exist
if (vbo == -1)
vbo = (int)glCreateBuffer();
@@ -175,11 +187,22 @@ public void RecomputeVBO() {
int lockOffset = NextLockOffset();
baseVertexIndex = VertexSize == 0 ? 0 : (lockOffset / VertexSize);
+ if (Dynamic) {
+ if (ring == null)
+ RecomputeVBO();
+ else if (discard && Written)
+ ring.Advance();
+
+ if (discard)
+ Written = false;
+
+ Locked = true;
+ Position = lockOffset;
+ return ring!.Memory + lockOffset;
+ }
+
if (SysmemBuffer == null)
RecomputeVBO();
- else if (discard)
- glNamedBufferData((uint)vbo, BufferSize, null, GL_DYNAMIC_DRAW);
-
Locked = true;
Position = lockOffset;
@@ -193,8 +216,12 @@ public void Unlock(int vertexCount) {
int lockOffset = NextLockOffset();
int bufferSize = vertexCount * VertexSize;
- if (bufferSize > 0)
- glNamedBufferSubData((uint)vbo, lockOffset, bufferSize, (byte*)SysmemBuffer + lockOffset);
+ if (bufferSize > 0) {
+ if (Dynamic)
+ Written = true;
+ else
+ glNamedBufferSubData((uint)vbo, lockOffset, bufferSize, (byte*)SysmemBuffer + lockOffset);
+ }
Position = lockOffset + bufferSize;
Locked = false;
}
@@ -204,11 +231,19 @@ public void Unlock(int vertexCount) {
public byte* ModifyLock(int firstVertex, int numVerts, out int baseVertexIndex) {
Assert(!Locked);
+ modifyOffset = firstVertex * VertexSize;
+ baseVertexIndex = firstVertex;
+ if (Dynamic) {
+ if (ring == null)
+ RecomputeVBO();
+
+ Locked = true;
+ return ring!.Memory + modifyOffset;
+ }
+
if (SysmemBuffer == null)
RecomputeVBO();
- modifyOffset = firstVertex * VertexSize;
- baseVertexIndex = firstVertex;
Locked = true;
return (byte*)SysmemBuffer + modifyOffset;
}
@@ -217,8 +252,12 @@ public void ModifyUnlock(int vertexCount) {
if (!Locked)
return;
- if (vertexCount > 0)
- glNamedBufferSubData((uint)vbo, modifyOffset, vertexCount * VertexSize, (byte*)SysmemBuffer + modifyOffset);
+ if (vertexCount > 0) {
+ if (Dynamic)
+ Written = true;
+ else
+ glNamedBufferSubData((uint)vbo, modifyOffset, vertexCount * VertexSize, (byte*)SysmemBuffer + modifyOffset);
+ }
Locked = false;
}
@@ -362,6 +401,12 @@ public static unsafe void ComputeVertexDescription(byte* vertexMemory, VertexFor
}
public void Dispose() {
+ if (ring != null) {
+ ring.Dispose();
+ ring = null;
+ lastBufferSize = -1;
+ }
+
if (vbo != -1) {
Assert(SysmemBuffer != null);
fixed (int* ugh = &vbo)
diff --git a/Source.StudioRender/StudioRender.cs b/Source.StudioRender/StudioRender.cs
index aa802dee0..63b248fe3 100644
--- a/Source.StudioRender/StudioRender.cs
+++ b/Source.StudioRender/StudioRender.cs
@@ -130,6 +130,44 @@ internal void DrawModel(ref DrawModelInfo info, StudioRenderCtx RC, Span poseToWorld, StudioHeader studioHdr, int boneMask, in Vector3 viewOrigin, Matrix3x4* pBoneToWorld) {
if ((studioHdr.Flags & StudioHdrFlags.StaticProp) != 0) {
poseToWorld[0] = pBoneToWorld[0];
diff --git a/Source.StudioRender/StudioRenderContext.cs b/Source.StudioRender/StudioRenderContext.cs
index 98eb25fbc..fb9873e47 100644
--- a/Source.StudioRender/StudioRenderContext.cs
+++ b/Source.StudioRender/StudioRenderContext.cs
@@ -731,6 +731,13 @@ public void DrawModel(ref DrawModelResults results, ref DrawModelInfo info, Span
studioRenderImp.DrawModel(ref info, RC, boneToWorld, flexWeightsOrZero, flexDelayedWeightsOrWeights, flags);
}
+ public void DrawModelStaticProp(ref DrawModelInfo info, in Matrix3x4 modelToWorld, StudioRenderFlags flags = StudioRenderFlags.DrawEntireModel) {
+ if (info.Lod < info.HardwareData.RootLOD)
+ info.Lod = info.HardwareData.RootLOD;
+
+ studioRenderImp.DrawModelStaticProp(ref info, RC, in modelToWorld, flags);
+ }
+
private void InvokeBindProxies(ref DrawModelInfo info) {
if (RC.ForcedMaterial != null) {
if (RC.ForcedMaterialType == OverrideType.Normal && RC.ForcedMaterial.HasProxy()) {