#import "WeatherRuntimeBridge.h" #import #import using namespace facebook::react; namespace { constexpr NSUInteger MaxPayloadBytes = 64u * 1024u * 1024u; constexpr NSUInteger MaxMetadataBytes = 1'048'576u; constexpr NSUInteger MaxContractBytes = 256u * 1024u; constexpr double MaxSafeInteger = 9007199254740991.0; BOOL validIdentifier(NSString *value) { const NSUInteger length = [value lengthOfBytesUsingEncoding:NSUTF8StringEncoding]; return length >= 1 && length <= 64; } BOOL validSafeInteger(double value, BOOL positive) { return isfinite(value) && floor(value) == value && value >= (positive ? 1 : 0) && value <= MaxSafeInteger; } BOOL validUInt16(double value, BOOL positive) { return isfinite(value) && floor(value) == value && value >= (positive ? 1 : 0) && value <= UINT16_MAX; } NSString *commandString(MLNParticleFieldRuntimeCommandResult result) { return result == MLNParticleFieldRuntimeCommandResultAccepted ? @"ACCEPTED" : @"REJECTED"; } NSString *v3String(MLNParticleHostFieldSubmitResult result) { switch (result) { case MLNParticleHostFieldSubmitResultRejectedSourceMode: return @"REJECTED_SOURCE_MODE"; case MLNParticleHostFieldSubmitResultRejectedAdapter: return @"REJECTED_ADAPTER"; case MLNParticleHostFieldSubmitResultRejectedIncompatible: return @"REJECTED_INCOMPATIBLE"; case MLNParticleHostFieldSubmitResultRejectedNonMonotonic: return @"REJECTED_NON_MONOTONIC"; case MLNParticleHostFieldSubmitResultRejectedCapacity: return @"REJECTED_CAPACITY"; case MLNParticleHostFieldSubmitResultAcceptedPendingInitialWindow: return @"ACCEPTED_PENDING_INITIAL_WINDOW"; case MLNParticleHostFieldSubmitResultInitialWindowReady: return @"INITIAL_WINDOW_READY"; case MLNParticleHostFieldSubmitResultAcceptedNextSnapshot: return @"ACCEPTED_NEXT_SNAPSHOT"; case MLNParticleHostFieldSubmitResultDuplicate: return @"DUPLICATE"; default: return @"UNKNOWN"; } } NSString *v4String(MLNParticleEncodedFieldSubmitResult result) { switch (result) { case MLNParticleEncodedFieldSubmitResultAcceptedInserted: return @"ACCEPTED_INSERTED"; case MLNParticleEncodedFieldSubmitResultAcceptedReplacedRevision: return @"ACCEPTED_REPLACED_REVISION"; case MLNParticleEncodedFieldSubmitResultDuplicate: return @"DUPLICATE"; case MLNParticleEncodedFieldSubmitResultRejectedDecode: return @"REJECTED_DECODE"; case MLNParticleEncodedFieldSubmitResultRejectedContract: return @"REJECTED_CONTRACT"; case MLNParticleEncodedFieldSubmitResultRejectedInvalidFieldKey: return @"REJECTED_INVALID_FIELD_KEY"; case MLNParticleEncodedFieldSubmitResultRejectedIdentityConflict: return @"REJECTED_IDENTITY_CONFLICT"; case MLNParticleEncodedFieldSubmitResultRejectedStaleRevision: return @"REJECTED_STALE_REVISION"; case MLNParticleEncodedFieldSubmitResultRejectedIncompatible: return @"REJECTED_INCOMPATIBLE"; case MLNParticleEncodedFieldSubmitResultRejectedCapacity: return @"REJECTED_CAPACITY"; default: return @"REJECTED_CONTRACT"; } } BOOL isWeatherObjectsNotReadyError(MLNParticleFieldRuntimeErrorCode code) { switch (code) { case MLNParticleFieldRuntimeErrorSourceMissing: case MLNParticleFieldRuntimeErrorSourceTypeMismatch: case MLNParticleFieldRuntimeErrorWeatherColorLayerMissing: case MLNParticleFieldRuntimeErrorWeatherColorLayerTypeMismatch: case MLNParticleFieldRuntimeErrorParticleLayerMissing: case MLNParticleFieldRuntimeErrorParticleLayerTypeMismatch: case MLNParticleFieldRuntimeErrorLayerSourceMismatch: return YES; default: return NO; } } NSString *attachErrorCode(NSError *error) { switch (error.code) { case MLNParticleFieldRuntimeErrorStyleNotReady: return @"STYLE_NOT_READY"; case MLNParticleFieldRuntimeErrorMapUnavailable: return @"MAP_NOT_READY"; default: return isWeatherObjectsNotReadyError((MLNParticleFieldRuntimeErrorCode)error.code) ? @"WEATHER_OBJECTS_NOT_READY" : @"NATIVE_REJECTED"; } } NSString *encodedFacadeErrorCode(NSError *error) { switch (error.code) { case MLNParticleFieldRuntimeErrorInvalidArgument: return @"INVALID_ARGUMENT"; case MLNParticleFieldRuntimeErrorDisposed: return @"RUNTIME_DISPOSED"; case MLNParticleFieldRuntimeErrorStyleNotReady: return @"SOURCE_MISSING"; default: return isWeatherObjectsNotReadyError((MLNParticleFieldRuntimeErrorCode)error.code) ? @"SOURCE_MISSING" : @"NATIVE_REJECTED"; } } NSDictionary *snapshot(MLNParticleFieldRuntime *runtime) { NSString *state[] = {@"DISABLED", @"WAITING_FOR_FIELDS", @"READY", @"ERROR"}; NSString *playback[] = {@"PLAY", @"PAUSE"}; NSString *fps[] = {@"AUTO", @"FPS_30", @"FPS_60"}; return @{ @"state": state[runtime.state], @"playbackState": playback[runtime.playbackState], @"targetFps": fps[runtime.targetFps], @"disposed": @(runtime.disposed) }; } } @implementation WeatherRuntimeBridge @synthesize viewRegistry_DEPRECATED = _viewRegistry_DEPRECATED; RCT_EXPORT_MODULE(WeatherRuntimeBridge) - (std::shared_ptr)getTurboModule:(const ObjCTurboModule::InitParams &)params { return std::make_shared(params); } // RN 0.85.2 injects this registry from the current Fabric/UIManager interop layer. It is not // a Map registry: every call schedules a fresh UI block and validates the MLRN component class. - (void)withMapForTag:(double)reactTag requireRuntime:(BOOL)requireRuntime resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject action:(void (^)(MLNMapView *, MLNParticleFieldRuntime * _Nullable))action { if (!validSafeInteger(reactTag, YES)) { reject(@"INVALID_ARGUMENT", @"reactTag must be a positive integer", nil); return; } RCTViewRegistry *registry = _viewRegistry_DEPRECATED; if (!registry) { reject(@"MAP_VIEW_NOT_FOUND", @"Fabric UIManager view registry is unavailable", nil); return; } [registry addUIBlock:^(RCTViewRegistry *currentRegistry) { UIView *component = [currentRegistry viewForReactTag:@((NSInteger)reactTag)]; // MLRN's iOS pod intentionally marks its implementation headers private. Its verified // Fabric seam is this component class + contentView; use runtime checks, not private imports. if (![component isKindOfClass:NSClassFromString(@"MLRNMapViewComponentView")]) { reject(@"MAP_VIEW_NOT_FOUND", @"reactTag does not resolve to a current MLRNMapView", nil); return; } UIView *content = [component valueForKey:@"contentView"]; if (![content isKindOfClass:NSClassFromString(@"MLRNMapView")] || ![content isKindOfClass:MLNMapView.class]) { reject(@"MAP_VIEW_NOT_FOUND", @"MLRN MapView content view is unavailable", nil); return; } MLNMapView *map = (MLNMapView *)content; MLNParticleFieldRuntime *runtime = map.particleFieldRuntime; if (requireRuntime && (!runtime || runtime.disposed)) { reject(@"RUNTIME_DISPOSED", @"WeatherRuntime is not attached to this map", nil); return; } action(map, runtime); }]; } - (NSData *)readPayload:(NSString *)reference error:(NSString **)message { if (reference.length == 0) { *message = @"payload file reference is empty"; return nil; } NSURL *url = nil; if ([reference hasPrefix:@"file://"]) url = [NSURL URLWithString:reference]; else if ([reference hasPrefix:@"/"]) url = [NSURL fileURLWithPath:reference]; else { *message = @"payload file reference must use file:// or an absolute local path"; return nil; } if (!url.isFileURL) { *message = @"payload file reference must be local"; return nil; } NSNumber *regular = nil, *readable = nil, *size = nil; if (![url getResourceValue:®ular forKey:NSURLIsRegularFileKey error:nil] || !regular.boolValue || ![url getResourceValue:&readable forKey:NSURLIsReadableKey error:nil] || !readable.boolValue || ![url getResourceValue:&size forKey:NSURLFileSizeKey error:nil] || size.unsignedLongLongValue == 0 || size.unsignedLongLongValue > MaxPayloadBytes) { *message = @"payload file must be a readable regular file of 1..67108864 bytes"; return nil; } const unsigned long long expected = size.unsignedLongLongValue; NSData *data = [NSData dataWithContentsOfURL:url options:NSDataReadingMappedIfSafe error:nil]; NSNumber *after = nil; [url getResourceValue:&after forKey:NSURLFileSizeKey error:nil]; if (!data || data.length != expected || after.unsignedLongLongValue != expected) { *message = @"payload file changed while being read"; return nil; } return data; } - (MLNParticleFieldRuntimeConfiguration *)configuration { return [[MLNParticleFieldRuntimeConfiguration alloc] initWithSourceIdentifier:@"rn-weather-fields" weatherColorLayerIdentifier:@"rn-weather-color" particleLayerIdentifier:@"rn-weather-particles"]; } - (void)attach:(double)reactTag resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { [self withMapForTag:reactTag requireRuntime:NO resolve:resolve reject:reject action:^(MLNMapView *map, MLNParticleFieldRuntime *runtime) { NSError *error = nil; MLNParticleFieldRuntime *attached = [map replaceParticleFieldRuntimeWithConfiguration:self.configuration error:&error]; if (!attached) { reject(attachErrorCode(error), error.localizedDescription ?: @"WeatherRuntime attach rejected", error); return; } resolve(snapshot(attached)); }]; } - (void)detach:(double)reactTag resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { [self withMapForTag:reactTag requireRuntime:NO resolve:resolve reject:reject action:^(MLNMapView *map, MLNParticleFieldRuntime *runtime) { // Duplicate detach is a no-op, but an actual Source cleanup failure is not. if (!runtime) { resolve(nil); return; } NSError *error = nil; if (![map disposeParticleFieldRuntimeWithError:&error]) { reject(@"RUNTIME_CLEANUP_FAILED", error.localizedDescription ?: @"WeatherRuntime detach cleanup failed", error); return; } resolve(nil); }]; } - (void)getState:(double)reactTag resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { [self withMapForTag:reactTag requireRuntime:YES resolve:resolve reject:reject action:^(MLNMapView *map, MLNParticleFieldRuntime *runtime) { resolve(snapshot(runtime)); }]; } - (void)setRetainedUnderlayEnabled:(double)reactTag enabled:(BOOL)enabled resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { [self withMapForTag:reactTag requireRuntime:NO resolve:resolve reject:reject action:^(MLNMapView *map, MLNParticleFieldRuntime *runtime) { map.retainedUnderlayEnabled = enabled; resolve(@"ACCEPTED"); }]; } - (void)setContinuousLayerRange:(double)reactTag startLayerId:(NSString *)startLayerId endLayerId:(NSString *)endLayerId resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { [self withMapForTag:reactTag requireRuntime:NO resolve:resolve reject:reject action:^(MLNMapView *map, MLNParticleFieldRuntime *runtime) { resolve([map setContinuousLayerRangeFromLayerIdentifier:startLayerId toLayerIdentifier:endLayerId] ? @"ACCEPTED" : @"REJECTED"); }]; } - (void)setRenderSegmentPlanJson:(double)reactTag planJson:(NSString *)planJson resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { [self withMapForTag:reactTag requireRuntime:NO resolve:resolve reject:reject action:^(MLNMapView *map, MLNParticleFieldRuntime *runtime) { resolve([map setRenderSegmentPlanJSON:planJson] ? @"ACCEPTED" : @"REJECTED"); }]; } - (void)getRenderSegmentDiagnosticsJson:(double)reactTag resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { [self withMapForTag:reactTag requireRuntime:NO resolve:resolve reject:reject action:^(MLNMapView *map, MLNParticleFieldRuntime *runtime) { resolve(map.renderSegmentDiagnosticsJSON); }]; } - (void)getRetainedUnderlayDiagnostics:(double)reactTag resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { [self withMapForTag:reactTag requireRuntime:NO resolve:resolve reject:reject action:^(MLNMapView *map, MLNParticleFieldRuntime *runtime) { MLNRetainedUnderlayDiagnostics *value = map.retainedUnderlayDiagnostics; resolve(@{ @"enabled": @(value.enabled), @"build": @(value.build), @"reuse": @(value.reuse), @"fallback": @(value.fallback), @"invalidation": @(value.invalidation), @"lastReason": value.lastReason, @"rangeConfigured": @(value.rangeConfigured), @"rangeValid": @(value.rangeValid), @"startLayerId": value.startLayerId, @"endLayerId": value.endLayerId, @"startIndex": @(value.startIndex), @"endIndex": @(value.endIndex), @"continuousBegin": @(value.continuousBegin), @"continuousEndExclusive": @(value.continuousEndExclusive), @"bottomPresent": @(value.bottomPresent), @"bottomCacheState": value.bottomCacheState, @"bottomBuild": @(value.bottomBuild), @"bottomReuse": @(value.bottomReuse), @"bottomInvalidation": @(value.bottomInvalidation), @"bottomLastInvalidationReason": value.bottomLastInvalidationReason, @"bottomTextureCreate": @(value.bottomTextureCreate), @"bottomCompositeCreate": @(value.bottomCompositeCreate), @"bottomResourceRelease": @(value.bottomResourceRelease), @"topPresent": @(value.topPresent), @"topCacheState": value.topCacheState, @"topBuild": @(value.topBuild), @"topReuse": @(value.topReuse), @"topInvalidation": @(value.topInvalidation), @"topLastInvalidationReason": value.topLastInvalidationReason, @"topTextureCreate": @(value.topTextureCreate), @"topCompositeCreate": @(value.topCompositeCreate), @"topResourceRelease": @(value.topResourceRelease), @"rangeFallback": @(value.rangeFallback), @"fastPathEligible": @(value.fastPathEligible), @"fastPathActive": @(value.fastPathActive), @"fastPathFallbackReason": value.fastPathFallbackReason, @"fastPathHit": @(value.fastPathHit), @"fastPathMiss": @(value.fastPathMiss), @"fullCpuFrame": @(value.fullCpuFrame) }); }]; } - (void)command:(double)tag resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject action:(MLNParticleFieldRuntimeCommandResult (^)(MLNParticleFieldRuntime *, NSError **))action { [self withMapForTag:tag requireRuntime:YES resolve:resolve reject:reject action:^(MLNMapView *map, MLNParticleFieldRuntime *runtime) { NSError *error = nil; MLNParticleFieldRuntimeCommandResult result = action(runtime, &error); resolve(error ? @"REJECTED" : commandString(result)); }]; } - (void)encodedCommand:(double)tag resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject action:(MLNParticleFieldRuntimeCommandResult (^)(MLNParticleFieldRuntime *, NSError **))action { [self withMapForTag:tag requireRuntime:YES resolve:resolve reject:reject action:^(MLNMapView *map, MLNParticleFieldRuntime *runtime) { NSError *error = nil; MLNParticleFieldRuntimeCommandResult result = action(runtime, &error); if (error) reject(encodedFacadeErrorCode(error), error.localizedDescription, error); else resolve(commandString(result)); }]; } - (void)clearWindFields:(double)t resolve:(RCTPromiseResolveBlock)r reject:(RCTPromiseRejectBlock)j { [self clearFields:t resolve:r reject:j]; } - (void)clearFields:(double)t resolve:(RCTPromiseResolveBlock)r reject:(RCTPromiseRejectBlock)j { [self command:t resolve:r reject:j action:^MLNParticleFieldRuntimeCommandResult(MLNParticleFieldRuntime *x, NSError **e) { return [x clearFieldsWithError:e]; }]; } - (void)setWeatherEnabled:(double)t enabled:(BOOL)v resolve:(RCTPromiseResolveBlock)r reject:(RCTPromiseRejectBlock)j { [self command:t resolve:r reject:j action:^MLNParticleFieldRuntimeCommandResult(MLNParticleFieldRuntime *x,NSError **e){return [x setWeatherEnabled:v error:e];}]; } - (void)setWeatherColorVisible:(double)t visible:(BOOL)v resolve:(RCTPromiseResolveBlock)r reject:(RCTPromiseRejectBlock)j { [self command:t resolve:r reject:j action:^MLNParticleFieldRuntimeCommandResult(MLNParticleFieldRuntime *x,NSError **e){return [x setWeatherColorVisible:v error:e];}]; } - (void)setParticleVisible:(double)t visible:(BOOL)v resolve:(RCTPromiseResolveBlock)r reject:(RCTPromiseRejectBlock)j { [self command:t resolve:r reject:j action:^MLNParticleFieldRuntimeCommandResult(MLNParticleFieldRuntime *x,NSError **e){return [x setParticleVisible:v error:e];}]; } - (void)submitWindField:(double)t metadataJson:(NSString *)metadata payloadFileUri:(NSString *)uri resolve:(RCTPromiseResolveBlock)r reject:(RCTPromiseRejectBlock)j { if (!metadata.length || [metadata lengthOfBytesUsingEncoding:NSUTF8StringEncoding] > MaxMetadataBytes) { j(@"INVALID_ARGUMENT", @"metadataJson UTF-8 payload must be non-empty and at most 1048576 bytes", nil); return; } NSString *message = nil; NSData *data = [self readPayload:uri error:&message]; if (!data) { j(@"PAYLOAD_FILE_UNREADABLE", message, nil); return; } [self withMapForTag:t requireRuntime:YES resolve:r reject:j action:^(MLNMapView *map, MLNParticleFieldRuntime *runtime) { NSError *error = nil; MLNParticleHostFieldSubmitResult result = [runtime submitWindFieldWithMetadataJSON:metadata payload:data error:&error]; if (error) j(@"NATIVE_REJECTED", error.localizedDescription, error); else r(v3String(result)); }]; } - (void)submitField:(double)t metadataJson:(NSString *)metadata validityFileUri:(NSString *)validity scalarFileUri:(NSString *)scalar vectorFileUri:(NSString *)vector resolve:(RCTPromiseResolveBlock)r reject:(RCTPromiseRejectBlock)j { if (!metadata.length || [metadata lengthOfBytesUsingEncoding:NSUTF8StringEncoding] > MaxMetadataBytes) { j(@"INVALID_ARGUMENT", @"metadataJson UTF-8 payload must be non-empty and at most 1048576 bytes", nil); return; } NSString *message = nil; NSData *v = [self readPayload:validity error:&message]; NSData *s = scalar.length ? [self readPayload:scalar error:&message] : nil; NSData *p = vector.length ? [self readPayload:vector error:&message] : nil; if (!v || (scalar.length && !s) || (vector.length && !p) || (!s && !p) || v.length + s.length + p.length > MaxPayloadBytes) { j(@"PAYLOAD_FILE_UNREADABLE", message ?: @"payloadRefs requires scalarFileUri or vectorFileUri and at most 67108864 bytes", nil); return; } [self withMapForTag:t requireRuntime:YES resolve:r reject:j action:^(MLNMapView *map, MLNParticleFieldRuntime *runtime) { NSError *error = nil; MLNParticleHostFieldSubmitResult result = [runtime submitFieldWithMetadataJSON:metadata scalar:s vector:p validity:v error:&error]; if (error) j(@"NATIVE_REJECTED", error.localizedDescription, error); else r(v3String(result)); }]; } - (void)submitEncodedField:(double)t fieldKey:(NSString *)key fieldID:(NSString *)fieldID generation:(double)generation dataVersion:(double)dataVersion contractJson:(NSString *)contract qbinFileUri:(NSString *)uri resolve:(RCTPromiseResolveBlock)r reject:(RCTPromiseRejectBlock)j { if (!validIdentifier(key) || !validIdentifier(fieldID) || !validSafeInteger(generation, YES) || !validSafeInteger(dataVersion, NO) || !contract.length || [contract lengthOfBytesUsingEncoding:NSUTF8StringEncoding] > MaxContractBytes) { j(@"INVALID_ARGUMENT", @"invalid V4 encoded field control-plane argument", nil); return; } NSError *jsonError = nil; id json = [NSJSONSerialization JSONObjectWithData:[contract dataUsingEncoding:NSUTF8StringEncoding] options:0 error:&jsonError]; if (![json isKindOfClass:NSDictionary.class] || !validIdentifier(((NSDictionary *)json)[@"profileID"])) { j(@"INVALID_ARGUMENT", @"invalid V4 encoded field control-plane argument", nil); return; } NSString *message = nil; NSData *qbin = [self readPayload:uri error:&message]; if (!qbin) { j(@"PAYLOAD_FILE_UNREADABLE", message, nil); return; } [self withMapForTag:t requireRuntime:YES resolve:r reject:j action:^(MLNMapView *map, MLNParticleFieldRuntime *runtime) { NSError *error = nil; MLNParticleEncodedFieldSubmitResult result = [runtime submitEncodedFieldWithFieldKey:key fieldID:fieldID generation:(uint64_t)generation dataVersion:(uint64_t)dataVersion contractJSON:contract qbin:qbin error:&error]; if (error) j(encodedFacadeErrorCode(error), error.localizedDescription, error); else r(v4String(result)); }]; } - (void)setEncodedColorBinding:(double)t fieldKey:(NSString *)key semanticID:(double)semantic groupID:(double)group expectedValueClass:(NSString *)valueClass expectedUnitID:(double)unit resolve:(RCTPromiseResolveBlock)r reject:(RCTPromiseRejectBlock)j { NSInteger value = [valueClass isEqual:@"SCALAR"] ? 0 : [valueClass isEqual:@"VELOCITY_COMPONENT"] ? 1 : [valueClass isEqual:@"DIRECTION_ANGLE"] ? 2 : -1; if (!validIdentifier(key) || !validUInt16(semantic, YES) || !validUInt16(group, NO) || !validUInt16(unit, NO) || value < 0) { j(@"INVALID_ARGUMENT", @"invalid encoded color binding selector", nil); return; } [self encodedCommand:t resolve:r reject:j action:^MLNParticleFieldRuntimeCommandResult(MLNParticleFieldRuntime *x,NSError **e){ return [x setEncodedColorBindingWithFieldKey:key semanticID:semantic groupID:group expectedValueClass:(MLNParticleFieldValueClass)value expectedUnitID:unit error:e] ? MLNParticleFieldRuntimeCommandResultAccepted : MLNParticleFieldRuntimeCommandResultRejected; }]; } - (void)setEncodedParticleBinding:(double)t fieldKey:(NSString *)key mode:(NSString *)mode uSemanticID:(double)us uGroupID:(double)ug vSemanticID:(double)vs vGroupID:(double)vg directionSemanticID:(double)ds directionGroupID:(double)dg expectedUnitID:(double)unit resolve:(RCTPromiseResolveBlock)r reject:(RCTPromiseRejectBlock)j { BOOL inactiveU = us == 0 && ug == 0, inactiveV = vs == 0 && vg == 0, inactiveD = ds == 0 && dg == 0; BOOL vector = [mode isEqual:@"VECTOR_COMPONENTS"] && validUInt16(us, YES) && validUInt16(ug, NO) && validUInt16(vs, YES) && validUInt16(vg, NO) && inactiveD; BOOL direction = [mode isEqual:@"DIRECTION_ANGLE"] && inactiveU && inactiveV && validUInt16(ds, YES) && validUInt16(dg, NO); if (!validIdentifier(key) || !validUInt16(unit, NO) || (!vector && !direction)) { j(@"INVALID_ARGUMENT", @"active encoded particle selectors must be uint16 with non-zero semanticID; inactive slots are exactly zero", nil); return; } [self encodedCommand:t resolve:r reject:j action:^MLNParticleFieldRuntimeCommandResult(MLNParticleFieldRuntime *x,NSError **e){ return [x setEncodedParticleBindingWithFieldKey:key mode:(vector ? MLNParticleEncodedParticleBindingModeVectorComponents : MLNParticleEncodedParticleBindingModeDirectionAngle) uSemanticID:us uGroupID:ug vSemanticID:vs vGroupID:vg directionSemanticID:ds directionGroupID:dg expectedUnitID:unit error:e] ? MLNParticleFieldRuntimeCommandResultAccepted : MLNParticleFieldRuntimeCommandResultRejected; }]; } - (void)clearEncodedField:(double)t fieldKey:(NSString *)key resolve:(RCTPromiseResolveBlock)r reject:(RCTPromiseRejectBlock)j { if (!validIdentifier(key)) { j(@"INVALID_ARGUMENT", @"invalid fieldKey", nil); return; } [self encodedCommand:t resolve:r reject:j action:^MLNParticleFieldRuntimeCommandResult(MLNParticleFieldRuntime *x,NSError **e){return [x clearEncodedFieldWithFieldKey:key error:e] ? MLNParticleFieldRuntimeCommandResultAccepted : MLNParticleFieldRuntimeCommandResultRejected;}]; } - (void)clearAllEncodedFields:(double)t resolve:(RCTPromiseResolveBlock)r reject:(RCTPromiseRejectBlock)j { [self encodedCommand:t resolve:r reject:j action:^MLNParticleFieldRuntimeCommandResult(MLNParticleFieldRuntime *x,NSError **e){return [x clearAllEncodedFieldsWithError:e] ? MLNParticleFieldRuntimeCommandResultAccepted : MLNParticleFieldRuntimeCommandResultRejected;}]; } - (void)setEncodedForecastTime:(double)t timestampMs:(double)v resolve:(RCTPromiseResolveBlock)r reject:(RCTPromiseRejectBlock)j { if (!validSafeInteger(v, NO)) { j(@"INVALID_ARGUMENT", @"encoded forecast time must be safe integer",nil); return; } [self encodedCommand:t resolve:r reject:j action:^MLNParticleFieldRuntimeCommandResult(MLNParticleFieldRuntime *x,NSError **e){return [x setEncodedForecastTime:v error:e] ? MLNParticleFieldRuntimeCommandResultAccepted : MLNParticleFieldRuntimeCommandResultRejected;}]; } - (void)setEncodedForecastPlaybackState:(double)t playback:(NSString *)v resolve:(RCTPromiseResolveBlock)r reject:(RCTPromiseRejectBlock)j { if (!([v isEqual:@"PLAY"] || [v isEqual:@"PAUSE"])) { j(@"INVALID_ARGUMENT", @"invalid encoded playback",nil); return; } [self encodedCommand:t resolve:r reject:j action:^MLNParticleFieldRuntimeCommandResult(MLNParticleFieldRuntime *x,NSError **e){return [x setEncodedForecastPlaybackState:[v isEqual:@"PLAY"] error:e] ? MLNParticleFieldRuntimeCommandResultAccepted : MLNParticleFieldRuntimeCommandResultRejected;}]; } - (void)setEncodedForecastPlaybackSpeed:(double)t speed:(double)v resolve:(RCTPromiseResolveBlock)r reject:(RCTPromiseRejectBlock)j { if (!isfinite(v) || v <= 0 || v > FLT_MAX) { j(@"INVALID_ARGUMENT", @"invalid encoded playback speed",nil); return; } [self encodedCommand:t resolve:r reject:j action:^MLNParticleFieldRuntimeCommandResult(MLNParticleFieldRuntime *x,NSError **e){return [x setEncodedForecastPlaybackSpeed:v error:e] ? MLNParticleFieldRuntimeCommandResultAccepted : MLNParticleFieldRuntimeCommandResultRejected;}]; } - (void)setTargetFps:(double)t target:(NSString *)v resolve:(RCTPromiseResolveBlock)r reject:(RCTPromiseRejectBlock)j { MLNParticleFieldRuntimeTargetFps x = [v isEqual:@"AUTO"] ? MLNParticleFieldRuntimeTargetFpsAuto : [v isEqual:@"FPS_30"] ? MLNParticleFieldRuntimeTargetFps30 : [v isEqual:@"FPS_60"] ? MLNParticleFieldRuntimeTargetFps60 : (MLNParticleFieldRuntimeTargetFps)99; if (x == 99) { j(@"INVALID_ARGUMENT", @"unsupported enum value",nil); return; } [self command:t resolve:r reject:j action:^MLNParticleFieldRuntimeCommandResult(MLNParticleFieldRuntime *q,NSError **e){return [q setTargetFps:x error:e];}]; } - (void)setForecastTime:(double)t timestampMs:(double)v resolve:(RCTPromiseResolveBlock)r reject:(RCTPromiseRejectBlock)j { if (!validSafeInteger(v, NO)) { j(@"INVALID_ARGUMENT", @"forecast timestamp must be a non-negative epoch millisecond integer",nil); return; } [self command:t resolve:r reject:j action:^MLNParticleFieldRuntimeCommandResult(MLNParticleFieldRuntime *q,NSError **e){return [q setForecastTime:v error:e];}]; } - (void)setPlaybackState:(double)t playback:(NSString *)v resolve:(RCTPromiseResolveBlock)r reject:(RCTPromiseRejectBlock)j { MLNParticleFieldRuntimePlaybackState x = [v isEqual:@"PLAY"] ? MLNParticleFieldRuntimePlaybackStatePlay : [v isEqual:@"PAUSE"] ? MLNParticleFieldRuntimePlaybackStatePause : (MLNParticleFieldRuntimePlaybackState)99; if (x == 99) { j(@"INVALID_ARGUMENT", @"unsupported enum value",nil); return; } [self command:t resolve:r reject:j action:^MLNParticleFieldRuntimeCommandResult(MLNParticleFieldRuntime *q,NSError **e){return [q setPlaybackState:x error:e];}]; } - (void)setPlaybackSpeed:(double)t speed:(NSString *)v resolve:(RCTPromiseResolveBlock)r reject:(RCTPromiseRejectBlock)j { float x = [v isEqual:@"HALF"] ? .5f : [v isEqual:@"NORMAL"] ? 1.f : [v isEqual:@"DOUBLE"] ? 2.f : -1.f; if (x < 0) { j(@"INVALID_ARGUMENT", @"unsupported enum value",nil); return; } [self command:t resolve:r reject:j action:^MLNParticleFieldRuntimeCommandResult(MLNParticleFieldRuntime *q,NSError **e){return [q setPlaybackSpeed:x error:e];}]; } - (void)setFieldValueSource:(double)t source:(NSString *)v resolve:(RCTPromiseResolveBlock)r reject:(RCTPromiseRejectBlock)j { MLNParticleFieldValueSource x = [v isEqual:@"SCALAR"] ? MLNParticleFieldValueSourceScalar : [v isEqual:@"VECTOR_MAGNITUDE"] ? MLNParticleFieldValueSourceVectorMagnitude : (MLNParticleFieldValueSource)99; if (x == 99) { j(@"INVALID_ARGUMENT", @"unsupported enum value",nil); return; } [self command:t resolve:r reject:j action:^MLNParticleFieldRuntimeCommandResult(MLNParticleFieldRuntime *q,NSError **e){return [q setFieldValueSource:x error:e];}]; } - (void)setParticleAdvectionScale:(double)t scale:(double)v resolve:(RCTPromiseResolveBlock)r reject:(RCTPromiseRejectBlock)j { if (!isfinite(v) || v <= 0) { j(@"INVALID_ARGUMENT", @"advectionScale must be finite and > 0",nil); return; } [self command:t resolve:r reject:j action:^MLNParticleFieldRuntimeCommandResult(MLNParticleFieldRuntime *q,NSError **e){return [q setParticleAdvectionScale:v error:e];}]; } - (void)floatCommand:(double)t value:(double)v resolve:(RCTPromiseResolveBlock)r reject:(RCTPromiseRejectBlock)j action:(MLNParticleFieldRuntimeCommandResult (^)(MLNParticleFieldRuntime *, float, NSError **))a { if (!isfinite(v)) { j(@"INVALID_ARGUMENT", @"value must be finite",nil); return; } [self command:t resolve:r reject:j action:^MLNParticleFieldRuntimeCommandResult(MLNParticleFieldRuntime *q,NSError **e){return a(q, v, e);}]; } - (void)setWeatherColorOpacity:(double)t value:(double)v resolve:(RCTPromiseResolveBlock)r reject:(RCTPromiseRejectBlock)j { [self floatCommand:t value:v resolve:r reject:j action:^MLNParticleFieldRuntimeCommandResult(MLNParticleFieldRuntime *q,float x,NSError **e){return [q setWeatherColorOpacity:x error:e];}]; } - (void)setParticleLineWidth:(double)t value:(double)v resolve:(RCTPromiseResolveBlock)r reject:(RCTPromiseRejectBlock)j { [self floatCommand:t value:v resolve:r reject:j action:^MLNParticleFieldRuntimeCommandResult(MLNParticleFieldRuntime *q,float x,NSError **e){return [q setParticleLineWidth:x error:e];}]; } - (void)setParticleLength:(double)t value:(double)v resolve:(RCTPromiseResolveBlock)r reject:(RCTPromiseRejectBlock)j { [self floatCommand:t value:v resolve:r reject:j action:^MLNParticleFieldRuntimeCommandResult(MLNParticleFieldRuntime *q,float x,NSError **e){return [q setParticleLength:x error:e];}]; } - (void)setParticleOpacity:(double)t value:(double)v resolve:(RCTPromiseResolveBlock)r reject:(RCTPromiseRejectBlock)j { [self floatCommand:t value:v resolve:r reject:j action:^MLNParticleFieldRuntimeCommandResult(MLNParticleFieldRuntime *q,float x,NSError **e){return [q setParticleOpacity:x error:e];}]; } - (void)setTrailOpacity:(double)t value:(double)v resolve:(RCTPromiseResolveBlock)r reject:(RCTPromiseRejectBlock)j { [self floatCommand:t value:v resolve:r reject:j action:^MLNParticleFieldRuntimeCommandResult(MLNParticleFieldRuntime *q,float x,NSError **e){return [q setTrailOpacity:x error:e];}]; } - (void)setTrailFade:(double)t value:(double)v resolve:(RCTPromiseResolveBlock)r reject:(RCTPromiseRejectBlock)j { [self floatCommand:t value:v resolve:r reject:j action:^MLNParticleFieldRuntimeCommandResult(MLNParticleFieldRuntime *q,float x,NSError **e){return [q setTrailFade:x error:e];}]; } - (void)setParticleCount:(double)t count:(double)v resolve:(RCTPromiseResolveBlock)r reject:(RCTPromiseRejectBlock)j { if (!isfinite(v) || v < 0 || v > 2147483647.0 || floor(v) != v) { j(@"INVALID_ARGUMENT", @"particle count must be a non-negative integer",nil); return; } [self command:t resolve:r reject:j action:^MLNParticleFieldRuntimeCommandResult(MLNParticleFieldRuntime *q,NSError **e){return [q setParticleCount:v error:e];}]; } - (void)colorCommand:(double)t red:(double)red green:(double)green blue:(double)blue alpha:(double)alpha resolve:(RCTPromiseResolveBlock)r reject:(RCTPromiseRejectBlock)j action:(MLNParticleFieldRuntimeCommandResult (^)(MLNParticleFieldRuntime *, float, float, float, float, NSError **))a { if (!isfinite(red)||!isfinite(green)||!isfinite(blue)||!isfinite(alpha)||red<0||red>1||green<0||green>1||blue<0||blue>1||alpha<0||alpha>1) { j(@"INVALID_ARGUMENT", @"RGBA must be finite values in [0,1]",nil); return; } [self command:t resolve:r reject:j action:^MLNParticleFieldRuntimeCommandResult(MLNParticleFieldRuntime *q,NSError **e){return a(q,red,green,blue,alpha,e);}]; } - (void)setParticleColor:(double)t red:(double)red green:(double)green blue:(double)blue alpha:(double)alpha resolve:(RCTPromiseResolveBlock)r reject:(RCTPromiseRejectBlock)j { [self colorCommand:t red:red green:green blue:blue alpha:alpha resolve:r reject:j action:^MLNParticleFieldRuntimeCommandResult(MLNParticleFieldRuntime *q,float a,float b,float c,float d,NSError **e){return [q setParticleColorRed:a green:b blue:c alpha:d error:e];}]; } - (void)setTrailColor:(double)t red:(double)red green:(double)green blue:(double)blue alpha:(double)alpha resolve:(RCTPromiseResolveBlock)r reject:(RCTPromiseRejectBlock)j { [self colorCommand:t red:red green:green blue:blue alpha:alpha resolve:r reject:j action:^MLNParticleFieldRuntimeCommandResult(MLNParticleFieldRuntime *q,float a,float b,float c,float d,NSError **e){return [q setTrailColorRed:a green:b blue:c alpha:d error:e];}]; } - (void)setWeatherColorStops:(double)t stops:(NSArray *)stops resolve:(RCTPromiseResolveBlock)r reject:(RCTPromiseRejectBlock)j { NSMutableArray *native = NSMutableArray.array; float previous = -INFINITY; for (id entry in stops) { if (![entry isKindOfClass:NSDictionary.class]) { j(@"INVALID_ARGUMENT", @"color stop must be an object",nil); return; } NSDictionary *d = entry; NSNumber *speed=d[@"speedMps"],*red=d[@"red"],*green=d[@"green"],*blue=d[@"blue"],*alpha=d[@"alpha"]; if (!speed||!red||!green||!blue||!alpha||!isfinite(speed.floatValue)||speed.floatValue<=previous||!isfinite(red.floatValue)||!isfinite(green.floatValue)||!isfinite(blue.floatValue)||!isfinite(alpha.floatValue)||red.floatValue<0||red.floatValue>1||green.floatValue<0||green.floatValue>1||blue.floatValue<0||blue.floatValue>1||alpha.floatValue<0||alpha.floatValue>1) { j(@"INVALID_ARGUMENT", @"stops require finite, strictly increasing speedMps and finite RGBA in [0,1]",nil); return; } previous=speed.floatValue; [native addObject:[[MLNParticleWeatherColorStop alloc] initWithValue:previous red:red.floatValue green:green.floatValue blue:blue.floatValue alpha:alpha.floatValue]]; } [self command:t resolve:r reject:j action:^MLNParticleFieldRuntimeCommandResult(MLNParticleFieldRuntime *q,NSError **e){return [q setWeatherColorStops:native error:e];}]; } @end