Skip to content

Commit d6a3af0

Browse files
committed
tweak: reduce codegen size, use getters for ts
1 parent 4252ed1 commit d6a3af0

7 files changed

Lines changed: 236 additions & 10 deletions

File tree

src/NativeCodeGen.Core/Generation/ILanguageEmitter.cs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,12 @@ public interface ILanguageEmitter
120120
/// </summary>
121121
void EmitMethodEnd(CodeBuilder cb);
122122

123+
/// <summary>
124+
/// Emits a getter proxy that calls an underlying method.
125+
/// Used when a method has all optional parameters - generates both the method and a getter.
126+
/// </summary>
127+
void EmitGetterProxy(CodeBuilder cb, string propertyName, string methodName, string returnType);
128+
123129
/// <summary>
124130
/// Emits a native invoke call.
125131
/// </summary>
@@ -204,7 +210,11 @@ public enum MethodKind
204210
/// <summary>Instance method (uses self/this)</summary>
205211
Instance,
206212
/// <summary>Static method</summary>
207-
Static
213+
Static,
214+
/// <summary>Getter property (parameterless method returning a value)</summary>
215+
Getter,
216+
/// <summary>Setter property (single-parameter void method)</summary>
217+
Setter
208218
}
209219

210220
/// <summary>

src/NativeCodeGen.Core/Generation/SharedClassGenerator.cs

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,45 @@ private void EmitMethod(
250250
var outputParams = parameters.Where(p => p.IsPureOutput).ToList();
251251
var outputParamTypes = outputParams.Select(p => p.Type).ToList();
252252

253+
var returnType = _emitter.TypeMapper.BuildCombinedReturnType(native.ReturnType, outputParamTypes);
254+
255+
// Determine if this should be a getter or setter (TypeScript only)
256+
var actualKind = kind;
257+
var actualMethodName = methodName;
258+
var hasOutputParams = outputParams.Count > 0;
259+
260+
// Check if this is a getter candidate (for generating getter proxy when params are optional)
261+
// Note: Methods with output params return tuples, which are still valid getter return types
262+
var hasReturnValue = native.ReturnType.Category != TypeCategory.Void || hasOutputParams;
263+
var isGetterCandidate = kind == MethodKind.Instance &&
264+
_emitter.Config.SupportsGetters &&
265+
hasReturnValue &&
266+
NameConverter.IsGetterName(methodName);
267+
268+
var allParamsOptional = inputParams.Count > 0 && inputParams.All(p => p.HasDefaultValue);
269+
var shouldEmitGetterProxy = isGetterCandidate && allParamsOptional;
270+
271+
if (kind == MethodKind.Instance && _emitter.Config.SupportsGetters)
272+
{
273+
// Getter: parameterless, returns a value (including tuples), name starts with "get" or "is"
274+
if (inputParams.Count == 0 &&
275+
hasReturnValue &&
276+
NameConverter.IsGetterName(methodName))
277+
{
278+
actualKind = MethodKind.Getter;
279+
actualMethodName = NameConverter.GetterToPropertyName(methodName);
280+
}
281+
// Setter: single parameter, void return, name starts with "set"
282+
else if (inputParams.Count == 1 &&
283+
native.ReturnType.Category == TypeCategory.Void &&
284+
!hasOutputParams &&
285+
NameConverter.IsSetterName(methodName))
286+
{
287+
actualKind = MethodKind.Setter;
288+
actualMethodName = NameConverter.SetterToPropertyName(methodName);
289+
}
290+
}
291+
253292
GenerateMethodDoc(cb, native, inputParams, outputParams);
254293

255294
var methodParams = inputParams.Select(p => new MethodParameter(
@@ -258,8 +297,7 @@ private void EmitMethod(
258297
p.HasDefaultValue
259298
)).ToList();
260299

261-
var returnType = _emitter.TypeMapper.BuildCombinedReturnType(native.ReturnType, outputParamTypes);
262-
_emitter.EmitMethodStart(cb, className, methodName, methodParams, returnType, kind);
300+
_emitter.EmitMethodStart(cb, className, actualMethodName, methodParams, returnType, actualKind);
263301

264302
// Build invoke args
265303
var args = new List<string>();
@@ -271,6 +309,13 @@ private void EmitMethod(
271309

272310
_emitter.EmitInvokeNative(cb, native.Hash, args, native.ReturnType, outputParamTypes);
273311
_emitter.EmitMethodEnd(cb);
312+
313+
// If all params are optional and it's a getter candidate, also emit a getter proxy
314+
if (shouldEmitGetterProxy)
315+
{
316+
var propertyName = NameConverter.GetterToPropertyName(methodName);
317+
_emitter.EmitGetterProxy(cb, propertyName, methodName, returnType);
318+
}
274319
}
275320

276321
private void GenerateMethodDoc(CodeBuilder cb, NativeDefinition native, List<NativeParameter> inputParams, List<NativeParameter>? outputParams = null)

src/NativeCodeGen.Core/TypeSystem/TypeMapperBase.cs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,9 @@ public record LanguageConfig
4343
public required bool UseFloatWrapper { get; init; }
4444
public required bool UseHashWrapper { get; init; }
4545

46+
// Whether the language supports getter properties (TypeScript does, Lua doesn't)
47+
public required bool SupportsGetters { get; init; }
48+
4649
public static readonly LanguageConfig TypeScript = new()
4750
{
4851
VoidType = "void",
@@ -72,7 +75,8 @@ public record LanguageConfig
7275
FloatWrapperAlias = "f",
7376
HashWrapperAlias = "_h",
7477
UseFloatWrapper = true,
75-
UseHashWrapper = true
78+
UseHashWrapper = true,
79+
SupportsGetters = true
7680
};
7781

7882
public static readonly LanguageConfig Lua = new()
@@ -104,7 +108,8 @@ public record LanguageConfig
104108
FloatWrapperAlias = "f",
105109
HashWrapperAlias = "_h",
106110
UseFloatWrapper = false,
107-
UseHashWrapper = true
111+
UseHashWrapper = true,
112+
SupportsGetters = false
108113
};
109114
}
110115

