Repository navigation
v0.17.0 breaking: fail-loud config + actually-work SkipServerOnFalse
⚠ Breaking — config delivery is now fail-loud
Pre-0.17.0 the generator unconditionally emitted resolver.TryRegisterConfigProvider<TConfig>(new StaticConfigProvider<TConfig>(new TConfig())) for every service that declared a config type, and MetaServiceResolver.ResolveConfigAsync returned null (with a warning) when no provider was registered. Together these two behaviors silently substituted an empty default-constructed config — bugs hid for hours: Context.Config.X looked legitimate but contained zeroed fields, NREs detonated deep inside service code, the warning got drowned in regular console output. Two changes close that:
- Generator no longer auto-registers a default-constructed
StaticConfigProvider<TConfig>unconditionally. Auto-registration is now opt-in via[MetaService(DefaultConfig = true)]— the flag is the explicit "I am OK withnew TConfig()as a fallback" contract. Services that declareConfigType = typeof(X)withoutDefaultConfig = truerequire an explicitresolver.RegisterConfigProvider<X>(...)call from the host. MetaServiceResolver.ResolveConfigAsyncnow throwsInvalidOperationExceptionwhen no provider is registered, instead of warning-and-returning-null. The exception text names the missing config type, the failing service, the entity, and prints the two recommended registration recipes (StaticConfigProviderfor preloaded instances,DownloadingConfigProviderfor server-pushed bytes).
Failure now happens at the first subscribe — easy to spot, exact stack trace, actionable message. No more silent zeroed config.
Migration
If your build broke after the bump, you have two paths depending on intent:
(A) You always want the explicit registration (recommended — works for both LocalBackend and real servers):
// Before RegisterAllServices() is fine; RegisterConfigProvider is clobbering — order
// relative to RegisterAllServices doesn't matter, the explicit call always wins over
// any auto-emitted default that DefaultConfig = true might add.
client.Resolver.RegisterConfigProvider<MyConfig>(
new StaticConfigProvider<MyConfig>(loadedConfig));
// or for server-driven configs:
client.Resolver.RegisterConfigProvider<MyConfig>(
new DownloadingConfigProvider<MyConfig>(
urlResolver: client.ConfigDownloadUrlResolver(typeof(MyState).FullName!),
downloader: UnityConfigDownloader.DownloadAsync,
serializer: client.Serializer,
cache: new FileConfigCache<MyConfig>(cacheDir, client.Serializer)));(B) An empty new MyConfig() truly is acceptable as a fallback (the legacy behavior — opt in explicitly):
[MetaService(StateType = typeof(MyState), DefaultConfig = true)]
// ^^^^^^^^^^^^^^^^^^^
// the generator sees this and emits TryRegisterConfigProvider<MyConfig>(...)
// with new MyConfig() as before — same auto-default, but now requested.
public interface IMyService : IMetaService { ... }If you're seeing the new exception and MyConfig's ctor genuinely produces a usable default — option (B) is one attribute change. If MyConfig requires actual data (most cases) — option (A) is the right answer.
Changed
ServiceRegistrationGenerator—Generatenow tracksusesDefaultConfigseparately fromconfigTypeFullName. Emission ofTryRegisterConfigProvider<TConfig>(new StaticConfigProvider<TConfig>(new TConfig()))is gated byusesDefaultConfig; explicitConfigType = typeof(X)withoutDefaultConfig = trueproduces no auto-fallback.MetaServiceResolver.ResolveConfigAsync— missing provider raisesInvalidOperationExceptionwith an actionable message and registration recipes; the previousMetaLog.Warning+return nullpath is gone.
Fixed
[MetaMethod(SkipServerOnFalse = true)]was silently ignored bySimplifiedApiClientGeneratorsince the generator was rewritten — the attribute parsed and the docs described it, but the codegen always emitted an unconditional_ = _network.CallBytesAsync(...)for Optimistic methods. OldXApiClientGeneratorhad it; the rewrite lost it. NowSimplifiedApiClientGenerator.GenerateMethodparsesSkipServerOnFalse, threads it intoGenerateOptimisticMethod/GenerateOptimisticMethodSync, and wraps the fire-and-forget RPC inif (!EqualityComparer<T>.Default.Equals(localResult, default!)) { ... }for both async and sync overloads — when local impl returns the default value (typicallyfalsefor validation-style methods), the RPC is short-circuited entirely. Compile-time validation (#error) now also rejects: (a)SkipServerOnFalse = trueon avoidmethod (no return value to compare againstdefault), and (b)SkipServerOnFalse = truecombined with an explicit non-OptimisticMode. Coverage: newSkipServerOnFalseTestsintegration class with three scenarios (true → server receives, false → server skipped, mixed → only the trues land).