src/NativeCodeGen.Core/Utilities/NameConverter.cs

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,4 +188,74 @@ public static string EscapeLuaReserved(string name)
188188
if (string.IsNullOrEmpty(name)) return name;
189189
return LuaReservedWords.Contains(name) ? "_" + name : name;
190190
}
191+
192+
/// <summary>
193+
/// Checks if a method name is a getter (starts with "get" or "is" followed by uppercase).
194+
/// </summary>
195+
public static bool IsGetterName(string methodName)
196+
{
197+
if (string.IsNullOrEmpty(methodName))
198+
return false;
199+
200+
// Check "get" prefix (getHealth -> Health)
201+
if (methodName.Length > 3 &&
202+
methodName.StartsWith("get", StringComparison.Ordinal) &&
203+
char.IsUpper(methodName[3]))
204+
return true;
205+
206+
// Check "is" prefix (isMale -> IsMale)
207+
if (methodName.Length > 2 &&
208+
methodName.StartsWith("is", StringComparison.Ordinal) &&
209+
char.IsUpper(methodName[2]))
210+
return true;
211+
212+
return false;
213+
}
214+
215+
/// <summary>
216+
/// Converts a getter method name to a property name.
217+
/// "get" prefix is removed, "is" prefix is kept (for clarity).
218+
/// </summary>
219+
public static string GetterToPropertyName(string methodName)
220+
{
221+
if (string.IsNullOrEmpty(methodName))
222+
return methodName;
223+
224+
// "get" prefix: getHealth -> Health
225+
if (methodName.Length > 3 &&
226+
methodName.StartsWith("get", StringComparison.Ordinal) &&
227+
char.IsUpper(methodName[3]))
228+
return methodName[3..];
229+
230+
// "is" prefix: isMale -> IsMale (keep the "Is" for boolean clarity)
231+
if (methodName.Length > 2 &&
232+
methodName.StartsWith("is", StringComparison.Ordinal) &&
233+
char.IsUpper(methodName[2]))
234+
return char.ToUpperInvariant(methodName[0]) + methodName[1..];
235+
236+
return methodName;
237+
}
238+
239+
/// <summary>
240+
/// Checks if a method name is a setter (starts with "set" followed by uppercase).
241+
/// </summary>
242+
public static bool IsSetterName(string methodName)
243+
{
244+
if (string.IsNullOrEmpty(methodName) || methodName.Length <= 3)
245+
return false;
246+
247+
return methodName.StartsWith("set", StringComparison.Ordinal) &&
248+
char.IsUpper(methodName[3]);
249+
}
250+
251+
/// <summary>
252+
/// Converts a setter method name to a property name (removes "set" prefix).
253+
/// </summary>
254+
public static string SetterToPropertyName(string methodName)
255+
{
256+
if (!IsSetterName(methodName))
257+
return methodName;
258+
259+
return methodName[3..]; // Remove "set", keep PascalCase
260+
}
191261
}

src/NativeCodeGen.Lua/LuaEmitter.cs

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,9 @@ public void EmitTypeImports(CodeBuilder cb, IEnumerable<string> enumTypes, IEnum
6060

6161
public void EmitClassStart(CodeBuilder cb, string className, string? baseClass, ClassKind kind)
6262
{
63+
// Emit native aliases at the top of the file
64+
RawNativeBuilder.EmitLuaAliases(cb);
65+
6366
switch (kind)
6467
{
6568
case ClassKind.Handle:
@@ -152,6 +155,30 @@ public void EmitClassEnd(CodeBuilder cb, string className, ClassKind kind)
152155
cb.Dedent();
153156
cb.AppendLine("end");
154157
}
158+
else if (kind == ClassKind.Handle && className == "Player")
159+
{
160+
cb.AppendLine();
161+
cb.AppendLine("---Gets the player's server ID. In multiplayer, this is the player's unique server-side identifier.");
162+
cb.AppendLine("---@return number");
163+
cb.AppendLine("function Player:getServerId()");
164+
cb.Indent();
165+
// GET_PLAYER_SERVER_ID = 0x4D97BCC7 (CFX native)
166+
cb.AppendLine("return inv(0x4D97BCC7, self.handle, rai())");
167+
cb.Dedent();
168+
cb.AppendLine("end");
169+
}
170+
else if (kind == ClassKind.Handle && className == "Entity")
171+
{
172+
cb.AppendLine();
173+
cb.AppendLine("---Gets the network ID of this entity for network synchronization.");
174+
cb.AppendLine("---@return number");
175+
cb.AppendLine("function Entity:getNetworkId()");
176+
cb.Indent();
177+
// NETWORK_GET_NETWORK_ID_FROM_ENTITY = 0xF260AF6F43953316
178+
cb.AppendLine("return inv(0xF260AF6F43953316, self.handle, rai())");
179+
cb.Dedent();
180+
cb.AppendLine("end");
181+
}
155182

156183
cb.AppendLine($"return {className}");
157184
}
@@ -199,8 +226,8 @@ public void EmitFromNetworkIdMethod(CodeBuilder cb, string className)
199226
cb.Indent();
200227
// NETWORK_DOES_ENTITY_EXIST_WITH_NETWORK_ID = 0x38CE16C96BD11F2C
201228
// NETWORK_GET_ENTITY_FROM_NETWORK_ID = 0x5B912C3F653822E6
202-
cb.AppendLine("if not Citizen.InvokeNative(0x38CE16C96BD11F2C, netId) then return nil end");
203-
cb.AppendLine($"return {className}.fromHandle(Citizen.InvokeNative(0x5B912C3F653822E6, netId))");
229+
cb.AppendLine("if not inv(0x38CE16C96BD11F2C, netId, rai()) then return nil end");
230+
cb.AppendLine($"return {className}.fromHandle(inv(0x5B912C3F653822E6, netId, rai()))");
204231
cb.Dedent();
205232
cb.AppendLine("end");
206233
cb.AppendLine();
@@ -253,7 +280,8 @@ public void EmitWeaponConstructor(CodeBuilder cb, string className)
253280
public void EmitMethodStart(CodeBuilder cb, string className, string methodName, List<MethodParameter> parameters, string returnType, MethodKind kind)
254281
{
255282
var paramNames = string.Join(", ", parameters.Select(p => p.Name));
256-
var separator = kind == MethodKind.Instance ? ":" : ".";
283+
// Lua doesn't have getters/setters, so treat them as Instance methods
284+
var separator = (kind == MethodKind.Instance || kind == MethodKind.Getter || kind == MethodKind.Setter) ? ":" : ".";
257285
cb.AppendLine($"function {className}{separator}{methodName}({paramNames})");
258286
cb.Indent();
259287
}
@@ -265,6 +293,11 @@ public void EmitMethodEnd(CodeBuilder cb)
265293
cb.AppendLine();
266294
}
267295

296+
public void EmitGetterProxy(CodeBuilder cb, string propertyName, string methodName, string returnType)
297+
{
298+
// Lua doesn't support getters, so this is a no-op
299+
}
300+
268301
public void EmitInvokeNative(CodeBuilder cb, string hash, List<string> args, TypeInfo returnType, List<TypeInfo> outputParamTypes)
269302
{
270303
var allArgs = new List<string> { hash };
@@ -275,7 +308,7 @@ public void EmitInvokeNative(CodeBuilder cb, string hash, List<string> args, Typ
275308
allArgs.Add(_typeMapper.GetResultMarker(returnType));
276309
}
277310

278-
var invokeExpr = $"Citizen.InvokeNative({string.Join(", ", allArgs)})";
311+
var invokeExpr = $"inv({string.Join(", ", allArgs)})";
279312
var hasOutputParams = outputParamTypes.Count > 0;
280313

281314
// If no output params, use the simple return logic

src/NativeCodeGen.TypeScript/Generation/TypeScriptGenerator.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -695,7 +695,12 @@ export function createFromHandle<T>(name: string, handle: number): T | null {
695695
// GetHashKey accepts string|number since hashes can be pre-computed
696696
declare function GetHashKey(str: string | number): number;
697697
698-
export const inv = Citizen.invokeNative;
698+
// Native invoke wrapper - can be overridden for logging/debugging
699+
type InvokeFn = <T>(hash: string, ...args: any[]) => T;
700+
let _inv: InvokeFn = Citizen.invokeNative;
701+
export const inv = <T>(hash: string, ...args: any[]): T => _inv<T>(hash, ...args);
702+
export const setInvokeNative = (fn: InvokeFn) => { _inv = fn; };
703+
699704
export const rai = Citizen.resultAsInteger;
700705
export const raf = Citizen.resultAsFloat;
701706
export const ras = Citizen.resultAsString;

src/NativeCodeGen.TypeScript/TypeScriptEmitter.cs

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,32 @@ public void EmitClassEnd(CodeBuilder cb, string className, ClassKind kind)
225225
cb.Dedent();
226226
cb.AppendLine("}");
227227
}
228+
else if (kind == ClassKind.Handle && className == "Player")
229+
{
230+
cb.AppendLine();
231+
cb.AppendLine("/**");
232+
cb.AppendLine(" * Gets the player's server ID. In multiplayer, this is the player's unique server-side identifier.");
233+
cb.AppendLine(" */");
234+
cb.AppendLine("get ServerId(): number {");
235+
cb.Indent();
236+
// GET_PLAYER_SERVER_ID = 0x4D97BCC7 (CFX native)
237+
cb.AppendLine("return inv<number>('0x4D97BCC7', this.handle, rai());");
238+
cb.Dedent();
239+
cb.AppendLine("}");
240+
}
241+
else if (kind == ClassKind.Handle && className == "Entity")
242+
{
243+
cb.AppendLine();
244+
cb.AppendLine("/**");
245+
cb.AppendLine(" * Gets the network ID of this entity for network synchronization.");
246+
cb.AppendLine(" */");
247+
cb.AppendLine("get NetworkId(): number {");
248+
cb.Indent();
249+
// NETWORK_GET_NETWORK_ID_FROM_ENTITY = 0xF260AF6F43953316 (same as PED_TO_NET, VEH_TO_NET, OBJ_TO_NET)
250+
cb.AppendLine("return inv<number>('0xF260AF6F43953316', this.handle, rai());");
251+
cb.Dedent();
252+
cb.AppendLine("}");
253+
}
228254

229255
cb.Dedent();
230256
cb.AppendLine("}");
@@ -329,6 +355,23 @@ public void EmitWeaponConstructor(CodeBuilder cb, string className)
329355

330356
public void EmitMethodStart(CodeBuilder cb, string className, string methodName, List<MethodParameter> parameters, string returnType, MethodKind kind)
331357
{
358+
if (kind == MethodKind.Getter)
359+
{
360+
// Emit as getter property
361+
cb.AppendLine($"get {methodName}(): {returnType} {{");
362+
cb.Indent();
363+
return;
364+
}
365+
366+
if (kind == MethodKind.Setter)
367+
{
368+
// Emit as setter property - single parameter
369+
var param = parameters.First();
370+
cb.AppendLine($"set {methodName}({param.Name}: {param.Type}) {{");
371+
cb.Indent();
372+
return;
373+
}
374+
332375
var paramString = string.Join(", ", parameters.Select(p =>
333376
{
334377
// Rest parameters (starting with ...) need array type - use any[] since variadic can accept multiple types
@@ -351,6 +394,16 @@ public void EmitMethodEnd(CodeBuilder cb)
351394
cb.AppendLine();
352395
}
353396

397+
public void EmitGetterProxy(CodeBuilder cb, string propertyName, string methodName, string returnType)
398+
{
399+
cb.AppendLine($"get {propertyName}(): {returnType} {{");
400+
cb.Indent();
401+
cb.AppendLine($"return this.{methodName}();");
402+
cb.Dedent();
403+
cb.AppendLine("}");
404+
cb.AppendLine();
405+
}
406+
354407
public void EmitInvokeNative(CodeBuilder cb, string hash, List<string> args, TypeInfo returnType, List<TypeInfo> outputParamTypes)
355408
{
356409
var allArgs = new List<string> { $"'{hash}'" };
@@ -384,6 +437,11 @@ public void EmitInvokeNative(CodeBuilder cb, string hash, List<string> args, Typ
384437
{
385438
invokeExpr = $"({invokeExpr}) & 0xFFFFFFFF";
386439
}
440+
else if (returnType.IsBool)
441+
{
442+
// Coerce 0/1 to true/false
443+
invokeExpr = $"!!{invokeExpr}";
444+
}
387445

388446
if (returnType.Category == TypeCategory.Void)
389447
{

0 commit comments

Comments
 (0)