diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index a0d720bb1e..3eed2c8364 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -6,6 +6,9 @@ concurrency: on: [pull_request] +env: + UNITY_LICENSE: ${{ secrets.UNITY_LICENSE }} + jobs: build: runs-on: ubuntu-latest @@ -46,6 +49,7 @@ jobs: Ruby31, AppleSwift56, Swift56, + Unity2021, WebChromium, WebNode ] diff --git a/example.php b/example.php index bbc2180ac8..0452b1e2c9 100644 --- a/example.php +++ b/example.php @@ -21,6 +21,7 @@ use Appwrite\SDK\Language\Android; use Appwrite\SDK\Language\Kotlin; use Appwrite\SDK\Language\ReactNative; +use Appwrite\SDK\Language\Unity; use Appwrite\SDK\Language\Markdown; use Appwrite\SDK\Language\AgentSkills; @@ -121,6 +122,13 @@ function configureSDK($sdk, $overrides = []) { $sdk->generate(__DIR__ . '/examples/php'); } + // Unity + if (!$requestedSdk || $requestedSdk === 'unity') { + $sdk = new SDK(new Unity(), new Swagger2($spec)); + configureSDK($sdk); + $sdk->generate(__DIR__ . '/examples/unity'); + } + // Web if (!$requestedSdk || $requestedSdk === 'web') { $sdk = new SDK(new Web(), new Swagger2($spec)); diff --git a/src/SDK/Language/DotNet.php b/src/SDK/Language/DotNet.php index 1a3c64079e..5eb25ad4d0 100644 --- a/src/SDK/Language/DotNet.php +++ b/src/SDK/Language/DotNet.php @@ -522,6 +522,12 @@ public function getFilters(): array new TwigFilter('propertyType', function (array $property, array $spec = []) { return $this->getPropertyType($property, $spec); }), + new TwigFilter('propertyAssignment', function (array $property) { + return $this->getPropertyAssignment($property); + }), + new TwigFilter('toMapValue', function (array $property, string $definitionName) { + return $this->getToMapExpression($property, $definitionName); + }), new TwigFilter('enumExample', function (array $param) { $enumValues = $param['enumValues'] ?? []; if (empty($enumValues)) { @@ -580,7 +586,7 @@ public function getFilters(): array * @param array $spec * @return string */ - protected function getPropertyType(array $property, array $spec = []): string + protected function getPropertyType(array $property, array $spec = [], bool $fullyQualified = true): string { if (isset($property['sub_schema']) && !empty($property['sub_schema'])) { $type = $this->toPascalCase($property['sub_schema']); @@ -593,7 +599,8 @@ protected function getPropertyType(array $property, array $spec = []): string if (isset($property['enum']) && !empty($property['enum'])) { $enumName = $property['enumName'] ?? $property['name']; - return 'Appwrite.Enums.' . $this->toPascalCase($enumName); + $prefix = $fullyQualified ? 'Appwrite.Enums.' : ''; + return $prefix . $this->toPascalCase($enumName); } return $this->getTypeName($property, $spec); @@ -607,39 +614,171 @@ public function getFunctions(): array { return [ new TwigFunction('sub_schema', function (array $property) { - $result = ''; - - if (isset($property['sub_schema']) && !empty($property['sub_schema'])) { - if ($property['type'] === 'array') { - $result = 'List<' . $this->toPascalCase($property['sub_schema']) . '>'; - } else { - $result = $this->toPascalCase($property['sub_schema']); - } - } elseif (isset($property['enum']) && !empty($property['enum'])) { - $enumName = $property['enumName'] ?? $property['name']; - $result = $this->toPascalCase($enumName); - } else { - $result = $this->getTypeName($property); - } + $result = $this->getPropertyType($property, [], false); if (!($property['required'] ?? true)) { $result .= '?'; } return $result; - }), + }, ['is_safe' => ['html']]), new TwigFunction('property_name', function (array $definition, array $property) { - $name = $property['name']; - $name = \str_replace('$', '', $name); - $name = $this->toPascalCase($name); - if (\in_array($name, $this->getKeywords())) { - $name = '@' . $name; - } - return $name; + return $this->getPropertyName($property); }), ]; } + /** + * Generate property name for C# model + * + * @param array $property + * @return string + */ + protected function getPropertyName(array $property): string + { + $name = $property['name']; + $name = \str_replace('$', '', $name); + $name = $this->toPascalCase($name); + if (\in_array($name, $this->getKeywords())) { + $name = '@' . $name; + } + return $name; + } + + /** + * Resolved property name with overrides applied + * + * @param array $property + * @param string $definitionName + * @return string + */ + protected function getResolvedPropertyName(array $property, string $definitionName): string + { + $name = $this->getPropertyName($property); + $overrides = $this->getPropertyOverrides(); + if (isset($overrides[$definitionName][$name])) { + return $overrides[$definitionName][$name]; + } + return $name; + } + + /** + * Generate full property assignment expression for model deserialization (From method). + * Handles TryGetValue wrapping for optional properties internally. + * + * @param array $property + * @return string + */ + protected function getPropertyAssignment(array $property): string + { + $required = $property['required'] ?? false; + $propertyName = $property['name']; + $mapAccess = "map[\"{$propertyName}\"]"; + + if ($required) { + return $this->convertValue($property, $mapAccess); + } + + $v = 'v' . $this->toPascalCase(\str_replace('$', '', $propertyName)); + $tryGet = "map.TryGetValue(\"{$propertyName}\", out var {$v})"; + + // Sub_schema objects — use pattern matching for type-safe cast + if (!empty($property['sub_schema']) && $property['type'] !== 'array') { + $subSchema = $this->toPascalCase($property['sub_schema']); + return "{$tryGet} && {$v} is Dictionary {$v}Map ? {$subSchema}.From(map: {$v}Map) : null"; + } + + // Integer, number, enum — guard with null check to avoid Convert/constructor on null + if (\in_array($property['type'], ['integer', 'number']) || !empty($property['enum'])) { + $expr = $this->convertValue($property, $v); + return "{$tryGet} && {$v} != null ? {$expr} : null"; + } + + // String, boolean, arrays — null-safe conversion + $expr = $this->convertValue($property, $v, false); + return "{$tryGet} ? {$expr} : null"; + } + + /** + * Build type conversion expression for a single value. + * + * @param array $property Property definition + * @param string $src Source variable or expression + * @param bool $srcNonNull Whether $src is guaranteed non-null + * @return string + */ + private function convertValue(array $property, string $src, bool $srcNonNull = true): string + { + // Sub_schema (nested objects) + if (!empty($property['sub_schema'])) { + $subSchema = $this->toPascalCase($property['sub_schema']); + if ($property['type'] === 'array') { + return "{$src}.ToEnumerable().Select(it => {$subSchema}.From(map: (Dictionary)it)).ToList()"; + } + return "{$subSchema}.From(map: (Dictionary){$src})"; + } + + // Enum + if (!empty($property['enum'])) { + $enumClass = $this->toPascalCase($property['enumName'] ?? $property['name']); + return "new {$enumClass}({$src}.ToString())"; + } + + // Arrays + if ($property['type'] === 'array') { + $itemsType = $property['items']['type'] ?? 'object'; + $selectExpression = match ($itemsType) { + 'string' => 'x.ToString()', + 'integer' => 'Convert.ToInt64(x)', + 'number' => 'Convert.ToDouble(x)', + 'boolean' => '(bool)x', + default => 'x' + }; + return "{$src}.ToEnumerable().Select(x => {$selectExpression}).ToList()"; + } + + // Integer/Number + if ($property['type'] === 'integer' || $property['type'] === 'number') { + $convertMethod = $property['type'] === 'integer' ? 'Int64' : 'Double'; + return "Convert.To{$convertMethod}({$src})"; + } + + // Boolean + if ($property['type'] === 'boolean') { + return $srcNonNull ? "(bool){$src}" : "(bool?){$src}"; + } + + // String (default) + return $srcNonNull ? "{$src}.ToString()" : "{$src}?.ToString()"; + } + + /** + * Generate ToMap() value expression for a property. + * + * @param array $property + * @param string $definitionName + * @return string + */ + protected function getToMapExpression(array $property, string $definitionName): string + { + $propName = $this->getResolvedPropertyName($property, $definitionName); + $required = $property['required'] ?? true; + $nullOp = $required ? '' : '?'; + + if (!empty($property['sub_schema'])) { + if ($property['type'] === 'array') { + return "{$propName}{$nullOp}.Select(it => it.ToMap()).ToList()"; + } + return "{$propName}{$nullOp}.ToMap()"; + } + + if (!empty($property['enum'])) { + return "{$propName}{$nullOp}.Value"; + } + + return $propName; + } + /** * Format a PHP array as a C# anonymous object */ diff --git a/src/SDK/Language/Unity.php b/src/SDK/Language/Unity.php new file mode 100644 index 0000000000..f47ae2ae22 --- /dev/null +++ b/src/SDK/Language/Unity.php @@ -0,0 +1,424 @@ + 'default', + 'destination' => 'CHANGELOG.md', + 'template' => 'unity/CHANGELOG.md.twig', + ], + [ + 'scope' => 'copy', + 'destination' => '/icon.png', + 'template' => 'unity/icon.png', + ], + [ + 'scope' => 'default', + 'destination' => 'LICENSE', + 'template' => 'unity/LICENSE.twig', + ], + [ + 'scope' => 'default', + 'destination' => 'README.md', + 'template' => 'unity/README.md.twig', + ], + [ + 'scope' => 'method', + 'destination' => 'Assets/docs~/examples/{{service.name | caseLower}}/{{method.name | caseDash}}.md', + 'template' => 'unity/docs/example.md.twig', + ], + [ + 'scope' => 'default', + 'destination' => 'Assets/package.json', + 'template' => 'unity/package.json.twig', + ], + [ + 'scope' => 'default', + 'destination' => 'Assets/Runtime/{{ spec.title | caseUcfirst }}.asmdef', + 'template' => 'unity/Assets/Runtime/Appwrite.asmdef.twig', + ], + // Appwrite + [ + 'scope' => 'default', + 'destination' => 'Assets/Runtime/{{ spec.title | caseUcfirst }}Config.cs', + 'template' => 'unity/Assets/Runtime/AppwriteConfig.cs.twig', + ], + [ + 'scope' => 'default', + 'destination' => 'Assets/Runtime/{{ spec.title | caseUcfirst }}Manager.cs', + 'template' => 'unity/Assets/Runtime/AppwriteManager.cs.twig', + ], + [ + 'scope' => 'default', + 'destination' => 'Assets/Runtime/Realtime.cs', + 'template' => 'unity/Assets/Runtime/Realtime.cs.twig', + ], + [ + 'scope' => 'default', + 'destination' => 'Assets/Runtime/Channel.cs', + 'template' => 'unity/Assets/Runtime/Channel.cs.twig', + ], + [ + 'scope' => 'default', + 'destination' => 'Assets/Runtime/Utilities/{{ spec.title | caseUcfirst }}Utilities.cs', + 'template' => 'unity/Assets/Runtime/Utilities/AppwriteUtilities.cs.twig', + ], + // Appwrite.Core + [ + 'scope' => 'copy', + 'destination' => 'Assets/Runtime/Core/csc.rsp', + 'template' => 'unity/Assets/Runtime/Core/csc.rsp', + ], + [ + 'scope' => 'default', + 'destination' => 'Assets/Runtime/Core/{{ spec.title | caseUcfirst }}.Core.asmdef', + 'template' => 'unity/Assets/Runtime/Core/Appwrite.Core.asmdef.twig', + ], + [ + 'scope' => 'default', + 'destination' => 'Assets/Runtime/Core/Client.cs', + 'template' => 'unity/Assets/Runtime/Core/Client.cs.twig', + ], + [ + 'scope' => 'default', + 'destination' => 'Assets/Runtime/Core/{{ spec.title | caseUcfirst }}Exception.cs', + 'template' => 'dotnet/Package/Exception.cs.twig', + ], + [ + 'scope' => 'default', + 'destination' => 'Assets/Runtime/Core/ID.cs', + 'template' => 'dotnet/Package/ID.cs.twig', + ], + [ + 'scope' => 'default', + 'destination' => 'Assets/Runtime/Core/Permission.cs', + 'template' => 'dotnet/Package/Permission.cs.twig', + ], + [ + 'scope' => 'default', + 'destination' => 'Assets/Runtime/Core/Query.cs', + 'template' => 'dotnet/Package/Query.cs.twig', + ], + [ + 'scope' => 'default', + 'destination' => 'Assets/Runtime/Core/Role.cs', + 'template' => 'dotnet/Package/Role.cs.twig', + ], + [ + 'scope' => 'default', + 'destination' => 'Assets/Runtime/Core/Operator.cs', + 'template' => 'dotnet/Package/Operator.cs.twig', + ], + [ + 'scope' => 'default', + 'destination' => 'Assets/Runtime/Core/CookieContainer.cs', + 'template' => 'unity/Assets/Runtime/Core/CookieContainer.cs.twig', + ], + [ + 'scope' => 'default', + 'destination' => 'Assets/Runtime/Core/Converters/ValueClassConverter.cs', + 'template' => 'dotnet/Package/Converters/ValueClassConverter.cs.twig', + ], + [ + 'scope' => 'default', + 'destination' => 'Assets/Runtime/Core/Converters/ObjectToInferredTypesConverter.cs', + 'template' => 'dotnet/Package/Converters/ObjectToInferredTypesConverter.cs.twig', + ], + [ + 'scope' => 'default', + 'destination' => 'Assets/Runtime/Core/Extensions/Extensions.cs', + 'template' => 'dotnet/Package/Extensions/Extensions.cs.twig', + ], + [ + 'scope' => 'default', + 'destination' => 'Assets/Runtime/Core/Models/OrderType.cs', + 'template' => 'dotnet/Package/Models/OrderType.cs.twig', + ], + [ + 'scope' => 'default', + 'destination' => 'Assets/Runtime/Core/Models/UploadProgress.cs', + 'template' => 'dotnet/Package/Models/UploadProgress.cs.twig', + ], + [ + 'scope' => 'default', + 'destination' => 'Assets/Runtime/Core/Models/InputFile.cs', + 'template' => 'dotnet/Package/Models/InputFile.cs.twig', + ], + [ + 'scope' => 'default', + 'destination' => 'Assets/Runtime/Core/Services/Service.cs', + 'template' => 'dotnet/Package/Services/Service.cs.twig', + ], + [ + 'scope' => 'service', + 'destination' => 'Assets/Runtime/Core/Services/{{service.name | caseUcfirst}}.cs', + 'template' => 'unity/Assets/Runtime/Core/Services/ServiceTemplate.cs.twig', + ], + [ + 'scope' => 'definition', + 'destination' => 'Assets/Runtime/Core/Models/{{ definition.name | caseUcfirst | overrideIdentifier }}.cs', + 'template' => 'dotnet/Package/Models/Model.cs.twig', + ], + [ + 'scope' => 'requestModel', + 'destination' => 'Assets/Runtime/Core/Models/{{ requestModel.name | caseUcfirst | overrideIdentifier }}.cs', + 'template' => 'dotnet/Package/Models/RequestModel.cs.twig', + ], + [ + 'scope' => 'enum', + 'destination' => 'Assets/Runtime/Core/Enums/{{ enum.name | caseUcfirst | overrideIdentifier }}.cs', + 'template' => 'dotnet/Package/Enums/Enum.cs.twig', + ], + [ + 'scope' => 'default', + 'destination' => 'Assets/Runtime/Core/WebAuthComponent.cs', + 'template' => 'unity/Assets/Runtime/Core/WebAuthComponent.cs.twig', + ], + [ + 'scope' => 'default', + 'destination' => 'Assets/Runtime/Core/Enums/IEnum.cs', + 'template' => 'dotnet/Package/Enums/IEnum.cs.twig', + ], + // Plugins + [ + 'scope' => 'copy', + 'destination' => 'Assets/Runtime/Core/Plugins/Microsoft.Bcl.AsyncInterfaces.dll', + 'template' => 'unity/Assets/Runtime/Core/Plugins/Microsoft.Bcl.AsyncInterfaces.dll', + ], + [ + 'scope' => 'copy', + 'destination' => 'Assets/Runtime/Core/Plugins/System.IO.Pipelines.dll', + 'template' => 'unity/Assets/Runtime/Core/Plugins/System.IO.Pipelines.dll', + ], + [ + 'scope' => 'copy', + 'destination' => 'Assets/Runtime/Core/Plugins/System.Runtime.CompilerServices.Unsafe.dll', + 'template' => 'unity/Assets/Runtime/Core/Plugins/System.Runtime.CompilerServices.Unsafe.dll', + ], + [ + 'scope' => 'copy', + 'destination' => 'Assets/Runtime/Core/Plugins/System.Text.Encodings.Web.dll', + 'template' => 'unity/Assets/Runtime/Core/Plugins/System.Text.Encodings.Web.dll', + ], + [ + 'scope' => 'copy', + 'destination' => 'Assets/Runtime/Core/Plugins/Android/AndroidManifest.xml', + 'template' => 'unity/Assets/Runtime/Core/Plugins/Android/AndroidManifest.xml', + ], + [ + 'scope' => 'copy', + 'destination' => 'Assets/Runtime/Core/Plugins/WebGLCookies.jslib', + 'template' => 'unity/Assets/Runtime/Core/Plugins/WebGLCookies.jslib', + ], + [ + 'scope' => 'copy', + 'destination' => 'Assets/Runtime/Core/Plugins/System.Text.Json.dll', + 'template' => 'unity/Assets/Runtime/Core/Plugins/System.Text.Json.dll', + ], + // Appwrite.Editor + [ + 'scope' => 'default', + 'destination' => 'Assets/Editor/{{ spec.title | caseUcfirst }}.Editor.asmdef', + 'template' => 'unity/Assets/Editor/Appwrite.Editor.asmdef.twig', + ], + [ + 'scope' => 'default', + 'destination' => 'Assets/Editor/{{ spec.title | caseUcfirst }}SetupAssistant.cs', + 'template' => 'unity/Assets/Editor/AppwriteSetupAssistant.cs.twig', + ], + [ + 'scope' => 'default', + 'destination' => 'Assets/Editor/{{ spec.title | caseUcfirst }}SetupWindow.cs', + 'template' => 'unity/Assets/Editor/AppwriteSetupWindow.cs.twig', + ], + // Samples + [ + 'scope' => 'default', + 'destination' => 'Assets/Samples~/{{ spec.title | caseUcfirst }}Example/{{ spec.title | caseUcfirst }}Example.cs', + 'template' => 'unity/Assets/Samples~/AppwriteExample/AppwriteExample.cs.twig', + ], + // Packages + [ + 'scope' => 'copy', + 'destination' => 'Packages/manifest.json', + 'template' => 'unity/Packages/manifest.json', + ], + [ + 'scope' => 'copy', + 'destination' => 'Packages/packages-lock.json', + 'template' => 'unity/Packages/packages-lock.json', + ], + // ProjectSettings + [ + 'scope' => 'copy', + 'destination' => 'ProjectSettings/AudioManager.asset', + 'template' => 'unity/ProjectSettings/AudioManager.asset', + ], + [ + 'scope' => 'copy', + 'destination' => 'ProjectSettings/boot.config', + 'template' => 'unity/ProjectSettings/boot.config', + ], + [ + 'scope' => 'copy', + 'destination' => 'ProjectSettings/ClusterInputManager.asset', + 'template' => 'unity/ProjectSettings/ClusterInputManager.asset', + ], + [ + 'scope' => 'copy', + 'destination' => 'ProjectSettings/DynamicsManager.asset', + 'template' => 'unity/ProjectSettings/DynamicsManager.asset', + ], + [ + 'scope' => 'copy', + 'destination' => 'ProjectSettings/EditorBuildSettings.asset', + 'template' => 'unity/ProjectSettings/EditorBuildSettings.asset', + ], + [ + 'scope' => 'copy', + 'destination' => 'ProjectSettings/EditorSettings.asset', + 'template' => 'unity/ProjectSettings/EditorSettings.asset', + ], + [ + 'scope' => 'copy', + 'destination' => 'ProjectSettings/GraphicsSettings.asset', + 'template' => 'unity/ProjectSettings/GraphicsSettings.asset', + ], + [ + 'scope' => 'copy', + 'destination' => 'ProjectSettings/InputManager.asset', + 'template' => 'unity/ProjectSettings/InputManager.asset', + ], + [ + 'scope' => 'copy', + 'destination' => 'ProjectSettings/MemorySettings.asset', + 'template' => 'unity/ProjectSettings/MemorySettings.asset', + ], + [ + 'scope' => 'copy', + 'destination' => 'ProjectSettings/NavMeshAreas.asset', + 'template' => 'unity/ProjectSettings/NavMeshAreas.asset', + ], + [ + 'scope' => 'copy', + 'destination' => 'ProjectSettings/NetworkManager.asset', + 'template' => 'unity/ProjectSettings/NetworkManager.asset', + ], + [ + 'scope' => 'copy', + 'destination' => 'ProjectSettings/PackageManagerSettings.asset', + 'template' => 'unity/ProjectSettings/PackageManagerSettings.asset', + ], + [ + 'scope' => 'copy', + 'destination' => 'ProjectSettings/Physics2DSettings.asset', + 'template' => 'unity/ProjectSettings/Physics2DSettings.asset', + ], + [ + 'scope' => 'copy', + 'destination' => 'ProjectSettings/PresetManager.asset', + 'template' => 'unity/ProjectSettings/PresetManager.asset', + ], + [ + 'scope' => 'copy', + 'destination' => 'ProjectSettings/ProjectSettings.asset', + 'template' => 'unity/ProjectSettings/ProjectSettings.asset', + ], + [ + 'scope' => 'copy', + 'destination' => 'ProjectSettings/ProjectVersion.txt', + 'template' => 'unity/ProjectSettings/ProjectVersion.txt', + ], + [ + 'scope' => 'copy', + 'destination' => 'ProjectSettings/QualitySettings.asset', + 'template' => 'unity/ProjectSettings/QualitySettings.asset', + ], + [ + 'scope' => 'copy', + 'destination' => 'ProjectSettings/TagManager.asset', + 'template' => 'unity/ProjectSettings/TagManager.asset', + ], + [ + 'scope' => 'copy', + 'destination' => 'ProjectSettings/TimeManager.asset', + 'template' => 'unity/ProjectSettings/TimeManager.asset', + ], + [ + 'scope' => 'copy', + 'destination' => 'ProjectSettings/UnityConnectSettings.asset', + 'template' => 'unity/ProjectSettings/UnityConnectSettings.asset', + ], + [ + 'scope' => 'copy', + 'destination' => 'ProjectSettings/VersionControlSettings.asset', + 'template' => 'unity/ProjectSettings/VersionControlSettings.asset', + ], + [ + 'scope' => 'copy', + 'destination' => 'ProjectSettings/VFXManager.asset', + 'template' => 'unity/ProjectSettings/VFXManager.asset', + ], + [ + 'scope' => 'copy', + 'destination' => 'ProjectSettings/XRSettings.asset', + 'template' => 'unity/ProjectSettings/XRSettings.asset', + ], + ]; + + // Filter out problematic files in test mode + // Check if we're in test mode by looking for a global variable + if (isset($GLOBALS['UNITY_TEST_MODE']) && $GLOBALS['UNITY_TEST_MODE'] === true) { + $excludeInTest = [ + 'Assets/Runtime/Utilities/{{ spec.title | caseUcfirst }}Utilities.cs', + 'Assets/Runtime/{{ spec.title | caseUcfirst }}Config.cs', + 'Assets/Runtime/{{ spec.title | caseUcfirst }}Manager.cs', + 'Assets/Editor/{{ spec.title | caseUcfirst }}.Editor.asmdef', + 'Assets/Editor/{{ spec.title | caseUcfirst }}SetupAssistant.cs', + 'Assets/Editor/{{ spec.title | caseUcfirst }}SetupWindow.cs', + ]; + + $files = array_filter($files, function ($file) use ($excludeInTest): bool { + return !in_array($file['destination'], $excludeInTest); + }); + } + + return $files; + } +} diff --git a/templates/dotnet/Package/Client.cs.twig b/templates/dotnet/Package/Client.cs.twig index 53bd7ba62c..d4bd56af0f 100644 --- a/templates/dotnet/Package/Client.cs.twig +++ b/templates/dotnet/Package/Client.cs.twig @@ -154,6 +154,7 @@ namespace {{ spec.title | caseUcfirst }} foreach (var parameter in parameters) { + if (parameter.Value == null) continue; if (parameter.Key == "file") { var fileContent = parameters["file"] as MultipartFormDataContent; diff --git a/templates/dotnet/Package/Converters/ObjectToInferredTypesConverter.cs.twig b/templates/dotnet/Package/Converters/ObjectToInferredTypesConverter.cs.twig index 563f92992a..ce772c93df 100644 --- a/templates/dotnet/Package/Converters/ObjectToInferredTypesConverter.cs.twig +++ b/templates/dotnet/Package/Converters/ObjectToInferredTypesConverter.cs.twig @@ -7,32 +7,60 @@ namespace {{ spec.title | caseUcfirst }}.Converters { public class ObjectToInferredTypesConverter : JsonConverter { - public override object Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override object? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { - switch (reader.TokenType) + using (JsonDocument document = JsonDocument.ParseValue(ref reader)) { - case JsonTokenType.True: - return true; - case JsonTokenType.False: - return false; - case JsonTokenType.Number: - if (reader.TryGetInt64(out long l)) + return ConvertElement(document.RootElement); + } + } + + private object? ConvertElement(JsonElement element) + { + switch (element.ValueKind) + { + case JsonValueKind.Object: + var dictionary = new Dictionary(); + foreach (var property in element.EnumerateObject()) { - return l; + dictionary[property.Name] = ConvertElement(property.Value); + } + return dictionary; + + case JsonValueKind.Array: + var list = new List(); + foreach (var item in element.EnumerateArray()) + { + list.Add(ConvertElement(item)); } - return reader.GetDouble(); - case JsonTokenType.String: - if (reader.TryGetDateTime(out DateTime datetime)) + return list; + + case JsonValueKind.String: + if (element.TryGetDateTime(out DateTime datetime)) { return datetime; } - return reader.GetString()!; - case JsonTokenType.StartObject: - return JsonSerializer.Deserialize>(ref reader, options)!; - case JsonTokenType.StartArray: - return JsonSerializer.Deserialize(ref reader, options)!; + return element.GetString(); + + case JsonValueKind.Number: + if (element.TryGetInt64(out long l)) + { + return l; + } + return element.GetDouble(); + + case JsonValueKind.True: + return true; + + case JsonValueKind.False: + return false; + + case JsonValueKind.Null: + case JsonValueKind.Undefined: + return null; + default: - return JsonDocument.ParseValue(ref reader).RootElement.Clone(); + throw new JsonException($"Unsupported JsonValueKind: {element.ValueKind}"); } } diff --git a/templates/dotnet/Package/Exception.cs.twig b/templates/dotnet/Package/Exception.cs.twig index e78d78c2cc..857bd0eeb2 100644 --- a/templates/dotnet/Package/Exception.cs.twig +++ b/templates/dotnet/Package/Exception.cs.twig @@ -5,8 +5,8 @@ namespace {{spec.title | caseUcfirst}} public class {{spec.title | caseUcfirst}}Exception : Exception { public int? Code { get; set; } - public string? Type { get; set; } = null; - public string? Response { get; set; } = null; + public string? Type { get; set; } + public string? Response { get; set; } public {{spec.title | caseUcfirst}}Exception( string? message = null, @@ -18,10 +18,10 @@ namespace {{spec.title | caseUcfirst}} this.Type = type; this.Response = response; } + public {{spec.title | caseUcfirst}}Exception(string message, Exception inner) : base(message, inner) { } } } - diff --git a/templates/dotnet/Package/Extensions/Extensions.cs.twig b/templates/dotnet/Package/Extensions/Extensions.cs.twig index 0ac19f7ce7..eb2f1a237e 100644 --- a/templates/dotnet/Package/Extensions/Extensions.cs.twig +++ b/templates/dotnet/Package/Extensions/Extensions.cs.twig @@ -13,15 +13,14 @@ namespace {{ spec.title | caseUcfirst }}.Extensions return JsonSerializer.Serialize(dict, Client.SerializerOptions); } - public static List ConvertToList(this object value) + public static IEnumerable ToEnumerable(this object value) { return value switch { - JsonElement jsonElement => jsonElement.Deserialize>() ?? throw new InvalidCastException($"Cannot deserialize {jsonElement} to List<{typeof(T)}>."), - object[] objArray => objArray.Cast().ToList(), - List list => list, - IEnumerable enumerable => enumerable.ToList(), - _ => throw new InvalidCastException($"Cannot convert {value.GetType()} to List<{typeof(T)}>") + object[] array => array, + IEnumerable enumerable => enumerable, + IEnumerable nonGeneric => nonGeneric.Cast(), + _ => throw new InvalidCastException($"Cannot convert {value?.GetType().Name ?? "null"} to IEnumerable") }; } @@ -50,7 +49,26 @@ namespace {{ spec.title | caseUcfirst }}.Extensions return Uri.EscapeUriString(string.Join("&", query)); } - private static IDictionary _mappings = new Dictionary(StringComparer.InvariantCultureIgnoreCase) { + public static Dictionary FromQueryString(this string query) + { + var dict = new Dictionary(); + if (string.IsNullOrEmpty(query)) return dict; + if (query.StartsWith("?")) query = query.Substring(1); + var pairs = query.Split('&'); + foreach (var pair in pairs) + { + var kv = pair.Split(new char[] { '=' }, 2); + if (kv.Length >= 1) + { + var key = Uri.UnescapeDataString(kv[0]); + var value = kv.Length > 1 ? Uri.UnescapeDataString(kv[1]) : ""; + dict[key] = value; + } + } + return dict; + } + + private static readonly IDictionary Mappings = new Dictionary(StringComparer.InvariantCultureIgnoreCase) { #region Mime Types {".323", "text/h323"}, @@ -621,7 +639,7 @@ namespace {{ spec.title | caseUcfirst }}.Extensions { if (extension == null) { - throw new ArgumentNullException("extension"); + throw new ArgumentNullException(nameof(extension)); } if (!extension.StartsWith(".")) @@ -629,7 +647,7 @@ namespace {{ spec.title | caseUcfirst }}.Extensions extension = "." + extension; } - return _mappings.TryGetValue(extension, out var mime) ? mime : "application/octet-stream"; + return Mappings.TryGetValue(extension, out var mime) ? mime : "application/octet-stream"; } public static string GetMimeType(this string path) @@ -637,4 +655,4 @@ namespace {{ spec.title | caseUcfirst }}.Extensions return GetMimeTypeFromExtension(System.IO.Path.GetExtension(path)); } } -} \ No newline at end of file +} diff --git a/templates/dotnet/Package/Models/InputFile.cs.twig b/templates/dotnet/Package/Models/InputFile.cs.twig index 241a3adad5..aaf7a66202 100644 --- a/templates/dotnet/Package/Models/InputFile.cs.twig +++ b/templates/dotnet/Package/Models/InputFile.cs.twig @@ -1,5 +1,5 @@ using System.IO; -using Appwrite.Extensions; +using {{ spec.title | caseUcfirst }}.Extensions; namespace {{ spec.title | caseUcfirst }}.Models { @@ -38,4 +38,4 @@ namespace {{ spec.title | caseUcfirst }}.Models SourceType = "bytes" }; } -} \ No newline at end of file +} diff --git a/templates/dotnet/Package/Models/Model.cs.twig b/templates/dotnet/Package/Models/Model.cs.twig index e3f0bd132d..3b77f1b160 100644 --- a/templates/dotnet/Package/Models/Model.cs.twig +++ b/templates/dotnet/Package/Models/Model.cs.twig @@ -1,28 +1,30 @@ - +{% set DefinitionClass = definition.name | caseUcfirst | overrideIdentifier %} using System; using System.Linq; using System.Collections.Generic; using System.Text.Json; using System.Text.Json.Serialization; +{% if definition.properties | filter(p => p.enum) | length > 0 %} using {{ spec.title | caseUcfirst }}.Enums; +{% endif %} using {{ spec.title | caseUcfirst }}.Extensions; namespace {{ spec.title | caseUcfirst }}.Models { - public class {{ definition.name | caseUcfirst | overrideIdentifier }} + public class {{ DefinitionClass }} { {%~ for property in definition.properties %} [JsonPropertyName("{{ property.name }}")] - public {{ sub_schema(property) | raw }} {{ property_name(definition, property) | overrideProperty(definition.name) }} { get; private set; } + public {{ sub_schema(property) }} {{ property_name(definition, property) | overrideProperty(definition.name) }} { get; private set; } {%~ endfor %} {%~ if definition.additionalProperties %} public Dictionary Data { get; private set; } {%~ endif %} - public {{ definition.name | caseUcfirst | overrideIdentifier }}( + public {{ DefinitionClass }}( {%~ for property in definition.properties %} - {{ sub_schema(property) | raw }} {{ property.name | caseCamel | escapeKeyword }}{% if not loop.last or (loop.last and definition.additionalProperties) %},{% endif %} + {{ sub_schema(property) }} {{ property.name | caseCamel | escapeKeyword }}{% if not loop.last or (loop.last and definition.additionalProperties) %},{% endif %} {%~ endfor %} {%~ if definition.additionalProperties %} @@ -37,57 +39,20 @@ namespace {{ spec.title | caseUcfirst }}.Models {%~ endif %} } - public static {{ definition.name | caseUcfirst | overrideIdentifier }} From(Dictionary map) => new {{ definition.name | caseUcfirst | overrideIdentifier }}( + public static {{ DefinitionClass }} From(Dictionary map) => new {{ DefinitionClass }}( {%~ for property in definition.properties %} - {{ property.name | caseCamel | escapeKeyword | removeDollarSign }}:{{' '}} - {%- if property.sub_schema %} - {%- if property.type == 'array' -%} - map["{{ property.name }}"].ConvertToList>().Select(it => {{ property.sub_schema | caseUcfirst | overrideIdentifier }}.From(map: it)).ToList() - {%- else -%} - {{ property.sub_schema | caseUcfirst | overrideIdentifier }}.From(map: map["{{ property.name }}"] is JsonElement jsonObj{{ loop.index }} ? jsonObj{{ loop.index }}.Deserialize>()! : (Dictionary)map["{{ property.name }}"]) - {%- endif %} - {%- elseif property.enum %} - {%- set enumName = property['enumName'] ?? property.name -%} - {%- if not property.required -%} - map.TryGetValue("{{ property.name }}", out var enumRaw{{ loop.index }}) - ? enumRaw{{ loop.index }} == null - ? null - : new {{ enumName | caseUcfirst }}(enumRaw{{ loop.index }}.ToString()!) - : null - {%- else -%} - new {{ enumName | caseUcfirst }}(map["{{ property.name }}"].ToString()!) - {%- endif %} - {%- else %} - {%- if property.type == 'array' -%} - map["{{ property.name }}"].ConvertToList<{{ property | typeName | replace({'List<': '', '>': ''}) }}>() - {%- else %} - {%- if property.type == "integer" or property.type == "number" %} - {%- if not property.required -%}map["{{ property.name }}"] == null ? null :{% endif %}Convert.To{% if property.type == "integer" %}Int64{% else %}Double{% endif %}(map["{{ property.name }}"]) - {%- else %} - {%- if property.type == "boolean" -%} - ({{ property | typeName }}{% if not property.required %}?{% endif %})map["{{ property.name }}"] - {%- else %} - {%- if not property.required -%} - map.TryGetValue("{{ property.name }}", out var {{ property.name | caseCamel | escapeKeyword | removeDollarSign }}) ? {{ property.name | caseCamel | escapeKeyword | removeDollarSign }}?.ToString() : null - {%- else -%} - map["{{ property.name }}"].ToString() - {%- endif %} - {%- endif %} - {%~ endif %} - {%~ endif %} - {%~ endif %} - {%- if not loop.last or (loop.last and definition.additionalProperties) %}, - {%~ endif %} + {{ property.name | caseCamel | escapeKeyword | removeDollarSign }}: {{ property | propertyAssignment | raw }}{% if not loop.last or (loop.last and definition.additionalProperties) %},{% endif %} + {%~ endfor %} - {%- if definition.additionalProperties %} + {%~ if definition.additionalProperties %} data: map.TryGetValue("data", out var dataValue) ? (Dictionary)dataValue : map - {%- endif ~%} + {%~ endif %} ); public Dictionary ToMap() => new Dictionary() { {%~ for property in definition.properties %} - { "{{ property.name }}", {% if property.sub_schema %}{% if property.type == 'array' %}{{ property_name(definition, property) | overrideProperty(definition.name) }}.Select(it => it.ToMap()){% else %}{{ property_name(definition, property) | overrideProperty(definition.name) }}.ToMap(){% endif %}{% elseif property.enum %}{{ property_name(definition, property) | overrideProperty(definition.name) }}{% if not property.required %}?{% endif %}.Value{% else %}{{ property_name(definition, property) | overrideProperty(definition.name) }}{% endif %}{{ ' }' }}{% if not loop.last or (loop.last and definition.additionalProperties) %},{% endif %} + { "{{ property.name }}", {{ property | toMapValue(definition.name) | raw }} }{% if not loop.last or (loop.last and definition.additionalProperties) %},{% endif %} {%~ endfor %} {%~ if definition.additionalProperties %} @@ -100,7 +65,7 @@ namespace {{ spec.title | caseUcfirst }}.Models fromJson.Invoke(Data); {%~ endif %} {%~ for property in definition.properties %} - {%~ if property.sub_schema %} + {%~ if property.sub_schema and not definition.additionalProperties %} {%~ for def in spec.definitions %} {%~ if def.name == property.sub_schema and def.additionalProperties and property.type == 'array' %} diff --git a/templates/dotnet/Package/Models/RequestModel.cs.twig b/templates/dotnet/Package/Models/RequestModel.cs.twig index 333320442e..e87c9bc41f 100644 --- a/templates/dotnet/Package/Models/RequestModel.cs.twig +++ b/templates/dotnet/Package/Models/RequestModel.cs.twig @@ -36,7 +36,7 @@ namespace {{ spec.title | caseUcfirst }}.Models return new Dictionary { {%~ for property in requestModel.properties %} - { "{{ property.name }}", {% if property.sub_schema %}{% if property.type == 'array' %}{{ property.name | caseUcfirst | overrideIdentifier }}?.ConvertAll(p => p.ToMap()){% else %}{{ property.name | caseUcfirst | overrideIdentifier }}?.ToMap(){% endif %}{% elseif property.enum %}{{ property.name | caseUcfirst | overrideIdentifier }}{% if not property.required %}?{% endif %}.Value{% else %}{{ property.name | caseUcfirst | overrideIdentifier }}{% endif %} }{% if not loop.last %},{% endif %} + { "{{ property.name }}", {{ property | toMapValue(requestModel.name) | raw }} }{% if not loop.last %},{% endif %} {%~ endfor %} }; diff --git a/templates/dotnet/Package/Models/UploadProgress.cs.twig b/templates/dotnet/Package/Models/UploadProgress.cs.twig index 47c78391ce..ee6fb58ba3 100644 --- a/templates/dotnet/Package/Models/UploadProgress.cs.twig +++ b/templates/dotnet/Package/Models/UploadProgress.cs.twig @@ -23,4 +23,4 @@ namespace {{ spec.title | caseUcfirst }} ChunksUploaded = chunksUploaded; } } -} \ No newline at end of file +} diff --git a/templates/dotnet/Package/Query.cs.twig b/templates/dotnet/Package/Query.cs.twig index 17c647193c..f77ad1b079 100644 --- a/templates/dotnet/Package/Query.cs.twig +++ b/templates/dotnet/Package/Query.cs.twig @@ -340,4 +340,4 @@ namespace {{ spec.title | caseUcfirst }} return new Query("notTouches", attribute, new List { values }).ToString(); } } -} \ No newline at end of file +} diff --git a/templates/dotnet/Package/Role.cs.twig b/templates/dotnet/Package/Role.cs.twig index b3ecf2610b..3c7b2b33f3 100644 --- a/templates/dotnet/Package/Role.cs.twig +++ b/templates/dotnet/Package/Role.cs.twig @@ -1,4 +1,4 @@ -namespace Appwrite +namespace {{ spec.title | caseUcfirst }} { /// /// Helper class to generate role strings for Permission. @@ -89,4 +89,4 @@ namespace Appwrite return $"label:{name}"; } } -} \ No newline at end of file +} diff --git a/templates/dotnet/Package/Services/ServiceTemplate.cs.twig b/templates/dotnet/Package/Services/ServiceTemplate.cs.twig index 7252134bef..8a0086f04b 100644 --- a/templates/dotnet/Package/Services/ServiceTemplate.cs.twig +++ b/templates/dotnet/Package/Services/ServiceTemplate.cs.twig @@ -1,5 +1,4 @@ {% import 'dotnet/base/utils.twig' as utils %} - using System; using System.Collections.Generic; using System.Linq; diff --git a/templates/unity/Assets/Editor/Appwrite.Editor.asmdef.twig b/templates/unity/Assets/Editor/Appwrite.Editor.asmdef.twig new file mode 100644 index 0000000000..46bb7794fc --- /dev/null +++ b/templates/unity/Assets/Editor/Appwrite.Editor.asmdef.twig @@ -0,0 +1,16 @@ +{ + "name": "{{ spec.title | caseUcfirst }}.Editor", + "rootNamespace": "{{ spec.title | caseUcfirst }}", + "references": [], + "includePlatforms": [ + "Editor" + ], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": false, + "precompiledReferences": [], + "autoReferenced": true, + "defineConstraints": [], + "versionDefines": [], + "noEngineReferences": false +} \ No newline at end of file diff --git a/templates/unity/Assets/Editor/AppwriteSetupAssistant.cs.twig b/templates/unity/Assets/Editor/AppwriteSetupAssistant.cs.twig new file mode 100644 index 0000000000..f9d34ce4a4 --- /dev/null +++ b/templates/unity/Assets/Editor/AppwriteSetupAssistant.cs.twig @@ -0,0 +1,178 @@ +using UnityEngine; +using UnityEditor; +using UnityEditor.PackageManager; +using System.Linq; +using System; +using System.Collections.Generic; + +namespace {{ spec.title | caseUcfirst }}.Editor +{ + [InitializeOnLoad] + public static class {{ spec.title | caseUcfirst }}SetupAssistant + { + private const string UNITASK_PACKAGE_URL = "https://github.com/Cysharp/UniTask.git?path=src/UniTask/Assets/Plugins/UniTask"; + private const string UNITASK_PACKAGE_NAME = "com.cysharp.unitask"; + private const string WEBSOCKET_PACKAGE_URL = "https://github.com/endel/NativeWebSocket.git#upm"; + private const string WEBSOCKET_PACKAGE_NAME = "com.endel.nativewebsocket"; + private const string SETUP_COMPLETED_KEY = "{{ spec.title | caseUcfirst }}_Setup_Completed"; + private const string SHOW_SETUP_DIALOG_KEY = "{{ spec.title | caseUcfirst }}_Show_Setup_Dialog"; + private static bool _isBusy; // General flag to prevent running two operations at once + + public static bool HasUniTask { get; private set; } + public static bool HasWebSocket { get; private set; } + + static {{ spec.title | caseUcfirst }}SetupAssistant() + { + // Use delayCall so the Unity Editor has time to initialize + EditorApplication.delayCall += InitialCheck; + } + + private static void InitialCheck() + { + if (EditorApplication.isCompiling || EditorApplication.isUpdating || EditorPrefs.GetBool(SETUP_COMPLETED_KEY, false)) return; + + RefreshPackageStatus(() => { + if (!HasUniTask || !HasWebSocket) + { + if (!EditorPrefs.GetBool(SHOW_SETUP_DIALOG_KEY, false)) + { + EditorPrefs.SetBool(SHOW_SETUP_DIALOG_KEY, true); + ShowSetupWindow(); + } + } + else + { + CompleteSetup(); + } + }); + } + + /// + /// Asynchronously checks installed packages and invokes the callback when finished. + /// + public static void RefreshPackageStatus(Action onRefreshed = null) + { + if (_isBusy) return; + _isBusy = true; + + var request = Client.List(); + // Subscribe to the editor update event to poll the request status each frame + EditorApplication.update += CheckProgress; + + void CheckProgress() + { + if (!request.IsCompleted) return; + + EditorApplication.update -= CheckProgress; // Unsubscribe so we don't call it again + if (request.Status == StatusCode.Success) + { + HasUniTask = request.Result.Any(p => p.name == UNITASK_PACKAGE_NAME); + HasWebSocket = request.Result.Any(p => p.name == WEBSOCKET_PACKAGE_NAME); + } + else + { + Debug.LogWarning($"{{ spec.title | caseUcfirst }} Setup: Could not refresh package status - {request.Error?.message ?? "Unknown"}"); + } + _isBusy = false; + onRefreshed?.Invoke(); // Invoke the callback + } + } + + public static void InstallUniTask(Action onCompleted) => InstallPackage(UNITASK_PACKAGE_URL, onCompleted); + public static void InstallWebSocket(Action onCompleted) => InstallPackage(WEBSOCKET_PACKAGE_URL, onCompleted); + + /// + /// New reliable method to install all missing packages. + /// + public static void InstallAllPackages(Action onSuccess, Action onError) + { + if (_isBusy) { onError?.Invoke("Another operation is already in progress."); return; } + + var packagesToInstall = new Queue(); + if (!HasUniTask) packagesToInstall.Enqueue(UNITASK_PACKAGE_URL); + if (!HasWebSocket) packagesToInstall.Enqueue(WEBSOCKET_PACKAGE_URL); + + if (packagesToInstall.Count == 0) + { + onSuccess?.Invoke(); + return; + } + + _isBusy = true; + AssetDatabase.StartAssetEditing(); // Pause asset importing to speed up operations + InstallNextPackage(packagesToInstall, onSuccess, onError); + } + + /// + /// Recursively installs packages from the queue one by one. + /// + private static void InstallNextPackage(Queue packageQueue, Action onSuccess, Action onError) + { + if (packageQueue.Count == 0) + { + AssetDatabase.StopAssetEditing(); + _isBusy = false; + onSuccess?.Invoke(); // All packages installed, invoke the final callback + return; + } + + string packageUrl = packageQueue.Dequeue(); + var request = Client.Add(packageUrl); + EditorApplication.update += CheckInstallProgress; + + void CheckInstallProgress() + { + if (!request.IsCompleted) return; + + EditorApplication.update -= CheckInstallProgress; + if (request.Status == StatusCode.Success) + { + Debug.Log($"{{ spec.title | caseUcfirst }} Setup: Successfully installed {request.Result.displayName}."); + InstallNextPackage(packageQueue, onSuccess, onError); // Install the next package + } + else + { + string error = request.Error?.message ?? "Unknown error"; + Debug.LogError($"{{ spec.title | caseUcfirst }} Setup: Failed to install {packageUrl} - {error}"); + AssetDatabase.StopAssetEditing(); + _isBusy = false; + onError?.Invoke(error); + } + } + } + + private static void InstallPackage(string packageUrl, Action onCompleted) + { + if (_isBusy) return; + + var queue = new Queue(); + queue.Enqueue(packageUrl); + + InstallNextPackage(queue,() => onCompleted?.Invoke(), Debug.LogError); + } + + private static void ShowSetupWindow() + { + var window = EditorWindow.GetWindow<{{ spec.title | caseUcfirst }}SetupWindow>(true, "{{ spec.title | caseUcfirst }} Setup Assistant"); + window.Show(); + window.Focus(); + } + private static void CompleteSetup() + { + EditorPrefs.SetBool(SETUP_COMPLETED_KEY, true); + EditorPrefs.SetBool(SHOW_SETUP_DIALOG_KEY, true); + Debug.Log("{{ spec.title | caseUcfirst }} Setup: Setup completed successfully!"); + } + [MenuItem("{{ spec.title | caseUcfirst }}/Setup Assistant", priority = 1)] + public static void ShowSetupAssistant() => ShowSetupWindow(); + [MenuItem("{{ spec.title | caseUcfirst }}/Reset Setup", priority = 100)] + public static void ResetSetup() + { + EditorPrefs.DeleteKey(SETUP_COMPLETED_KEY); + EditorPrefs.DeleteKey(SHOW_SETUP_DIALOG_KEY); + HasUniTask = false; + HasWebSocket = false; + Debug.Log("{{ spec.title | caseUcfirst }} Setup: Setup state reset. Reopening the window will trigger the check."); + } + } +} \ No newline at end of file diff --git a/templates/unity/Assets/Editor/AppwriteSetupWindow.cs.twig b/templates/unity/Assets/Editor/AppwriteSetupWindow.cs.twig new file mode 100644 index 0000000000..a27b4816ed --- /dev/null +++ b/templates/unity/Assets/Editor/AppwriteSetupWindow.cs.twig @@ -0,0 +1,256 @@ +using UnityEngine; +using UnityEditor; +using System; + +namespace {{ spec.title | caseUcfirst }}.Editor +{ + public class {{ spec.title | caseUcfirst }}SetupWindow : EditorWindow + { + private Vector2 _scrollPosition; + private string _statusMessage = ""; + private MessageType _statusMessageType = MessageType.Info; + private bool _isBusy; // Flag to block the UI during asynchronous operations + + private void OnEnable() + { + titleContent = new GUIContent("{{ spec.title | caseUcfirst }} Setup", "{{ spec.title | caseUcfirst }} SDK Setup"); + minSize = new Vector2(520, 520); + maxSize = new Vector2(520, 520); + RefreshStatus(); + } + + private void OnFocus() + { + RefreshStatus(); + } + + // Requests a status refresh and provides a callback to repaint the window + private void RefreshStatus() + { + _isBusy = true; + Repaint(); // Repaint immediately to show the "Working..." message + {{ spec.title | caseUcfirst }}SetupAssistant.RefreshPackageStatus(() => { + _isBusy = false; + Repaint(); + }); + } + + private void OnGUI() + { + EditorGUILayout.Space(20); + DrawHeader(); + EditorGUILayout.Space(15); + + _scrollPosition = EditorGUILayout.BeginScrollView(_scrollPosition); + + if (!string.IsNullOrEmpty(_statusMessage)) + { + EditorGUILayout.HelpBox(_statusMessage, _statusMessageType); + EditorGUILayout.Space(10); + } + + // Disable the UI while _isBusy = true + using (new EditorGUI.DisabledScope(_isBusy)) + { + DrawDependenciesSection(); + EditorGUILayout.Space(15); + + DrawQuickStartSection(); + EditorGUILayout.Space(15); + + DrawActionButtons(); + } + + if (_isBusy) + { + EditorGUILayout.Space(10); + EditorGUILayout.HelpBox("Working, please wait...", MessageType.Info); + } + + EditorGUILayout.EndScrollView(); + + EditorGUILayout.Space(10); + DrawFooter(); + } + + private void DrawDependenciesSection() + { + EditorGUILayout.BeginVertical(EditorStyles.helpBox); + + EditorGUILayout.BeginHorizontal(); + EditorGUILayout.LabelField("📦 Dependencies", EditorStyles.boldLabel); + + var missingPackages = !{{ spec.title | caseUcfirst }}SetupAssistant.HasUniTask || !{{ spec.title | caseUcfirst }}SetupAssistant.HasWebSocket; + if (GUILayout.Button("Install All", GUILayout.Width(100)) && missingPackages) + { + _isBusy = true; + ShowMessage("Installing all required packages...", MessageType.Info); + // Call the new, reliable method to install packages + {{ spec.title | caseUcfirst }}SetupAssistant.InstallAllPackages( + onSuccess: () => { + ShowMessage("All packages installed successfully!", MessageType.Info); + RefreshStatus(); // Refresh package statuses and UI after completion + }, + onError: (error) => { + ShowMessage($"Failed to install packages: {error}", MessageType.Error); + _isBusy = false; // Clear busy flag in case of error + Repaint(); + } + ); + } + + EditorGUILayout.EndHorizontal(); + EditorGUILayout.Space(10); + + // Pass installation methods to the UI + DrawPackageStatus("UniTask", {{ spec.title | caseUcfirst }}SetupAssistant.HasUniTask, + "Required for async operations", + {{ spec.title | caseUcfirst }}SetupAssistant.InstallUniTask); + + EditorGUILayout.Space(5); + + DrawPackageStatus("NativeWebSocket", {{ spec.title | caseUcfirst }}SetupAssistant.HasWebSocket, + "Required for realtime features", + {{ spec.title | caseUcfirst }}SetupAssistant.InstallWebSocket); + + EditorGUILayout.Space(5); + + if (!missingPackages && !_isBusy) + { + EditorGUILayout.HelpBox("✨ All required packages are installed!", MessageType.Info); + } + + EditorGUILayout.EndVertical(); + } + + private void DrawPackageStatus(string packageName, bool isInstalled, string description, Action installAction) + { + var boxStyle = new GUIStyle(EditorStyles.helpBox) + { + padding = new RectOffset(10, 10, 10, 10), + margin = new RectOffset(5, 5, 0, 0) + }; + + EditorGUILayout.BeginVertical(boxStyle); + EditorGUILayout.BeginHorizontal(); + + var statusIcon = isInstalled ? "✅" : "⚠️"; + var nameStyle = new GUIStyle(EditorStyles.boldLabel) { fontSize = 12 }; + EditorGUILayout.LabelField($"{statusIcon} {packageName}", nameStyle); + + if (!isInstalled && GUILayout.Button("Install", GUILayout.Width(100))) + { + _isBusy = true; + ShowMessage($"Installing {packageName}...", MessageType.Info); + installAction?.Invoke(() => { // Invoke installation + ShowMessage($"{packageName} installed successfully!", MessageType.Info); + RefreshStatus(); // Refresh package statuses and UI after completion + }); + } + + EditorGUILayout.EndHorizontal(); + + if (!isInstalled) + { + EditorGUILayout.Space(2); + var descStyle = new GUIStyle(EditorStyles.miniLabel) { wordWrap = true }; + EditorGUILayout.LabelField(description, descStyle); + } + + EditorGUILayout.EndVertical(); + } + + private void DrawHeader() + { + EditorGUILayout.BeginHorizontal(); + GUILayout.FlexibleSpace(); + var headerStyle = new GUIStyle(EditorStyles.boldLabel) { fontSize = 16, alignment = TextAnchor.MiddleCenter }; + EditorGUILayout.LabelField("🚀 {{ spec.title | caseUcfirst }} SDK Setup", headerStyle, GUILayout.ExpandWidth(false)); + GUILayout.FlexibleSpace(); + EditorGUILayout.EndHorizontal(); + EditorGUILayout.Space(4); + EditorGUILayout.BeginHorizontal(); + GUILayout.FlexibleSpace(); + EditorGUILayout.LabelField("Configure your {{ spec.title | caseUcfirst }} SDK for Unity", EditorStyles.centeredGreyMiniLabel, GUILayout.ExpandWidth(false)); + GUILayout.FlexibleSpace(); + EditorGUILayout.EndHorizontal(); + } + private void DrawQuickStartSection() + { + EditorGUILayout.BeginVertical(EditorStyles.helpBox); + EditorGUILayout.LabelField("⚡ Quick Start", EditorStyles.boldLabel); + EditorGUILayout.Space(10); + var allPackagesInstalled = {{ spec.title | caseUcfirst }}SetupAssistant.HasUniTask && {{ spec.title | caseUcfirst }}SetupAssistant.HasWebSocket; + GUI.enabled = allPackagesInstalled; + var buttonStyle = new GUIStyle(GUI.skin.button) { padding = new RectOffset(12, 12, 8, 8), margin = new RectOffset(5, 5, 5, 5), fontSize = 12 }; + if (GUILayout.Button("🎮 Setup Current Scene", buttonStyle)) { SetupCurrentScene(); } + GUI.enabled = true; + EditorGUILayout.Space(10); + var headerStyle = new GUIStyle(EditorStyles.boldLabel) { fontSize = 11 }; + EditorGUILayout.LabelField("This will create in the current scene:", headerStyle); + var itemStyle = new GUIStyle(EditorStyles.label) { richText = true, padding = new RectOffset(15, 0, 2, 2), fontSize = 11 }; + EditorGUILayout.LabelField("• {{ spec.title | caseUcfirst }}Manager - Main SDK manager component", itemStyle); + EditorGUILayout.LabelField("• {{ spec.title | caseUcfirst }}Config - Configuration asset for your project", itemStyle); + EditorGUILayout.LabelField("• Realtime - WebSocket connection handler", itemStyle); + if (!allPackagesInstalled) + { + EditorGUILayout.Space(10); + EditorGUILayout.HelpBox("Please install all required packages first", MessageType.Warning); + } + EditorGUILayout.EndVertical(); + } + private void DrawActionButtons() + { + EditorGUILayout.BeginVertical(EditorStyles.helpBox); + EditorGUILayout.BeginHorizontal(); + var buttonStyle = new GUIStyle(GUI.skin.button) { padding = new RectOffset(15, 15, 8, 8), margin = new RectOffset(5, 5, 5, 5), fontSize = 11 }; + if (GUILayout.Button(new GUIContent(" 📖 Documentation", "Open {{ spec.title | caseUcfirst }} documentation"), buttonStyle)) { Application.OpenURL("https://appwrite.io/docs"); } + if (GUILayout.Button(new GUIContent(" 💬 Discord Community", "Join our Discord community"), buttonStyle)) { Application.OpenURL("https://discord.gg/dJ9xrMr9hq"); } + EditorGUILayout.EndHorizontal(); + EditorGUILayout.EndVertical(); + } + private void DrawFooter() + { + EditorGUI.DrawRect(GUILayoutUtility.GetRect(0, 1), Color.gray); + EditorGUILayout.Space(5); + EditorGUILayout.LabelField("{{ spec.title | caseUcfirst }} SDK for Unity", EditorStyles.centeredGreyMiniLabel); + } + private async void SetupCurrentScene() + { + try + { + ShowMessage("Setting up the scene...", MessageType.Info); + var type = Type.GetType("{{ spec.title | caseUcfirst }}.Utilities.{{ spec.title | caseUcfirst }}Utilities, {{ spec.title | caseUcfirst }}"); + if (type == null) { ShowMessage("{{ spec.title | caseUcfirst }}Utilities not found. Ensure the Runtime assembly is compiled.", MessageType.Warning); return; } + var method = type.GetMethod("QuickSetup", System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.Public); + if (method == null) { ShowMessage("QuickSetup method not found in {{ spec.title | caseUcfirst }}Utilities.", MessageType.Warning); return; } + var task = method.Invoke(null, null); + if (task == null) { ShowMessage("QuickSetup returned null.", MessageType.Warning); return; } + dynamic dynamicTask = task; + var result = await dynamicTask; + if (result != null) + { + var goProp = result.GetType().GetProperty("gameObject"); + var go = goProp?.GetValue(result) as GameObject; + if (go != null) { Selection.activeGameObject = go; ShowMessage("Scene setup completed successfully!", MessageType.Info); } + } + } + catch (Exception ex) { ShowMessage($"Setup failed: {ex.Message}", MessageType.Error); } + } + private void ShowMessage(string message, MessageType type) + { + _statusMessage = message; + _statusMessageType = type; + Repaint(); + if (type != MessageType.Error) + { + EditorApplication.delayCall += () => { + if (_statusMessage == message) + { + System.Threading.Tasks.Task.Delay(5000).ContinueWith(_ => { if (_statusMessage == message) { _statusMessage = ""; Repaint(); } }, System.Threading.Tasks.TaskScheduler.FromCurrentSynchronizationContext()); + } + }; + } + } + } +} \ No newline at end of file diff --git a/templates/unity/Assets/Runtime/Appwrite.asmdef.twig b/templates/unity/Assets/Runtime/Appwrite.asmdef.twig new file mode 100644 index 0000000000..9acbb2c37a --- /dev/null +++ b/templates/unity/Assets/Runtime/Appwrite.asmdef.twig @@ -0,0 +1,24 @@ +{ + "name": "{{ spec.title | caseUcfirst }}", + "rootNamespace": "{{ spec.title | caseUcfirst }}", + "references": [ + "{{ spec.title | caseUcfirst }}.Core", + "endel.nativewebsocket", + "UniTask" + ], + "includePlatforms": [], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": false, + "precompiledReferences": [], + "autoReferenced": true, + "defineConstraints": [], + "versionDefines": [ + { + "name": "com.cysharp.unitask", + "expression": "", + "define": "UNI_TASK" + } + ], + "noEngineReferences": false +} \ No newline at end of file diff --git a/templates/unity/Assets/Runtime/AppwriteConfig.cs.twig b/templates/unity/Assets/Runtime/AppwriteConfig.cs.twig new file mode 100644 index 0000000000..d004db16fc --- /dev/null +++ b/templates/unity/Assets/Runtime/AppwriteConfig.cs.twig @@ -0,0 +1,131 @@ +using System; +using UnityEngine; + +namespace {{ spec.title | caseUcfirst }} +{ + // Define the service enum with Flags attribute for multi-selection in the inspector + [Flags] + public enum {{ spec.title | caseUcfirst }}Service + { + None = 0, + [Tooltip("Selects all main services: Account, Databases, Functions, Storage")] + Main = (1 << 4) - 1, // 0-3 + [Tooltip("Selects all other services: Avatars, GraphQL, Locale, Messaging, Teams")] + Others = (1 << 9) - 1 ^ (1 << 4) - 1, // 4-8 + Account = 1 << 0, + Databases = 1 << 1, + Functions = 1 << 2, + Storage = 1 << 3, + Avatars = 1 << 4, + Graphql = 1 << 5, + Locale = 1 << 6, + Messaging = 1 << 7, + Teams = 1 << 8, + + [Tooltip("Selects all available services.")] + All = ~0 + + } + + /// + /// ScriptableObject configuration for {{ spec.title | caseUcfirst }} client settings + /// + [CreateAssetMenu(fileName = "{{ spec.title | caseUcfirst }}Config", menuName = "{{ spec.title | caseUcfirst }}/Configuration")] + public class {{ spec.title | caseUcfirst }}Config : ScriptableObject + { + [Header("Connection Settings")] + [Tooltip("Endpoint URL for {{ spec.title | caseUcfirst }} API (e.g., https://cloud.{{ spec.title | lower }}.io/v1)")] + [SerializeField] private string endpoint = "https://cloud.{{ spec.title | lower }}.io/v1"; + + [Tooltip("WebSocket endpoint for realtime updates (optional)")] + [SerializeField] private string realtimeEndpoint = "wss://cloud.{{ spec.title | lower }}.io/v1"; + + [Tooltip("Enable if using a self-signed SSL certificate")] + [SerializeField] private bool selfSigned; + + [Header("Project Settings")] + [Tooltip("Your {{ spec.title | caseUcfirst }} project ID")] + [SerializeField] private string projectId = ""; + + [Header("Service Initialization")] + [Tooltip("Select which {{ spec.title | caseUcfirst }} services to initialize.")] + [SerializeField] private {{ spec.title | caseUcfirst }}Service servicesToInitialize = {{ spec.title | caseUcfirst }}Service.All; + + [Header("Advanced Settings")] + [Tooltip("Dev key (optional). Dev keys allow bypassing rate limits and CORS errors in your development environment. WARNING: Storing dev keys in ScriptableObjects is a security risk. Do not expose this in public repositories. Consider loading from a secure location at runtime for production builds.")] + [SerializeField] private string devKey = ""; + + [Tooltip("Automatically connect to {{ spec.title | caseUcfirst }} on start")] + [SerializeField] private bool autoConnect; + + public string Endpoint => endpoint; + public string RealtimeEndpoint => realtimeEndpoint; + public bool SelfSigned => selfSigned; + public string ProjectId => projectId; + public string DevKey => devKey; + public bool AutoConnect => autoConnect; + public {{ spec.title | caseUcfirst }}Service ServicesToInitialize => servicesToInitialize; + + /// + /// Validate configuration settings + /// + private void OnValidate() + { + if (string.IsNullOrEmpty(endpoint)) + Debug.LogWarning("{{ spec.title | caseUcfirst }}Config: Endpoint is required"); + + if (string.IsNullOrEmpty(projectId)) + Debug.LogWarning("{{ spec.title | caseUcfirst }}Config: Project ID is required"); + + if (!string.IsNullOrEmpty(devKey)) + Debug.LogWarning("{{ spec.title | caseUcfirst }}Config: Dev Key is set. For security, avoid storing keys directly in assets for production builds."); + } + + + /// + /// Apply this configuration to a client + /// + public void ApplyTo(Client client) + { + client.SetEndpoint(endpoint); + client.SetProject(projectId); + + if (!string.IsNullOrEmpty(realtimeEndpoint)) + client.SetEndPointRealtime(realtimeEndpoint); + + client.SetSelfSigned(selfSigned); + + if (!string.IsNullOrEmpty(devKey)) + client.SetDevKey(devKey); + } + + #if UNITY_EDITOR + [UnityEditor.MenuItem("{{ spec.title | caseUcfirst }}/Create Configuration")] + public static {{ spec.title | caseUcfirst }}Config CreateConfiguration() + { + var config = CreateInstance<{{ spec.title | caseUcfirst }}Config>(); + + if (!System.IO.Directory.Exists("Assets/{{ spec.title | caseUcfirst }}")) + { + UnityEditor.AssetDatabase.CreateFolder("Assets", "{{ spec.title | caseUcfirst }}"); + } + if (!System.IO.Directory.Exists("Assets/{{ spec.title | caseUcfirst }}/Resources")) + { + UnityEditor.AssetDatabase.CreateFolder("Assets/{{ spec.title | caseUcfirst }}", "Resources"); + } + + string path = "Assets/{{ spec.title | caseUcfirst }}/Resources/{{ spec.title | caseUcfirst }}Config.asset"; + path = UnityEditor.AssetDatabase.GenerateUniqueAssetPath(path); + + UnityEditor.AssetDatabase.CreateAsset(config, path); + UnityEditor.AssetDatabase.SaveAssets(); + UnityEditor.EditorUtility.FocusProjectWindow(); + UnityEditor.Selection.activeObject = config; + + Debug.Log($"{{ spec.title | caseUcfirst }} configuration created at: {path}"); + + return config; + } + #endif + } +} \ No newline at end of file diff --git a/templates/unity/Assets/Runtime/AppwriteManager.cs.twig b/templates/unity/Assets/Runtime/AppwriteManager.cs.twig new file mode 100644 index 0000000000..e56f8949b5 --- /dev/null +++ b/templates/unity/Assets/Runtime/AppwriteManager.cs.twig @@ -0,0 +1,271 @@ +#if UNI_TASK +using System; +using System.Collections.Generic; +using {{ spec.title | caseUcfirst }}.Services; +using Cysharp.Threading.Tasks; +using UnityEngine; + +namespace {{ spec.title | caseUcfirst }} +{ + /// + /// Unity MonoBehaviour wrapper for {{ spec.title | caseUcfirst }} Client with DI support + /// + public class {{ spec.title | caseUcfirst }}Manager : MonoBehaviour + { + [Header("Configuration")] + [SerializeField] private {{ spec.title | caseUcfirst }}Config config; + [SerializeField] private bool initializeOnStart = true; + [SerializeField] private bool dontDestroyOnLoad = true; + + private Client _client; + private Realtime _realtime; + private bool _isInitialized; + + private readonly Dictionary _services = new(); + + // Events + public static event Action OnClientInitialized; + public static event Action OnClientDestroyed; + + // Singleton instance for easy access + public static {{ spec.title | caseUcfirst }}Manager Instance { get; private set; } + + // Properties + public Client Client + { + get + { + if (_client == null) + throw new InvalidOperationException("{{ spec.title | caseUcfirst }} client is not initialized. Call Initialize() first."); + return _client; + } + } + + public Realtime Realtime + { + get + { + if (ReferenceEquals(_realtime, null)) + Debug.LogWarning("Realtime was not initialized. Call Initialize(true) to enable it."); + return _realtime; + } + } + + public bool IsInitialized => _isInitialized; + public {{ spec.title | caseUcfirst }}Config Config => config; + + private void Awake() + { + if (ReferenceEquals(Instance, null)) + { + Instance = this; + if (dontDestroyOnLoad) + DontDestroyOnLoad(gameObject); + } + else if (!ReferenceEquals(Instance, this)) + { + Debug.LogWarning("Multiple {{ spec.title | caseUcfirst }}Manager instances detected. Destroying duplicate."); + Destroy(gameObject); + } + } + + private void Start() + { + if (initializeOnStart) + { + Initialize().Forget(); + } + } + + /// + /// Initialize the {{ spec.title | caseUcfirst }} client and selected services + /// + public async UniTask Initialize(bool needRealtime = false) + { + if (_isInitialized) + { + Debug.LogWarning("{{ spec.title | caseUcfirst }} client is already initialized"); + return true; + } + + if (!config) + { + Debug.LogError("{{ spec.title | caseUcfirst }}Config is not assigned!"); + return false; + } + + try + { + _client = new Client(); + config.ApplyTo(_client); + + InitializeSelectedServices(); + + if (config.AutoConnect) + { + var pingResult = await _client.Ping(); + Debug.Log($"{{ spec.title | caseUcfirst }} connected successfully: {pingResult}"); + } + + if (needRealtime) + { + InitializeRealtime(); + } + + _isInitialized = true; + OnClientInitialized?.Invoke(_client); + + Debug.Log("{{ spec.title | caseUcfirst }} client initialized successfully"); + return true; + } + catch (Exception ex) + { + Debug.LogError($"Failed to initialize {{ spec.title | caseUcfirst }} client: {ex.Message}"); + return false; + } + } + + private readonly Dictionary factory)> _serviceDefinitions = new() + { + { typeof(Account), ({{ spec.title | caseUcfirst }}Service.Account, client => new Account(client)) }, + { typeof(Databases), ({{ spec.title | caseUcfirst }}Service.Databases, client => new Databases(client)) }, + { typeof(Functions), ({{ spec.title | caseUcfirst }}Service.Functions, client => new Functions(client)) }, + { typeof(Storage), ({{ spec.title | caseUcfirst }}Service.Storage, client => new Storage(client)) }, + { typeof(Avatars), ({{ spec.title | caseUcfirst }}Service.Avatars, client => new Avatars(client)) }, + { typeof(Graphql), ({{ spec.title | caseUcfirst }}Service.Graphql, client => new Graphql(client)) }, + { typeof(Locale), ({{ spec.title | caseUcfirst }}Service.Locale, client => new Locale(client)) }, + { typeof(Messaging), ({{ spec.title | caseUcfirst }}Service.Messaging, client => new Messaging(client)) }, + { typeof(Teams), ({{ spec.title | caseUcfirst }}Service.Teams, client => new Teams(client)) }, + }; + + /// + /// Initialize selected {{ spec.title | caseUcfirst }} services based on the configuration. + /// + private void InitializeSelectedServices() + { + _services.Clear(); + var servicesToInit = config.ServicesToInitialize; + + foreach (var kvp in _serviceDefinitions) + { + if (servicesToInit.HasFlag(kvp.Value.flag)) + { + TryCreateService(kvp.Key, kvp.Value.factory); + } + } + } + + /// + /// Try to create and register a service instance. + /// + private void TryCreateService(Type type, Func factory) + { + try + { + _services[type] = factory(_client); + } + catch (Exception ex) + { + Debug.LogError($"Failed to create service {type.Name}: {ex.Message}"); + } + } + + private void InitializeRealtime() + { + if (_client == null) + throw new InvalidOperationException("Client must be initialized before realtime"); + if (ReferenceEquals(_realtime, null)) + { + var realtimeGo = new GameObject("{{ spec.title | caseUcfirst }}Realtime"); + realtimeGo.transform.SetParent(transform); + _realtime = realtimeGo.AddComponent(); + _realtime.Initialize(_client); + } + else + { + // Update existing realtime with new client reference + _realtime.UpdateClient(_client); + } + } + + /// + /// Get an initialized service instance + /// + public T GetService() where T : class + { + if (!_isInitialized) + throw new InvalidOperationException("Client is not initialized. Call Initialize() first."); + + var type = typeof(T); + if (_services.TryGetValue(type, out var service)) + { + return (T)service; + } + + throw new InvalidOperationException($"Service of type {type.Name} was not initialized. Ensure it is selected in the {{ spec.title | caseUcfirst }}Config asset."); + } + + /// + /// Try to get an initialized service instance without throwing an exception. + /// + /// True if the service was found and initialized, otherwise false. + public bool TryGetService(out T service) where T : class + { + if (!_isInitialized) + { + service = null; + Debug.LogWarning("{{ spec.title | caseUcfirst }}Manager: Cannot get service, client is not initialized."); + return false; + } + + var type = typeof(T); + if (_services.TryGetValue(type, out var serviceObj)) + { + service = (T)serviceObj; + return true; + } + + service = null; + return false; + } + + public void SetConfig({{ spec.title | caseUcfirst }}Config newConfig) + { + config = newConfig; + } + + public async UniTask Reinitialize({{ spec.title | caseUcfirst }}Config newConfig = null, bool needRealtime = false) + { + config = newConfig ?? config; + Shutdown(); + return await Initialize(needRealtime); + } + + private void Shutdown() + { + if (!ReferenceEquals(_realtime, null)) + { + _realtime.Disconnect().Forget(); + if (_realtime.gameObject != null) + Destroy(_realtime.gameObject); + } + _realtime = null; + _client = null; + _isInitialized = false; + _services.Clear(); + + OnClientDestroyed?.Invoke(); + Debug.Log("{{ spec.title | caseUcfirst }} client shutdown"); + } + + private void OnDestroy() + { + if (Instance == this) + { + Shutdown(); + Instance = null; + } + } + } +} +#endif diff --git a/templates/unity/Assets/Runtime/Channel.cs.twig b/templates/unity/Assets/Runtime/Channel.cs.twig new file mode 100644 index 0000000000..a11e08f6a4 --- /dev/null +++ b/templates/unity/Assets/Runtime/Channel.cs.twig @@ -0,0 +1,165 @@ +using System.Collections.Generic; + +namespace {{ spec.title | caseUcfirst }} +{ + /// + /// The result of building the channel. + /// + public class ResolvedChannel + { + public string Value { get; } + + internal ResolvedChannel(string value) + { + Value = value; + } + + public override string ToString() => Value; + } + + // Helper function for normalizing ID + internal static class ChannelHelper + { + internal static string Normalize(string id = null) => string.IsNullOrWhiteSpace(id) ? "*" : id.Trim(); + } + + // State interfaces + public interface IChannelValue + { + string Channel { get; } + } + + public interface IActionable : IChannelValue + { + ResolvedChannel Create(); + ResolvedChannel Update(); + ResolvedChannel Delete(); + } + + public interface IDatabase + { + ICollection Collection(string id = null); + } + + public interface ICollection + { + IDocument Document(string id = null); + } + + public interface IDocument : IActionable + { + ResolvedChannel Upsert(); + } + + public interface ITablesDB + { + ITable Table(string id = null); + } + + public interface ITable + { + IRow Row(string id = null); + } + + public interface IRow : IActionable + { + ResolvedChannel Upsert(); + } + + public interface IBucket + { + IFile File(string id = null); + } + + public interface IFile : IActionable { } + + public interface IFunction : IChannelValue { } + + public interface IExecution : IActionable { } + + // Builder implementation + internal class ChannelBuilder : + IDatabase, ICollection, IDocument, + ITablesDB, ITable, IRow, + IBucket, IFile, + IFunction, IExecution, IActionable + { + private readonly List _segments; + + internal ChannelBuilder(List segments) + { + _segments = segments; + } + + private ChannelBuilder Next(string segment, string id = null) + { + var newSegments = new List(_segments) + { + segment + }; + if (id != null) + { + newSegments.Add(ChannelHelper.Normalize(id)); + } + return new ChannelBuilder(newSegments); + } + + public ICollection Collection(string id = null) => Next("collections", id ?? "*"); + + public IDocument Document(string id = null) => Next("documents", id); + + public ITable Table(string id = null) => Next("tables", id ?? "*"); + + public IRow Row(string id = null) => Next("rows", id); + + public IFile File(string id = null) => Next("files", id); + + public ResolvedChannel Create() => Resolve("create"); + + public ResolvedChannel Upsert() => Resolve("upsert"); + + public ResolvedChannel Update() => Resolve("update"); + + public ResolvedChannel Delete() => Resolve("delete"); + + public string Channel => string.Join(".", _segments); + + public override string ToString() => Channel; + + private ResolvedChannel Resolve(string action) => new ResolvedChannel($"{Channel}.{action}"); + } + + // Entry point + public static class Channel + { + public static IDatabase Database(string id = null) => + new ChannelBuilder(new List { "databases", ChannelHelper.Normalize(id) }); + + public static ITablesDB TablesDB(string id = null) => + new ChannelBuilder(new List { "tablesdb", ChannelHelper.Normalize(id) }); + + public static IBucket Bucket(string id = null) => + new ChannelBuilder(new List { "buckets", ChannelHelper.Normalize(id) }); + + public static IFunction Function(string id = null) => + new ChannelBuilder(new List { "functions", ChannelHelper.Normalize(id) }); + + public static IExecution Execution(string id = null) => + new ChannelBuilder(new List { "executions", ChannelHelper.Normalize(id) }); + + public static IActionable Team(string id = null) => + new ChannelBuilder(new List { "teams", ChannelHelper.Normalize(id) }); + + public static IActionable Membership(string id = null) => + new ChannelBuilder(new List { "memberships", ChannelHelper.Normalize(id) }); + + // Global events (without ID) + public static string Account() => "account"; + public static string Documents() => "documents"; + public static string Rows() => "rows"; + public static string Files() => "files"; + public static string Executions() => "executions"; + public static string Teams() => "teams"; + public static string Memberships() => "memberships"; + } +} diff --git a/templates/unity/Assets/Runtime/Core/Appwrite.Core.asmdef.twig b/templates/unity/Assets/Runtime/Core/Appwrite.Core.asmdef.twig new file mode 100644 index 0000000000..f28030d1b4 --- /dev/null +++ b/templates/unity/Assets/Runtime/Core/Appwrite.Core.asmdef.twig @@ -0,0 +1,22 @@ +{ + "name": "{{ spec.title | caseUcfirst }}.Core", + "rootNamespace": "{{ spec.title | caseUcfirst }}", + "references": [ + "UniTask" + ], + "includePlatforms": [], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": false, + "precompiledReferences": [], + "autoReferenced": true, + "defineConstraints": [], + "versionDefines": [ + { + "name": "com.cysharp.unitask", + "expression": "", + "define": "UNI_TASK" + } + ], + "noEngineReferences": false +} \ No newline at end of file diff --git a/templates/unity/Assets/Runtime/Core/Client.cs.twig b/templates/unity/Assets/Runtime/Core/Client.cs.twig new file mode 100644 index 0000000000..f9650c988d --- /dev/null +++ b/templates/unity/Assets/Runtime/Core/Client.cs.twig @@ -0,0 +1,729 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Text.Json; +using System.Text.Json.Serialization; +#if UNI_TASK +using Cysharp.Threading.Tasks; +#endif +using UnityEngine; +using UnityEngine.Networking; +using {{ spec.title | caseUcfirst }}.Converters; +using {{ spec.title | caseUcfirst }}.Extensions; +using {{ spec.title | caseUcfirst }}.Models; + +namespace {{ spec.title | caseUcfirst }} +{ + public class Client + { + private const string SESSION_PREF = "{{ spec.title | caseUcfirst }}_Session"; + private const string JWT_PREF = "{{ spec.title | caseUcfirst }}_JWT"; + + public string Endpoint => _endpoint; + public Dictionary Config => _config; + public CookieContainer CookieContainer => _cookieContainer; + + private readonly Dictionary _headers; + private readonly Dictionary _config; + private string _endpoint; + private bool _selfSigned; + private readonly CookieContainer _cookieContainer; + + private static readonly int ChunkSize = 5 * 1024 * 1024; + + public static JsonSerializerOptions DeserializerOptions { get; set; } = new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + PropertyNameCaseInsensitive = true, + Converters = + { + new JsonStringEnumConverter(JsonNamingPolicy.CamelCase), + new ValueClassConverter(), + new ObjectToInferredTypesConverter() + } + }; + + public static JsonSerializerOptions SerializerOptions { get; set; } = new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + Converters = + { + new JsonStringEnumConverter(JsonNamingPolicy.CamelCase), + new ValueClassConverter(), + new ObjectToInferredTypesConverter() + } + }; + + public Client( + string endpoint = "{{spec.endpoint}}", + bool selfSigned = false) + { + _endpoint = endpoint; + _selfSigned = selfSigned; + _cookieContainer = new CookieContainer(); + + _headers = new Dictionary() + { + { "content-type", "application/json" }, + { "user-agent" , $"{{spec.title | caseUcfirst}}{{ language.name | caseUcfirst }}SDK/{{ sdk.version }} ({Environment.OSVersion.Platform}; {Environment.OSVersion.VersionString})"}, + { "x-sdk-name", "{{ sdk.name }}" }, + { "x-sdk-platform", "{{ sdk.platform }}" }, + { "x-sdk-language", "{{ language.name | caseLower }}" }, + { "x-sdk-version", "{{ sdk.version }}"}{% if spec.global.defaultHeaders | length > 0 %}, + {%~ for key,header in spec.global.defaultHeaders %} + { "{{key}}", "{{header}}" }{% if not loop.last %},{% endif %} + {%~ endfor %}{% endif %} + + }; + + _config = new Dictionary(); + // Load persistent data (session and JWT) + LoadSession(); + // Note: CookieContainer handles its own loading in constructor based on platform + } + + public Client SetSelfSigned(bool selfSigned) + { + _selfSigned = selfSigned; + return this; + } + + public Client SetEndpoint(string endpoint) + { + if (!endpoint.StartsWith("http://") && !endpoint.StartsWith("https://")) { + throw new {{ spec.title | caseUcfirst }}Exception("Invalid endpoint URL: " + endpoint); + } + + _endpoint = endpoint; + return this; + } +#if UNI_TASK + /// + /// Sends a "ping" request to {{ spec.title | caseUcfirst }} to verify connectivity. + /// + /// Ping response as string + public async UniTask Ping() + { + var headers = new Dictionary + { + ["content-type"] = "application/json" + }; + + var parameters = new Dictionary(); + + return await Call("GET", "/ping", headers, parameters, + response => (response.TryGetValue("result", out var result) ? result?.ToString() : null) ?? string.Empty); + } +#endif + /// + /// Set realtime endpoint for WebSocket connections + /// + /// Realtime endpoint URL + /// Client instance for method chaining + public Client SetEndPointRealtime(string endpointRealtime) + { + if (!endpointRealtime.StartsWith("ws://") && !endpointRealtime.StartsWith("wss://")) { + throw new {{ spec.title | caseUcfirst }}Exception("Invalid realtime endpoint URL: " + endpointRealtime); + } + + _config["endpointRealtime"] = endpointRealtime; + return this; + } + + {%~ for header in spec.global.headers %} + {%~ if header.description %} + /// {{header.description}} + {%~ endif %} + public Client Set{{header.key | caseUcfirst}}(string value) { + _config["{{ header.key | caseCamel }}"] = value; + AddHeader("{{header.name}}", value); + {%~ if header.key | caseCamel == "session" or header.key | caseCamel == "jWT" %} + SaveSession(); + {%~ endif %} + + return this; + } + + {%~ endfor %} + /// + /// Get the current session from config + /// + /// Current session token or null + public string GetSession() + { + return _config.GetValueOrDefault("session"); + } + + /// + /// Get the current JWT from config + /// + /// Current JWT token or null + public string GetJWT() + { + return _config.GetValueOrDefault("jWT"); + } + + /// + /// Clear session and JWT from client + /// + /// Client instance for method chaining + public Client ClearSession() + { + _config.Remove("session"); + _config.Remove("jWT"); + _headers.Remove("X-{{ spec.title | caseUcfirst }}-Session"); + _headers.Remove("X-{{ spec.title | caseUcfirst }}-JWT"); + SaveSession(); // Auto-save when session is cleared + return this; + } + + public Client AddHeader(string key, string value) + { + _headers[key] = value; + return this; + } + + /// + /// Load session data from persistent storage + /// + private void LoadSession() + { + try { + LoadPref(SESSION_PREF, "session", "X-{{ spec.title | caseUcfirst }}-Session"); + LoadPref(JWT_PREF, "jWT", "X-{{ spec.title | caseUcfirst }}-JWT"); + } catch (Exception ex) { + Debug.LogWarning($"Failed to load session: {ex.Message}"); + } + } + + private void LoadPref(string prefKey, string configKey, string headerKey) + { + if (!PlayerPrefs.HasKey(prefKey)) return; + var value = PlayerPrefs.GetString(prefKey); + if (string.IsNullOrEmpty(value)) return; + _config[configKey] = value; + _headers[headerKey] = value; + } + + /// + /// Save session data to persistent storage + /// + private void SaveSession() + { + try { + SavePref("session", SESSION_PREF); + SavePref("jWT", JWT_PREF); + PlayerPrefs.Save(); + } catch (Exception ex) { + Debug.LogWarning($"Failed to save session: {ex.Message}"); + } + } + + private void SavePref(string configKey, string prefKey) + { + if (_config.ContainsKey(configKey)) { + PlayerPrefs.SetString(prefKey, _config[configKey]); + } + else { + PlayerPrefs.DeleteKey(prefKey); + } + } + + /// + /// Delete persistent session storage + /// + public void DeleteSessionStorage() + { + try { + PlayerPrefs.DeleteKey(SESSION_PREF); + PlayerPrefs.DeleteKey(JWT_PREF); + PlayerPrefs.Save(); + _cookieContainer.DeleteCookieStorage(); + } catch (Exception ex) { + Debug.LogWarning($"Failed to delete session storage: {ex.Message}"); + } + } + + private UnityWebRequest PrepareRequest( + string method, + string path, + Dictionary headers, + Dictionary parameters) + { + var methodGet = "GET".Equals(method, StringComparison.OrdinalIgnoreCase); + var queryString = methodGet ? "?" + parameters.ToQueryString() : string.Empty; + var url = _endpoint + path + queryString; + + var isMultipart = headers.TryGetValue("Content-Type", out var contentType) && + "multipart/form-data".Equals(contentType, StringComparison.OrdinalIgnoreCase); + + UnityWebRequest request; + + if (isMultipart) + { + var form = new List(); + + foreach (var parameter in parameters) + { + if (parameter.Key == "file" && parameter.Value is InputFile inputFile) + { + byte[] fileData = {}; + switch (inputFile.SourceType) + { + case "path": + if (System.IO.File.Exists(inputFile.Path)) + { + fileData = System.IO.File.ReadAllBytes(inputFile.Path); + } + break; + case "stream": + if (inputFile.Data is Stream stream) + { + using (var memoryStream = new MemoryStream()) + { + stream.CopyTo(memoryStream); + fileData = memoryStream.ToArray(); + } + } + break; + case "bytes": + fileData = inputFile.Data as byte[] ?? Array.Empty(); + break; + } + + form.Add(new MultipartFormFileSection(parameter.Key, fileData, inputFile.Filename, inputFile.MimeType)); + } + else if (parameter.Value is IEnumerable enumerable) + { + if (parameter.Value == null) continue; + var list = new List(enumerable); + for (int index = 0; index < list.Count; index++) + { + form.Add(new MultipartFormDataSection($"{parameter.Key}[{index}]", list[index]?.ToString() ?? string.Empty)); + } + } + else + { + if (parameter.Value == null) continue; + form.Add(new MultipartFormDataSection(parameter.Key, parameter.Value?.ToString() ?? string.Empty)); + } + } + request = UnityWebRequest.Post(url, form); + } + else if (methodGet) + { + request = UnityWebRequest.Get(url); + } + else + { + request = CreateJsonRequest(url, method, parameters); + } + + // Add default headers + foreach (var header in _headers) + { + if (!header.Key.Equals("Content-Type", StringComparison.OrdinalIgnoreCase) || !isMultipart) + { + request.SetRequestHeader(header.Key, header.Value); + } + } + + // Add specific headers + foreach (var header in headers) + { + if (!header.Key.Equals("Content-Type", StringComparison.OrdinalIgnoreCase) || !isMultipart) + { + request.SetRequestHeader(header.Key, header.Value); + } + } + + // Add cookies + var uri = new Uri(url); + var cookieHeader = _cookieContainer.GetCookieHeader(uri.Host, uri.AbsolutePath); +#if !(UNITY_WEBGL && !UNITY_EDITOR) + if (!string.IsNullOrEmpty(cookieHeader)) + { + Debug.Log($"[Client] Setting cookie header: {cookieHeader}"); + request.SetRequestHeader("Cookie", cookieHeader); + } +#endif + + // Handle self-signed certificates + if (_selfSigned) + { + request.certificateHandler = new AcceptAllCertificatesHandler(); + } + + return request; + } + + private UnityWebRequest CreateJsonRequest(string url, string method, Dictionary parameters) + { + string body = parameters.ToJson(); + byte[] bodyData = Encoding.UTF8.GetBytes(body); + + var request = new UnityWebRequest(url, method.ToUpperInvariant()); + request.uploadHandler = new UploadHandlerRaw(bodyData); + request.downloadHandler = new DownloadHandlerBuffer(); + request.SetRequestHeader("Content-Type", "application/json"); + + return request; + } + +#if UNI_TASK + public async UniTask Redirect( + string method, + string path, + Dictionary headers, + Dictionary parameters) + { + var request = PrepareRequest(method, path, headers, parameters); + request.redirectLimit = 0; // Disable auto-redirect + + var operation = request.SendWebRequest(); + + while (!operation.isDone) + { + await UniTask.Yield(); + } + + var code = (int)request.responseCode; + + if (code >= 400) + { + var text = request.downloadHandler?.text ?? string.Empty; + var message = ""; + var type = ""; + + var contentType = request.GetResponseHeader("Content-Type") ?? string.Empty; + + if (contentType.Contains("application/json")) + { + try + { + using var errorDoc = JsonDocument.Parse(text); + message = errorDoc.RootElement.GetProperty("message").GetString() ?? ""; + if (errorDoc.RootElement.TryGetProperty("type", out var typeElement)) + { + type = typeElement.GetString() ?? ""; + } + } + catch + { + message = text; + } + } + else + { + message = text; + } + + request.Dispose(); + throw new {{ spec.title | caseUcfirst }}Exception(message, code, type, text); + } + + var location = request.GetResponseHeader("Location") ?? string.Empty; + request.Dispose(); + return location; + } + + public UniTask> Call( + string method, + string path, + Dictionary headers, + Dictionary parameters) + { + return Call>(method, path, headers, parameters); + } + + public async UniTask Call( + string method, + string path, + Dictionary headers, + Dictionary parameters, + Func, T>? convert = null) where T : class + { + var request = PrepareRequest(method, path, headers, parameters); + + var operation = request.SendWebRequest(); + while (!operation.isDone) + { + await UniTask.Yield(); + } + + var code = (int)request.responseCode; + + // Handle cookies after response +#if !(UNITY_WEBGL && !UNITY_EDITOR) + // Handle Set-Cookie headers (non-WebGL) + var setCookieHeader = request.GetResponseHeader("Set-Cookie"); + if (!string.IsNullOrEmpty(setCookieHeader)) + { + var uri = new Uri(request.url); + _cookieContainer.ParseSetCookieHeader(setCookieHeader, uri.Host); + } +#endif + + // Check for warnings + var warning = request.GetResponseHeader("x-{{ spec.title | lower }}-warning"); + if (!string.IsNullOrEmpty(warning)) + { + Debug.LogWarning("Warning: " + warning); + } + + var contentType = request.GetResponseHeader("Content-Type") ?? string.Empty; + var isJson = contentType.Contains("application/json"); + + if (code >= 400) + { + var text = request.downloadHandler?.text ?? string.Empty; + var message = ""; + var type = ""; + + if (isJson) + { + try + { + using var errorDoc = JsonDocument.Parse(text); + message = errorDoc.RootElement.GetProperty("message").GetString() ?? ""; + if (errorDoc.RootElement.TryGetProperty("type", out var typeElement)) + { + type = typeElement.GetString() ?? ""; + } + } + catch + { + message = text; + } + } + else + { + message = text; + } + + request.Dispose(); + throw new {{ spec.title | caseUcfirst }}Exception(message, code, type, text); + } + + if (isJson) + { + var responseString = request.downloadHandler.text; + + var dict = JsonSerializer.Deserialize>( + responseString, + DeserializerOptions); + + request.Dispose(); + + if (convert != null && dict != null) + { + return convert(dict); + } + + return (dict as T)!; + } + else + { + var result = request.downloadHandler.data as T; + request.Dispose(); + return result!; + } + } + + public async UniTask ChunkedUpload( + string path, + Dictionary headers, + Dictionary parameters, + Func, T> converter, + string paramName, + string? idParamName = null, + Action? onProgress = null) where T : class + { + if (string.IsNullOrEmpty(paramName)) + throw new ArgumentException("Parameter name cannot be null or empty", nameof(paramName)); + + if (!parameters.ContainsKey(paramName)) + throw new ArgumentException($"Parameter {paramName} not found", nameof(paramName)); + + var input = parameters[paramName] as InputFile; + if (input == null) + throw new ArgumentException($"Parameter {paramName} must be an InputFile", nameof(paramName)); + + var size = 0L; + switch(input.SourceType) + { + case "path": + var info = new FileInfo(input.Path); + input.Data = info.OpenRead(); + size = info.Length; + break; + case "stream": + var stream = input.Data as Stream; + if (stream == null) + throw new InvalidOperationException("Stream data is null"); + size = stream.Length; + break; + case "bytes": + var bytes = input.Data as byte[]; + if (bytes == null) + throw new InvalidOperationException("Byte array data is null"); + size = bytes.Length; + break; + }; + + var offset = 0L; + var buffer = new byte[Math.Min(size, ChunkSize)]; + var result = new Dictionary(); + + if (size < ChunkSize) + { + switch(input.SourceType) + { + case "path": + case "stream": + var dataStream = input.Data as Stream; + if (dataStream == null) + throw new InvalidOperationException("Stream data is null"); + await dataStream.ReadAsync(buffer, 0, (int)size); + break; + case "bytes": + var dataBytes = input.Data as byte[]; + if (dataBytes == null) + throw new InvalidOperationException("Byte array data is null"); + buffer = dataBytes; + break; + } + + var multipartHeaders = new Dictionary(headers) + { + ["Content-Type"] = "multipart/form-data" + }; + + var multipartParameters = new Dictionary(parameters); + multipartParameters[paramName] = new InputFile + { + Data = buffer, + Filename = input.Filename, + MimeType = input.MimeType, + SourceType = "bytes" + }; + + return await Call( + method: "POST", + path, + multipartHeaders, + multipartParameters, + converter + ); + } + + if (!string.IsNullOrEmpty(idParamName)) + { + try + { + // Make a request to check if a file already exists + var current = await Call>( + method: "GET", + path: $"{path}/{parameters[idParamName!]}", + new Dictionary { { "Content-Type", "application/json" } }, + parameters: new Dictionary() + ); + if (current.TryGetValue("chunksUploaded", out var chunksUploadedValue) && chunksUploadedValue != null) + { + offset = Convert.ToInt64(chunksUploadedValue) * ChunkSize; + } + } + catch + { + // ignored as it mostly means file not found + } + } + + while (offset < size) + { + switch(input.SourceType) + { + case "path": + case "stream": + var stream = input.Data as Stream; + if (stream == null) + throw new InvalidOperationException("Stream data is null"); + stream.Seek(offset, SeekOrigin.Begin); + await stream.ReadAsync(buffer, 0, ChunkSize); + break; + case "bytes": + buffer = ((byte[])input.Data) + .Skip((int)offset) + .Take((int)Math.Min(size - offset, ChunkSize - 1)) + .ToArray(); + break; + } + + var chunkHeaders = new Dictionary(headers) + { + ["Content-Type"] = "multipart/form-data", + ["Content-Range"] = $"bytes {offset}-{Math.Min(offset + ChunkSize - 1, size - 1)}/{size}" + }; + + var chunkParameters = new Dictionary(parameters); + chunkParameters[paramName] = new InputFile + { + Data = buffer, + Filename = input.Filename, + MimeType = input.MimeType, + SourceType = "bytes" + }; + + result = await Call>( + method: "POST", + path, + chunkHeaders, + chunkParameters + ); + + offset += ChunkSize; + + var id = result.ContainsKey("$id") + ? result["$id"]?.ToString() ?? string.Empty + : string.Empty; + var chunksTotal = result.TryGetValue("chunksTotal", out var chunksTotalValue) && chunksTotalValue != null + ? Convert.ToInt64(chunksTotalValue) + : 0L; + var chunksUploaded = result.TryGetValue("chunksUploaded", out var chunksUploadedValue) && chunksUploadedValue != null + ? Convert.ToInt64(chunksUploadedValue) + : 0L; + + headers["x-{{ spec.title | lower }}-id"] = id; + + onProgress?.Invoke( + new UploadProgress( + id: id, + progress: Math.Min(offset, size) / size * 100, + sizeUploaded: Math.Min(offset, size), + chunksTotal: chunksTotal, + chunksUploaded: chunksUploaded)); + } + + // Convert to non-nullable dictionary for converter + var nonNullableResult = result.Where(kvp => kvp.Value != null) + .ToDictionary(kvp => kvp.Key, kvp => kvp.Value!); + + return converter(nonNullableResult); + } +#endif + + } + + // Custom certificate handler for self-signed certificates + public class AcceptAllCertificatesHandler : CertificateHandler + { + protected override bool ValidateCertificate(byte[] certificateData) + { + return true; // Accept all certificates + } + } +} diff --git a/templates/unity/Assets/Runtime/Core/CookieContainer.cs.twig b/templates/unity/Assets/Runtime/Core/CookieContainer.cs.twig new file mode 100644 index 0000000000..110c365e98 --- /dev/null +++ b/templates/unity/Assets/Runtime/Core/CookieContainer.cs.twig @@ -0,0 +1,291 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.Json; +using UnityEngine; +#if UNITY_WEBGL && !UNITY_EDITOR +using System.Runtime.InteropServices; +#endif + +namespace {{ spec.title | caseUcfirst }} +{ + /// + /// Simple cookie container for Unity WebRequest + /// + [Serializable] + public class Cookie + { + public string Name { get; set; } = string.Empty; + public string Value { get; set; } = string.Empty; + public string Domain { get; set; } = string.Empty; + public string Path { get; set; } = "/"; + public DateTime Expires { get; set; } = DateTime.MaxValue; + public int? MaxAge { get; set; } // null means didn't set, 0+ means seconds from creation + public DateTime CreatedAt { get; set; } = DateTime.Now; + public bool HttpOnly { get; set; } + public bool Secure { get; set; } + public string SameSite { get; set; } = string.Empty; + + // Max-Age priority over Expires + public bool IsExpired => + MaxAge.HasValue + ? MaxAge <= 0 || DateTime.Now > CreatedAt.AddSeconds(MaxAge.Value) + : DateTime.Now > Expires; + + public bool MatchesDomain(string requestDomain) + { + if (string.IsNullOrEmpty(Domain)) return true; + var d = Domain.ToLowerInvariant(); + var r = requestDomain.ToLowerInvariant(); + return r == d || r.EndsWith("." + d) || (d.StartsWith(".") && r.EndsWith(d)); + } + + public bool MatchesPath(string requestPath) => + string.IsNullOrEmpty(Path) || requestPath.StartsWith(Path, StringComparison.OrdinalIgnoreCase); + + public override string ToString() + { + return $"{Name}={Value} " + + $"{(string.IsNullOrEmpty(Domain) ? "" : $"; Domain={Domain}")}" + + $"{(string.IsNullOrEmpty(Path) ? "" : $"; Path={Path}")}" + + $"{(Expires == DateTime.MaxValue ? "" : $"; Expires={Expires:R}")}" + + $"{(MaxAge.HasValue ? $"; Max-Age={MaxAge.Value}" : "")}" + + $"{(HttpOnly ? "; HttpOnly" : "")}" + + $"{(Secure ? "; Secure" : "")}" + + $"{(string.IsNullOrEmpty(SameSite) ? "" : $"; SameSite={SameSite}")}"; + } + } + + /// + /// Simple cookie container implementation for Unity + /// + public class CookieContainer + { + private readonly List _cookies = new List(); + private readonly object _lock = new object(); + + private const string CookiePrefsKey = "{{ spec.title | caseUcfirst }}_Cookies"; +#if UNITY_WEBGL && !UNITY_EDITOR + [DllImport("__Internal")] + private static extern void EnableWebGLHttpCredentials(int enable); +#endif + + public CookieContainer() + { +#if UNITY_WEBGL && !UNITY_EDITOR + try + { + EnableWebGLHttpCredentials(1); + Debug.Log("[CookieContainer] WebGL credentials enabled - browser will handle cookies."); + } + catch + { + // Ignore errors - jslib may not be loaded + } + // In WebGL, the browser handles cookies automatically, so we don't load from PlayerPrefs +#else + // In non-WebGL builds, load cookies from PlayerPrefs + LoadCookies(); +#endif + } + + /// + /// Add a cookie to the container + /// + private void AddCookie(Cookie cookie) + { + if (cookie?.Name == null) return; + + lock (_lock) + { + // Remove existing cookie with the same name, domain, and path + Debug.Log($"[CookieContainer] Removing duplicates for {cookie.Name}"); + _cookies.RemoveAll(c => c.Name == cookie.Name && c.Domain == cookie.Domain && c.Path == cookie.Path); + if (!cookie.IsExpired) + { + _cookies.Add(cookie); + Debug.Log($"[CookieContainer] Cookie added to container: {cookie}"); + SaveCookies(); // Auto-save when cookie is added + } + else + { + Debug.Log($"[CookieContainer] Cookie is expired, not added: {cookie.Name}"); + } + } + } + + /// + /// Get cookies for a specific domain and path + /// + public List GetCookies(string domain, string path = "/") + { + lock (_lock) + { + CleanExpiredCookies(); + var list = _cookies.Where(c => + c.MatchesDomain(domain) && + c.MatchesPath(path) && + !c.IsExpired).ToList(); + Debug.Log($"[CookieContainer] GetCookies for domain={domain} path={path} => {list.Count}"); + return list; + } + } + + /// + /// Get cookie header string for request + /// + public string GetCookieHeader(string domain, string path = "/") => + string.Join("; ", GetCookies(domain, path) + .Select(c => $"{c.Name}={c.Value}")); + + /// + /// Parse Set-Cookie header and add to container + /// + public void ParseSetCookieHeader(string setCookieHeader, string domain) + { + if (string.IsNullOrWhiteSpace(setCookieHeader) || string.IsNullOrWhiteSpace(domain)) return; + foreach (var c in setCookieHeader.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries)) + ParseCookie(c.Trim(), domain); + } + + /// + /// Parse a single cookie string + /// + private void ParseCookie(string cookieString, string domain) + { + var parts = cookieString.Split(';'); + var kv = parts[0].Split('=', 2); + if (kv.Length != 2) return; + + var c = new Cookie { Name = kv[0].Trim(), Value = kv[1].Trim(), Domain = domain.ToLowerInvariant() }; + foreach (var p in parts.Skip(1)) + { + var seg = p.Split('=', 2); + var key = seg[0].Trim().ToLowerInvariant(); + var val = seg.Length > 1 ? seg[1].Trim() : null; + switch (key) + { + case "domain": c.Domain = val?.ToLowerInvariant() ?? string.Empty; break; + case "path": c.Path = val ?? string.Empty; break; + case "expires": + if (DateTime.TryParse(val, out var e)) c.Expires = e; + break; + case "max-age": + if (int.TryParse(val, out var m)) c.MaxAge = m; + break; + case "httponly": c.HttpOnly = true; break; + case "secure": c.Secure = true; break; + case "samesite": c.SameSite = val?.ToLowerInvariant() ?? string.Empty; break; + } + } + Debug.Log($"[CookieContainer] Parsed cookie => {c}"); + AddCookie(c); + } + + /// + /// Clear all cookies + /// + public void Clear() + { + lock (_lock) + { + _cookies.Clear(); + SaveCookies(); // Auto-save when cookies are cleared + } + } + + /// + /// Get the total number of cookies in the container + /// + public int Count + { + get + { + lock (_lock) + { + CleanExpiredCookies(); + return _cookies.Count; + } + } + } + + public string GetContents() + { + lock (_lock) + { + CleanExpiredCookies(); + return string.Join("\n", _cookies); + } + } + + /// + /// Remove expired cookies (must be called within lock) + /// + private void CleanExpiredCookies() => + _cookies.RemoveAll(c => c == null || c.IsExpired); + + /// + /// Load cookies from persistent storage + /// + public void LoadCookies() + { + lock (_lock) + { + try + { + if (PlayerPrefs.HasKey(CookiePrefsKey)) + { + var loaded = JsonSerializer.Deserialize>(PlayerPrefs.GetString(CookiePrefsKey), Client.DeserializerOptions); + _cookies.Clear(); + if (loaded != null) + { + _cookies.AddRange(loaded); + } + } + Debug.Log($"[CookieContainer] Loaded cookies from prefs: {_cookies.Count}"); + CleanExpiredCookies(); + } + catch (Exception ex) + { + Debug.LogWarning($"Failed to load cookies: {ex.Message}"); + _cookies.Clear(); + } + } + } + + /// + /// Save cookies to persistent storage (must be called within lock) + /// + private void SaveCookies() + { + // Note: This method should only be called from within a lock block + try + { + CleanExpiredCookies(); + var json = JsonSerializer.Serialize(_cookies, Client.SerializerOptions); + PlayerPrefs.SetString(CookiePrefsKey, json); + PlayerPrefs.Save(); + Debug.Log($"[CookieContainer] Saved cookies to prefs: {_cookies.Count}"); + } + catch (Exception ex) + { + Debug.LogWarning($"Failed to save cookies: {ex.Message}"); + } + } + + /// + /// Delete persistent cookie storage + /// + public void DeleteCookieStorage() + { + lock (_lock) + { + _cookies.Clear(); + if (PlayerPrefs.HasKey(CookiePrefsKey)) + PlayerPrefs.DeleteKey(CookiePrefsKey); + PlayerPrefs.Save(); + Debug.Log("[CookieContainer] Deleted cookie storage"); + } + } + } +} diff --git a/templates/unity/Assets/Runtime/Core/Plugins/Android/AndroidManifest.xml b/templates/unity/Assets/Runtime/Core/Plugins/Android/AndroidManifest.xml new file mode 100644 index 0000000000..3d7dc9f313 --- /dev/null +++ b/templates/unity/Assets/Runtime/Core/Plugins/Android/AndroidManifest.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + diff --git a/templates/unity/Assets/Runtime/Core/Plugins/Microsoft.Bcl.AsyncInterfaces.dll b/templates/unity/Assets/Runtime/Core/Plugins/Microsoft.Bcl.AsyncInterfaces.dll new file mode 100644 index 0000000000..29fb9b9370 Binary files /dev/null and b/templates/unity/Assets/Runtime/Core/Plugins/Microsoft.Bcl.AsyncInterfaces.dll differ diff --git a/templates/unity/Assets/Runtime/Core/Plugins/System.IO.Pipelines.dll b/templates/unity/Assets/Runtime/Core/Plugins/System.IO.Pipelines.dll new file mode 100644 index 0000000000..796ec8395c Binary files /dev/null and b/templates/unity/Assets/Runtime/Core/Plugins/System.IO.Pipelines.dll differ diff --git a/templates/unity/Assets/Runtime/Core/Plugins/System.Runtime.CompilerServices.Unsafe.dll b/templates/unity/Assets/Runtime/Core/Plugins/System.Runtime.CompilerServices.Unsafe.dll new file mode 100644 index 0000000000..491a80a978 Binary files /dev/null and b/templates/unity/Assets/Runtime/Core/Plugins/System.Runtime.CompilerServices.Unsafe.dll differ diff --git a/templates/unity/Assets/Runtime/Core/Plugins/System.Text.Encodings.Web.dll b/templates/unity/Assets/Runtime/Core/Plugins/System.Text.Encodings.Web.dll new file mode 100644 index 0000000000..6042c008fa Binary files /dev/null and b/templates/unity/Assets/Runtime/Core/Plugins/System.Text.Encodings.Web.dll differ diff --git a/templates/unity/Assets/Runtime/Core/Plugins/System.Text.Json.dll b/templates/unity/Assets/Runtime/Core/Plugins/System.Text.Json.dll new file mode 100644 index 0000000000..9f85348581 Binary files /dev/null and b/templates/unity/Assets/Runtime/Core/Plugins/System.Text.Json.dll differ diff --git a/templates/unity/Assets/Runtime/Core/Plugins/WebGLCookies.jslib b/templates/unity/Assets/Runtime/Core/Plugins/WebGLCookies.jslib new file mode 100644 index 0000000000..e93b35e3ca --- /dev/null +++ b/templates/unity/Assets/Runtime/Core/Plugins/WebGLCookies.jslib @@ -0,0 +1,38 @@ +mergeInto(LibraryManager.library, { + OpenUrlSamePage: function (urlPtr) { + var url = UTF8ToString(urlPtr); + window.location.href = url; + }, + // Enable credentials on fetch/XHR so cookies are sent/received on cross-origin requests + EnableWebGLHttpCredentials: function(enable) { + try { + if (enable) { + // Patch fetch to default credentials: 'include' + if (typeof window !== 'undefined' && window.fetch && !window.__aw_fetchPatched) { + var origFetch = window.fetch.bind(window); + window.fetch = function(input, init) { + init = init || {}; + if (!init.credentials) init.credentials = 'include'; + return origFetch(input, init); + }; + window.__aw_fetchPatched = true; + } + // Patch XHR to set withCredentials=true + if (typeof window !== 'undefined' && window.XMLHttpRequest && !window.__aw_xhrPatched) { + var p = window.XMLHttpRequest.prototype; + var origOpen = p.open; + var origSend = p.send; + p.open = function() { + try { this.withCredentials = true; } catch (e) {} + return origOpen.apply(this, arguments); + }; + p.send = function() { + try { this.withCredentials = true; } catch (e) {} + return origSend.apply(this, arguments); + }; + window.__aw_xhrPatched = true; + } + } + } catch (e) { /* noop */ } + } +}); diff --git a/templates/unity/Assets/Runtime/Core/Services/ServiceTemplate.cs.twig b/templates/unity/Assets/Runtime/Core/Services/ServiceTemplate.cs.twig new file mode 100644 index 0000000000..dea25105bc --- /dev/null +++ b/templates/unity/Assets/Runtime/Core/Services/ServiceTemplate.cs.twig @@ -0,0 +1,81 @@ +{% import 'dotnet/base/utils.twig' as utils %} +#if UNI_TASK +using System; +using System.Collections.Generic; +using System.Linq; +using Cysharp.Threading.Tasks; +{% if spec.definitions is not empty %} +using {{ spec.title | caseUcfirst }}.Models; +{% endif %} +{% if spec.enums is not empty %} +using {{ spec.title | caseUcfirst }}.Enums; +{% endif %} +{% if service.name|lower == 'account' or service.name|lower == 'general' %} +using {{ spec.title | caseUcfirst }}.Extensions; +using UnityEngine; +{% endif %} + +namespace {{ spec.title | caseUcfirst }}.Services +{ + public class {{ service.name | caseUcfirst }} : Service + { + public {{ service.name | caseUcfirst }}(Client client) : base(client) + { + } + + {%~ for method in service.methods %} + {%~ if method.description %} + /// + /// + {{~ method.description | dotnetComment }} + /// + /// + {%~ endif %} + {%~ if method.deprecated %} + {%~ if method.since and method.replaceWith %} + [Obsolete("This API has been deprecated since {{ method.since }}. Please use `{{ method.replaceWith | capitalizeFirst }}` instead.")] + {%~ else %} + [Obsolete("This API has been deprecated.")] + {%~ endif %} + {%~ endif %} +{% if method.type == "webAuth" %} +#if UNITY_EDITOR || UNITY_IOS || UNITY_ANDROID || UNITY_WEBGL +{% endif %} + public {% if method.type == "webAuth" %}async {% endif ~%} UniTask{% if method.type == "webAuth" %}{% else %}<{{ utils.resultType(spec.title, method) }}>{% endif %} {{ method.name | caseUcfirst }}({{ utils.method_parameters(method.parameters, method.consumes) }}) + { + var apiPath = "{{ method.path }}"{% if method.parameters.path | length == 0 %};{% endif %} + + {{~ include('dotnet/base/params.twig') }} + + {%~ if method.responseModel %} + static {{ utils.resultType(spec.title, method) }} Convert(Dictionary it) => + {%~ if method.responseModel == 'any' or method.responseModels|length > 1 %} + it; + {%~ else %} + {{ utils.resultType(spec.title, method) }}.From(map: it); + {%~ endif %} + {%~ endif %} + + {%~ if method.type == 'location' %} + {{~ include('dotnet/base/requests/location.twig') }} + {%~ elseif method.type == 'webAuth' %} + {{~ include('unity/base/requests/oauth.twig') }} + {%~ elseif 'multipart/form-data' in method.consumes %} + {{~ include('dotnet/base/requests/file.twig') }} + {%~ else %} + {{~ include('dotnet/base/requests/api.twig')}} + {%~ endif %} + } +{% if method.type == "webAuth" %} +#else + public UniTask {{ method.name | caseUcfirst }}({{ utils.method_parameters(method.parameters, method.consumes) }}) + { + Debug.LogWarning("[{{ spec.title | caseUcfirst }}] OAuth2 authorization is not supported on this platform. Available only in Editor, WebGL, iOS or Android."); + return UniTask.CompletedTask; + } +#endif{% endif %} + + {%~ endfor %} + } +} +#endif \ No newline at end of file diff --git a/templates/unity/Assets/Runtime/Core/WebAuthComponent.cs.twig b/templates/unity/Assets/Runtime/Core/WebAuthComponent.cs.twig new file mode 100644 index 0000000000..09d0836761 --- /dev/null +++ b/templates/unity/Assets/Runtime/Core/WebAuthComponent.cs.twig @@ -0,0 +1,102 @@ +#if (UNITY_EDITOR || UNITY_IOS || UNITY_ANDROID || UNITY_WEBGL) && UNI_TASK +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using Appwrite.Extensions; +using Cysharp.Threading.Tasks; +using UnityEngine; + +namespace {{ spec.title | caseUcfirst }} +{ + public static class WebAuthComponent + { + private static readonly ConcurrentDictionary> PendingAuth = new(); + + public static event Action OnDeepLink; + + [RuntimeInitializeOnLoadMethod] + private static void Initialize() + { + Application.deepLinkActivated -= OnDeepLinkActivated; + Application.deepLinkActivated += OnDeepLinkActivated; + Debug.Log("[{{ spec.title | caseUcfirst }}DeepLinkHandler] Initialized for OAuth callbacks."); + } + + private static void OnDeepLinkActivated(string url) + { + Debug.Log($"[{{ spec.title | caseUcfirst }}DeepLinkHandler] Received deep link: {url}"); + OnDeepLink?.Invoke(url); + } + static WebAuthComponent() + { + OnDeepLink += HandleCallback; + } + + public static async UniTask Authenticate(string authUrl) + { + var authUri = new Uri(authUrl); + var queryDict = authUri.Query.FromQueryString(); + var projectId = queryDict.GetValueOrDefault("project"); + if (string.IsNullOrEmpty(projectId)) + { + throw new {{ spec.title | caseUcfirst }}Exception("Project ID not found in authentication URL."); + } + + var callbackScheme = $"{{ spec.title | caseLower }}-callback-{projectId}"; + var tcs = new UniTaskCompletionSource(); + + if (!PendingAuth.TryAdd(callbackScheme, tcs)) + { + throw new {{ spec.title | caseUcfirst }}Exception("Authentication process already in progress."); + } + + Debug.Log($"[WebAuthenticator] Opening authentication URL: {authUrl}"); +#if UNITY_WEBGL && !UNITY_EDITOR + OpenUrlSamePage(authUrl); +#else + Application.OpenURL(authUrl); +#endif + Debug.Log($"[WebAuthenticator] Waiting for callback with scheme: {callbackScheme}"); + + try + { + return await tcs.Task; + } + finally + { + PendingAuth.TryRemove(callbackScheme, out _); + } + } + + private static void HandleCallback(string url) + { + try + { + var uri = new Uri(url); + var scheme = uri.Scheme; + + Debug.Log($"[WebAuthenticator] Received callback with scheme: {scheme}"); + + if (PendingAuth.TryGetValue(scheme, out var tcs)) + { + Debug.Log($"[WebAuthenticator] Found matching pending authentication for scheme: {scheme}"); + tcs.TrySetResult(uri); + } + else + { + Debug.LogWarning($"[WebAuthenticator] No pending authentication found for scheme: {scheme}"); + } + } + catch (Exception ex) + { + Debug.LogError($"[WebAuthenticator] Error handling callback: {ex.Message}"); + } + } + +#if UNITY_WEBGL && !UNITY_EDITOR + [System.Runtime.InteropServices.DllImport("__Internal")] + private static extern void OpenUrlSamePage(string url); +#endif + } +} +#endif \ No newline at end of file diff --git a/templates/unity/Assets/Runtime/Core/csc.rsp b/templates/unity/Assets/Runtime/Core/csc.rsp new file mode 100644 index 0000000000..dcc377f897 --- /dev/null +++ b/templates/unity/Assets/Runtime/Core/csc.rsp @@ -0,0 +1 @@ +-nullable:enable \ No newline at end of file diff --git a/templates/unity/Assets/Runtime/Realtime.cs.twig b/templates/unity/Assets/Runtime/Realtime.cs.twig new file mode 100644 index 0000000000..b819a20b65 --- /dev/null +++ b/templates/unity/Assets/Runtime/Realtime.cs.twig @@ -0,0 +1,684 @@ +#if UNI_TASK +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Text; +using System.Text.Json; +using System.Text.Json.Serialization; +using Cysharp.Threading.Tasks; +using UnityEngine; +using NativeWebSocket; + +namespace {{ spec.title | caseUcfirst }} +{ + #region Realtime Data Models + + // Base class to identify a message type + internal class RealtimeMessageBase + { + [JsonPropertyName("type")] + public string Type { get; set; } + } + + // Generic message structure + internal class RealtimeMessage : RealtimeMessageBase + { + [JsonPropertyName("data")] + public T Data { get; set; } + } + + // Specific data models for different message types + internal class RealtimeErrorData + { + [JsonPropertyName("code")] + public int Code { get; set; } + [JsonPropertyName("message")] + public string Message { get; set; } + } + + internal class RealtimeConnectedData + { + [JsonPropertyName("user")] + public Dictionary User { get; set; } + + [JsonPropertyName("subscriptions")] + public Dictionary Subscriptions { get; set; } + } + + internal class RealtimeAuthData + { + [JsonPropertyName("session")] + public string Session { get; set; } + } + + /// + /// Realtime response event structure + /// + [Serializable] + public class RealtimeResponseEvent + { + [JsonPropertyName("events")] + public string[] Events { get; set; } + [JsonPropertyName("channels")] + public string[] Channels { get; set; } + [JsonPropertyName("subscriptions")] + public string[] Subscriptions { get; set; } + [JsonPropertyName("timestamp")] + public string Timestamp { get; set; } + [JsonPropertyName("payload")] + public T Payload { get; set; } + } + + #endregion + + /// + /// Realtime subscription for Unity + /// + public class RealtimeSubscription + { + public string[] Channels { get; internal set; } + public List Queries { get; internal set; } + public Action>> OnMessage { get; internal set; } + internal Action OnClose { get; set; } + + /// + /// Close this subscription + /// + public void Close() + { + OnClose?.Invoke(); + } + } + + /// + /// Realtime connection interface for Unity WebSocket communication + /// + public class Realtime : MonoBehaviour + { + private Client _client; + private WebSocket _webSocket; + private readonly HashSet _channels = new(); + private readonly Dictionary _subscriptions = new(); + private readonly Dictionary _slotToSubscriptionId = new(); + private readonly Dictionary _subscriptionIdToSlot = new(); + private int _subscriptionCounter; + private bool _reconnect = true; + private int _reconnectAttempts; + private CancellationTokenSource _cancellationTokenSource; + private bool _creatingSocket; + private bool _pendingSocketRebuild; + private string _lastUrl; + private CancellationTokenSource _heartbeatTokenSource; + private string _lastSession; + public HashSet Channels => _channels; + + public bool IsConnected => _webSocket?.State == WebSocketState.Open; + public event Action OnConnected; + public event Action OnDisconnected; + public event Action OnError; + + public void Initialize(Client client) + { + _client = client; + _lastSession = _client.GetSession(); + } + + /// + /// Update the client reference (used when client is reinitialized) + /// + public void UpdateClient(Client client) + { + _client = client; + var newSession = _client.GetSession(); + + // If session changed and we're connected, re-authenticate + if (_lastSession != newSession && IsConnected) + { + _lastSession = newSession; + SendFallbackAuthentication(); + } + } + + /// + /// Notify realtime that session has changed and re-authentication may be needed + /// + public void OnSessionChanged() + { + var newSession = _client?.GetSession(); + if (_lastSession != newSession && IsConnected) + { + _lastSession = newSession; + SendFallbackAuthentication(); + } + } + + private void Update() + { + // DispatchMessageQueue ensures that WebSocket messages are processed on the main thread. + // This is crucial for Unity API calls (e.g., modifying GameObjects, UI) from within WebSocket events. + // Note: This ties message processing to the game's frame rate and Time.timeScale. If the game is paused (Time.timeScale = 0), message processing will also pause. + #if !UNITY_WEBGL || UNITY_EDITOR + _webSocket?.DispatchMessageQueue(); + #endif + } + + /// + /// Subscribe to one or more channels using string array + /// + public RealtimeSubscription Subscribe(string[] channels, Action>> callback, List queries = null) + { + Debug.Log($"[Realtime] Subscribe called for channels: [{string.Join(", ", channels)}]"); + + var slot = ++_subscriptionCounter; + var normalizedChannels = channels.Select(channel => channel?.Trim()).Where(channel => !string.IsNullOrWhiteSpace(channel)).ToArray(); + var normalizedQueries = queries == null ? new List() : queries.Where(query => !string.IsNullOrWhiteSpace(query)).ToList(); + + var subscription = new RealtimeSubscription + { + Channels = normalizedChannels, + Queries = normalizedQueries, + OnMessage = callback, + OnClose = () => CloseSubscription(slot) + }; + + _subscriptions[slot] = subscription; + + // Add channels to the set + foreach (var channel in normalizedChannels) + { + _channels.Add(channel); + } + + // Ensure reconnect is enabled when subscribing + _reconnect = true; + CreateSocket().Forget(); + + return subscription; + } + + /// + /// Subscribe to a single channel using string + /// + public RealtimeSubscription Subscribe(string channel, Action>> callback, List queries = null) + { + return Subscribe(new[] { channel }, callback, queries); + } + + /// + /// Subscribe to a single channel using ResolvedChannel + /// + public RealtimeSubscription Subscribe(ResolvedChannel channel, Action>> callback, List queries = null) + { + return Subscribe(new[] { channel.ToString() }, callback, queries); + } + + /// + /// Subscribe to multiple channels using ResolvedChannel array + /// + public RealtimeSubscription Subscribe(ResolvedChannel[] channels, Action>> callback, List queries = null) + { + var channelStrings = channels.Select(c => c.ToString()).ToArray(); + return Subscribe(channelStrings, callback, queries); + } + + /// + /// Subscribe to an actionable channel (without requiring terminal action) + /// + public RealtimeSubscription Subscribe(IActionable actionable, Action>> callback, List queries = null) + { + return Subscribe(new[] { actionable.Channel }, callback, queries); + } + + /// + /// Subscribe to multiple actionable channels + /// + public RealtimeSubscription Subscribe(IActionable[] actionables, Action>> callback, List queries = null) + { + var channelStrings = actionables.Select(a => a.Channel).ToArray(); + return Subscribe(channelStrings, callback, queries); + } + + private void CloseSubscription(int slot) + { + if (_slotToSubscriptionId.TryGetValue(slot, out var subscriptionId)) + { + _subscriptionIdToSlot.Remove(subscriptionId); + } + + _slotToSubscriptionId.Remove(slot); + _subscriptions.Remove(slot); + + _channels.Clear(); + foreach (var activeSubscription in _subscriptions.Values) + { + foreach (var activeChannel in activeSubscription.Channels) + { + _channels.Add(activeChannel); + } + } + + // Recreate socket with new channels or close if none + if (_channels.Count > 0) + { + CreateSocket().Forget(); + } + else + { + // No more subscriptions, close and disable reconnect + CloseConnection(allowReconnect: false).Forget(); + } + } + + private async UniTask CreateSocket() + { + if (_creatingSocket) + { + _pendingSocketRebuild = true; + return; + } + + if (_channels.Count == 0) return; + + _creatingSocket = true; + + Debug.Log($"[Realtime] Creating socket for {_channels.Count} channels"); + + try + { + var uri = PrepareUri(); + Debug.Log($"[Realtime] Connecting to URI: {uri}"); + + if (_webSocket == null || _webSocket.State == WebSocketState.Closed) + { + _webSocket = new WebSocket(uri); + _lastUrl = uri; + SetupWebSocketEvents(); + } + else if (_lastUrl != uri && _webSocket.State != WebSocketState.Closed) + { + await CloseConnection(); + _webSocket = new WebSocket(uri); + _lastUrl = uri; + SetupWebSocketEvents(); + } + + if (_webSocket.State == WebSocketState.Connecting || _webSocket.State == WebSocketState.Open) + { + Debug.Log($"[Realtime] Socket already connecting/connected: {_webSocket.State}"); + _creatingSocket = false; + return; + } + + Debug.Log("[Realtime] Attempting to connect..."); + await _webSocket.Connect(); + Debug.Log("[Realtime] Connect call completed"); + _reconnectAttempts = 0; + } + catch (Exception ex) + { + Debug.LogError($"[Realtime] Connection failed: {ex.Message}"); + OnError?.Invoke(ex); + Retry(); + } + finally + { + _creatingSocket = false; + if (_pendingSocketRebuild) + { + _pendingSocketRebuild = false; + CreateSocket().Forget(); + } + } + } + + private void SetupWebSocketEvents() + { + _webSocket.OnOpen += OnWebSocketOpen; + _webSocket.OnMessage += OnWebSocketMessage; + _webSocket.OnError += OnWebSocketError; + _webSocket.OnClose += OnWebSocketClose; + } + + private void OnWebSocketOpen() + { + _reconnectAttempts = 0; + OnConnected?.Invoke(); + StartHeartbeat(); + Debug.Log("[Realtime] WebSocket opened successfully."); + } + + private void OnWebSocketMessage(byte[] data) + { + try + { + var message = Encoding.UTF8.GetString(data); + var baseMessage = JsonSerializer.Deserialize(message, Client.DeserializerOptions); + + switch (baseMessage.Type) + { + case "connected": + var connectedMsg = JsonSerializer.Deserialize>(message, Client.DeserializerOptions); + HandleConnectedMessage(connectedMsg.Data); + break; + case "event": + var eventMsg = JsonSerializer.Deserialize>>>(message, Client.DeserializerOptions); + HandleRealtimeEvent(eventMsg.Data); + break; + case "error": + var errorMsg = JsonSerializer.Deserialize>(message, Client.DeserializerOptions); + HandleErrorMessage(errorMsg.Data); + break; + case "pong": + Debug.Log("[Realtime] Received pong"); + break; + default: + Debug.Log($"[Realtime] Unknown message type: {baseMessage.Type}"); + break; + } + } + catch (Exception ex) + { + Debug.LogError($"[Realtime] Message processing failed: {ex.Message}"); + OnError?.Invoke(ex); + } + } + + private void HandleConnectedMessage(RealtimeConnectedData data) + { + Debug.Log("[Realtime] Received 'connected' message"); + + _slotToSubscriptionId.Clear(); + _subscriptionIdToSlot.Clear(); + if (data?.Subscriptions != null) + { + foreach (var pair in data.Subscriptions) + { + if (int.TryParse(pair.Key, out var slot) && !string.IsNullOrEmpty(pair.Value)) + { + _slotToSubscriptionId[slot] = pair.Value; + _subscriptionIdToSlot[pair.Value] = slot; + } + } + } + + if (data.User == null || data.User.Count == 0) + { + Debug.Log("[Realtime] No user found, sending fallback authentication"); + SendFallbackAuthentication(); + } + } + + private void SendFallbackAuthentication() + { + var session = _client.Config.GetValueOrDefault("session"); + + if (!string.IsNullOrEmpty(session)) + { + var authMessage = new RealtimeMessage + { + Type = "authentication", + Data = new RealtimeAuthData { Session = session } + }; + + var json = JsonSerializer.Serialize(authMessage, Client.SerializerOptions); + _webSocket.SendText(json); + } + } + + private void HandleErrorMessage(RealtimeErrorData data) + { + OnError?.Invoke(new {{ spec.title | caseUcfirst }}Exception(data.Message, data.Code)); + } + + private void HandleRealtimeEvent(RealtimeResponseEvent> eventData) + { + try + { + if (eventData?.Subscriptions != null && eventData.Subscriptions.Length > 0) + { + foreach (var subscriptionId in eventData.Subscriptions) + { + if (string.IsNullOrEmpty(subscriptionId)) + { + continue; + } + + if (_subscriptionIdToSlot.TryGetValue(subscriptionId, out var slot) && + _subscriptions.TryGetValue(slot, out var subscription)) + { + subscription.OnMessage?.Invoke(eventData); + } + } + + return; + } + + var eventChannels = eventData?.Channels ?? Array.Empty(); + var subscriptionsCopy = _subscriptions.Values.ToArray(); + foreach (var subscription in subscriptionsCopy) + { + if (subscription.Channels.Any(subChannel => eventChannels.Contains(subChannel))) + { + subscription.OnMessage?.Invoke(eventData); + } + } + } + catch (Exception ex) + { + Debug.LogError($"[Realtime] HandleRealtimeEvent error: {ex.Message}"); + OnError?.Invoke(ex); + } + } + + private void OnWebSocketError(string error) + { + Debug.LogError($"[Realtime] WebSocket error: {error}"); + OnError?.Invoke(new {{ spec.title | caseUcfirst }}Exception($"WebSocket error: {error}")); + Retry(); + + } + + private void OnWebSocketClose(WebSocketCloseCode closeCode) + { + Debug.Log($"[Realtime] WebSocket closed with code: {closeCode}"); + StopHeartbeat(); + OnDisconnected?.Invoke(); + if (_reconnect && closeCode != WebSocketCloseCode.PolicyViolation) + { + Retry(); + } + } + + private void StartHeartbeat() + { + StopHeartbeat(); + _heartbeatTokenSource = new CancellationTokenSource(); + + UniTask.Create(async () => + { + try + { + while (!_heartbeatTokenSource.Token.IsCancellationRequested && _webSocket?.State == WebSocketState.Open) + { + await UniTask.Delay(TimeSpan.FromSeconds(20), cancellationToken: _heartbeatTokenSource.Token); + + if (_webSocket?.State == WebSocketState.Open && !_heartbeatTokenSource.Token.IsCancellationRequested) + { + var pingMessage = new { type = "ping" }; + var json = JsonSerializer.Serialize(pingMessage, Client.SerializerOptions); + await _webSocket.SendText(json); + } + } + } + catch (OperationCanceledException) + { + // Expected when cancellation is requested + } + catch (Exception ex) + { + OnError?.Invoke(ex); + } + }); + } + + private void StopHeartbeat() + { + _heartbeatTokenSource?.Cancel(); + _heartbeatTokenSource?.Dispose(); + _heartbeatTokenSource = null; + } + + private void Retry() + { + if (!_reconnect) return; + + _reconnectAttempts++; + var timeout = GetTimeout(); + + Debug.Log($"[Realtime] Reconnecting in {timeout} seconds."); + + // Ensure we have a cancellation token source + if (_cancellationTokenSource == null || _cancellationTokenSource.IsCancellationRequested) + { + _cancellationTokenSource?.Dispose(); + _cancellationTokenSource = new CancellationTokenSource(); + } + + var token = _cancellationTokenSource.Token; + + UniTask.Create(async () => + { + try + { + await UniTask.Delay(TimeSpan.FromSeconds(timeout), cancellationToken: token); + + // Re-check _reconnect after delay in case disconnect was called during wait + if (!_reconnect || token.IsCancellationRequested) + { + Debug.Log("[Realtime] Retry cancelled - reconnect disabled"); + return; + } + + await CreateSocket(); + } + catch (OperationCanceledException) + { + Debug.Log("[Realtime] Retry cancelled"); + } + }); + } + + private int GetTimeout() + { + return _reconnectAttempts < 5 ? 1 : + _reconnectAttempts < 15 ? 5 : + _reconnectAttempts < 100 ? 10 : 60; + } + + private string PrepareUri() + { + var realtimeEndpoint = _client.Config.GetValueOrDefault("endpointRealtime"); + if (string.IsNullOrEmpty(realtimeEndpoint)) + { + throw new {{ spec.title | caseUcfirst }}Exception("Please set endPointRealtime to connect to the realtime server."); + } + + var project = _client.Config.GetValueOrDefault("project", ""); + if (string.IsNullOrEmpty(project)) + { + throw new {{ spec.title | caseUcfirst }}Exception("Project ID is required to connect to the realtime server."); + } + + var allChannels = new HashSet(_subscriptions.Values.SelectMany(subscription => subscription.Channels)); + var queryParts = new List + { + $"project={Uri.EscapeDataString(project)}" + }; + + foreach (var channel in allChannels) + { + queryParts.Add($"channels[]={Uri.EscapeDataString(channel)}"); + } + + const string selectAllQuery = "{\"method\":\"select\",\"values\":[\"*\"]}"; + + foreach (var entry in _subscriptions) + { + var slot = entry.Key; + var subscription = entry.Value; + var queries = subscription.Queries != null && subscription.Queries.Count > 0 + ? subscription.Queries + : new List { selectAllQuery }; + + foreach (var channel in subscription.Channels) + { + var encodedChannel = Uri.EscapeDataString(channel); + foreach (var query in queries) + { + queryParts.Add($"{encodedChannel}[{slot}][]={Uri.EscapeDataString(query)}"); + } + } + } + + var uri = new Uri(realtimeEndpoint); + + var realtimePath = uri.AbsolutePath.TrimEnd('/') + "/realtime"; + + var baseUrl = $"{uri.Scheme}://{uri.Host}"; + if ((uri.Scheme == "wss" && uri.Port != 443) || (uri.Scheme == "ws" && uri.Port != 80)) + { + baseUrl += $":{uri.Port}"; + } + + return $"{baseUrl}{realtimePath}?{string.Join("&", queryParts)}"; + } + + private async UniTask CloseConnection(bool allowReconnect = true) + { + var previousReconnect = _reconnect; + _reconnect = false; + StopHeartbeat(); + + // Cancel any pending retry operations + _cancellationTokenSource?.Cancel(); + _cancellationTokenSource?.Dispose(); + _cancellationTokenSource = null; + + if (_webSocket != null) + { + await _webSocket.Close(); + } + + _reconnectAttempts = 0; + + // Restore reconnect flag if we want to allow future reconnects + if (allowReconnect) + { + _reconnect = previousReconnect; + // Create fresh cancellation token for future retry operations + _cancellationTokenSource = new CancellationTokenSource(); + } + } + + public async UniTask Disconnect() + { + // Disconnect permanently - don't allow auto-reconnect + await CloseConnection(allowReconnect: false); + } + + /// + /// Reconnect after a manual disconnect + /// + public void EnableReconnect() + { + _reconnect = true; + } + + private void OnDestroy() + { + Disconnect().Forget(); + } + } +} +#endif \ No newline at end of file diff --git a/templates/unity/Assets/Runtime/Utilities/AppwriteUtilities.cs.twig b/templates/unity/Assets/Runtime/Utilities/AppwriteUtilities.cs.twig new file mode 100644 index 0000000000..ac0d5b43eb --- /dev/null +++ b/templates/unity/Assets/Runtime/Utilities/AppwriteUtilities.cs.twig @@ -0,0 +1,80 @@ +#if UNI_TASK +using System; +using UnityEngine; +using Cysharp.Threading.Tasks; + +namespace {{ spec.title | caseUcfirst }}.Utilities +{ + /// + /// Utility class for {{ spec.title | caseUcfirst }} Unity integration + /// + public static class {{ spec.title | caseUcfirst }}Utilities + { + #if UNITY_EDITOR + /// + /// Quick setup for {{ spec.title | caseUcfirst }} in Unity (Editor Only) + /// + public static async UniTask<{{ spec.title | caseUcfirst }}Manager> QuickSetup() + { + // Create configuration + var config = {{ spec.title | caseUcfirst }}Config.CreateConfiguration(); + + + // Create manager + var managerGO = new GameObject("{{ spec.title | caseUcfirst }}Manager"); + var manager = managerGO.AddComponent<{{ spec.title | caseUcfirst }}Manager>(); + manager.SetConfig(config); + + // Initialize + var success = await manager.Initialize(true); + if (!success) + { + UnityEngine.Object.Destroy(managerGO); + throw new InvalidOperationException("Failed to initialize {{ spec.title | caseUcfirst }}Manager"); + } + //Create Realtime instance + var a =manager.Realtime; + return manager; + } + #endif + + /// + /// Run async operation with Unity-safe error handling + /// + public static async UniTask SafeExecute( + Func> operation, + T defaultValue = default, + bool logErrors = true) + { + try + { + return await operation(); + } + catch (Exception ex) + { + if (logErrors) + Debug.LogError($"{{ spec.title | caseUcfirst }} operation failed: {ex.Message}"); + return defaultValue; + } + } + + /// + /// Run async operation with Unity-safe error handling (no return value) + /// + public static async UniTask SafeExecute( + Func operation, + bool logErrors = true) + { + try + { + await operation(); + } + catch (Exception ex) + { + if (logErrors) + Debug.LogError($"{{ spec.title | caseUcfirst }} operation failed: {ex.Message}"); + } + } + } +} +#endif diff --git a/templates/unity/Assets/Samples~/AppwriteExample/AppwriteExample.cs.twig b/templates/unity/Assets/Samples~/AppwriteExample/AppwriteExample.cs.twig new file mode 100644 index 0000000000..2d976dbc90 --- /dev/null +++ b/templates/unity/Assets/Samples~/AppwriteExample/AppwriteExample.cs.twig @@ -0,0 +1,128 @@ +using {{ spec.title | caseUcfirst }}; +using Cysharp.Threading.Tasks; +using UnityEngine; + +namespace Samples.{{ spec.title | caseUcfirst }}Example +{ + /// + /// Example of how to use {{ spec.title | caseUcfirst }} with Unity integration + /// + public class {{ spec.title | caseUcfirst }}Example : MonoBehaviour + { + [Header("Configuration")] + [SerializeField] private {{ spec.title | caseUcfirst }}Config config; + + private {{ spec.title | caseUcfirst }}Manager _manager; + + private async void Start() + + { + // Method 1: Using {{ spec.title | caseUcfirst }}Manager (Recommended) + await ExampleWithManager(); + + // Method 2: Using Client directly + await ExampleWithDirectClient(); + } + + /// + /// Example using {{ spec.title | caseUcfirst }}Manager for easy setup + /// + private async UniTask ExampleWithManager() + { + Debug.Log("=== Example with {{ spec.title | caseUcfirst }}Manager ==="); + + // Get or create manager + _manager = {{ spec.title | caseUcfirst }}Manager.Instance; + if (_manager == null) + { + var managerGo = new GameObject("{{ spec.title | caseUcfirst }}Manager"); + _manager = managerGo.AddComponent<{{ spec.title | caseUcfirst }}Manager>(); + _manager.SetConfig(config); + } + + // Initialize + var success = await _manager.Initialize(true); + if (!success) + { + Debug.LogError("Failed to initialize {{ spec.title | caseUcfirst }}Manager"); + return; + } + + // Use services through manager + try + { + // Direct client access + var client = _manager.Client; + var pingResult = await client.Ping(); + Debug.Log($"Ping result: {pingResult}"); + + // Service creation through DI container + // var account = _manager.GetService(); + // var databases = _manager.GetService(); + + // Realtime example + var realtime = _manager.Realtime; + var subscription = realtime.Subscribe( + new[] { "databases.*.collections.*.documents" }, + response => + { + Debug.Log($"Realtime event: {response.Events[0]}"); + } + ); + + Debug.Log("{{ spec.title | caseUcfirst }}Manager example completed successfully"); + } + catch (System.Exception ex) + { + Debug.LogError($"{{ spec.title | caseUcfirst }}Manager example failed: {ex.Message}"); + } + } + + /// + /// Example using Client directly + /// + private async UniTask ExampleWithDirectClient() + { + Debug.Log("=== Example with Direct Client ==="); + + try + { + // Create and configure client + var client = new Client() + .SetEndpoint(config.Endpoint) + .SetProject(config.ProjectId); + + if (!string.IsNullOrEmpty(config.DevKey)) + client.SetDevKey(config.DevKey); + + if (!string.IsNullOrEmpty(config.RealtimeEndpoint)) + client.SetEndPointRealtime(config.RealtimeEndpoint); + + // Test connection + var pingResult = await client.Ping(); + Debug.Log($"Direct client ping: {pingResult}"); + + // Create services manually + // var account = new Account(client); + // var databases = new Databases(client); + + // Realtime example + // You need to create a Realtime instance manually or attach dependently + // realtime.Initialize(client); + // var subscription = realtime.Subscribe( + // new[] { "databases.*.collections.*.documents" }, + // response => + // { + // Debug.Log($"Realtime event: {response.Events[0]}"); + // } + // ); + + Debug.Log("Direct client example completed successfully"); + } + catch (System.Exception ex) + { + Debug.LogError($"Direct client example failed: {ex.Message}"); + } + } + } +} diff --git a/templates/unity/CHANGELOG.md.twig b/templates/unity/CHANGELOG.md.twig new file mode 100644 index 0000000000..dfcefd0336 --- /dev/null +++ b/templates/unity/CHANGELOG.md.twig @@ -0,0 +1 @@ +{{sdk.changelog | raw}} \ No newline at end of file diff --git a/templates/unity/LICENSE.twig b/templates/unity/LICENSE.twig new file mode 100644 index 0000000000..21f5bc7f0a --- /dev/null +++ b/templates/unity/LICENSE.twig @@ -0,0 +1 @@ +{{sdk.license}} diff --git a/templates/unity/Packages/manifest.json b/templates/unity/Packages/manifest.json new file mode 100644 index 0000000000..98ba8ea842 --- /dev/null +++ b/templates/unity/Packages/manifest.json @@ -0,0 +1,47 @@ +{ + "dependencies": { + "com.cysharp.unitask": "https://github.com/Cysharp/UniTask.git?path=src/UniTask/Assets/Plugins/UniTask", + "com.endel.nativewebsocket": "https://github.com/endel/NativeWebSocket.git#upm", + "com.unity.collab-proxy": "2.1.0", + "com.unity.feature.2d": "2.0.0", + "com.unity.ide.rider": "3.0.25", + "com.unity.ide.visualstudio": "2.0.21", + "com.unity.ide.vscode": "1.2.5", + "com.unity.test-framework": "1.1.33", + "com.unity.textmeshpro": "3.0.6", + "com.unity.timeline": "1.6.5", + "com.unity.ugui": "1.0.0", + "com.unity.visualscripting": "1.9.1", + "com.unity.modules.ai": "1.0.0", + "com.unity.modules.androidjni": "1.0.0", + "com.unity.modules.animation": "1.0.0", + "com.unity.modules.assetbundle": "1.0.0", + "com.unity.modules.audio": "1.0.0", + "com.unity.modules.cloth": "1.0.0", + "com.unity.modules.director": "1.0.0", + "com.unity.modules.imageconversion": "1.0.0", + "com.unity.modules.imgui": "1.0.0", + "com.unity.modules.jsonserialize": "1.0.0", + "com.unity.modules.particlesystem": "1.0.0", + "com.unity.modules.physics": "1.0.0", + "com.unity.modules.physics2d": "1.0.0", + "com.unity.modules.screencapture": "1.0.0", + "com.unity.modules.terrain": "1.0.0", + "com.unity.modules.terrainphysics": "1.0.0", + "com.unity.modules.tilemap": "1.0.0", + "com.unity.modules.ui": "1.0.0", + "com.unity.modules.uielements": "1.0.0", + "com.unity.modules.umbra": "1.0.0", + "com.unity.modules.unityanalytics": "1.0.0", + "com.unity.modules.unitywebrequest": "1.0.0", + "com.unity.modules.unitywebrequestassetbundle": "1.0.0", + "com.unity.modules.unitywebrequestaudio": "1.0.0", + "com.unity.modules.unitywebrequesttexture": "1.0.0", + "com.unity.modules.unitywebrequestwww": "1.0.0", + "com.unity.modules.vehicles": "1.0.0", + "com.unity.modules.video": "1.0.0", + "com.unity.modules.vr": "1.0.0", + "com.unity.modules.wind": "1.0.0", + "com.unity.modules.xr": "1.0.0" + } +} diff --git a/templates/unity/Packages/packages-lock.json b/templates/unity/Packages/packages-lock.json new file mode 100644 index 0000000000..552d14b8c5 --- /dev/null +++ b/templates/unity/Packages/packages-lock.json @@ -0,0 +1,490 @@ +{ + "dependencies": { + "com.cysharp.unitask": { + "version": "https://github.com/Cysharp/UniTask.git?path=src/UniTask/Assets/Plugins/UniTask", + "depth": 0, + "source": "git", + "dependencies": {}, + "hash": "f213ff497e4ff462a77319cf677cf20cc0860ca9" + }, + "com.endel.nativewebsocket": { + "version": "https://github.com/endel/NativeWebSocket.git#upm", + "depth": 0, + "source": "git", + "dependencies": {}, + "hash": "1d8b49b3fee41c09a98141f1f1a5e4db47e14229" + }, + "com.unity.2d.animation": { + "version": "7.0.11", + "depth": 1, + "source": "registry", + "dependencies": { + "com.unity.2d.common": "6.0.6", + "com.unity.2d.sprite": "1.0.0", + "com.unity.modules.animation": "1.0.0", + "com.unity.modules.uielements": "1.0.0" + }, + "url": "https://packages.unity.com" + }, + "com.unity.2d.aseprite": { + "version": "1.0.1", + "depth": 1, + "source": "registry", + "dependencies": { + "com.unity.2d.common": "6.0.6", + "com.unity.2d.sprite": "1.0.0", + "com.unity.mathematics": "1.2.6", + "com.unity.modules.animation": "1.0.0" + }, + "url": "https://packages.unity.com" + }, + "com.unity.2d.common": { + "version": "6.0.6", + "depth": 2, + "source": "registry", + "dependencies": { + "com.unity.burst": "1.5.1", + "com.unity.2d.sprite": "1.0.0", + "com.unity.mathematics": "1.1.0", + "com.unity.modules.uielements": "1.0.0" + }, + "url": "https://packages.unity.com" + }, + "com.unity.2d.path": { + "version": "5.0.2", + "depth": 2, + "source": "registry", + "dependencies": {}, + "url": "https://packages.unity.com" + }, + "com.unity.2d.pixel-perfect": { + "version": "5.0.3", + "depth": 1, + "source": "registry", + "dependencies": {}, + "url": "https://packages.unity.com" + }, + "com.unity.2d.psdimporter": { + "version": "6.0.7", + "depth": 1, + "source": "registry", + "dependencies": { + "com.unity.2d.common": "6.0.6", + "com.unity.2d.sprite": "1.0.0", + "com.unity.2d.animation": "7.0.9" + }, + "url": "https://packages.unity.com" + }, + "com.unity.2d.sprite": { + "version": "1.0.0", + "depth": 1, + "source": "builtin", + "dependencies": {} + }, + "com.unity.2d.spriteshape": { + "version": "7.0.7", + "depth": 1, + "source": "registry", + "dependencies": { + "com.unity.2d.path": "5.0.2", + "com.unity.2d.common": "6.0.6", + "com.unity.mathematics": "1.1.0", + "com.unity.modules.physics2d": "1.0.0" + }, + "url": "https://packages.unity.com" + }, + "com.unity.2d.tilemap": { + "version": "1.0.0", + "depth": 1, + "source": "builtin", + "dependencies": {} + }, + "com.unity.2d.tilemap.extras": { + "version": "2.2.6", + "depth": 1, + "source": "registry", + "dependencies": { + "com.unity.ugui": "1.0.0", + "com.unity.2d.tilemap": "1.0.0", + "com.unity.modules.tilemap": "1.0.0", + "com.unity.modules.jsonserialize": "1.0.0" + }, + "url": "https://packages.unity.com" + }, + "com.unity.burst": { + "version": "1.6.6", + "depth": 3, + "source": "registry", + "dependencies": { + "com.unity.mathematics": "1.2.1" + }, + "url": "https://packages.unity.com" + }, + "com.unity.collab-proxy": { + "version": "2.1.0", + "depth": 0, + "source": "registry", + "dependencies": {}, + "url": "https://packages.unity.com" + }, + "com.unity.ext.nunit": { + "version": "1.0.6", + "depth": 1, + "source": "registry", + "dependencies": {}, + "url": "https://packages.unity.com" + }, + "com.unity.feature.2d": { + "version": "2.0.0", + "depth": 0, + "source": "builtin", + "dependencies": { + "com.unity.2d.animation": "7.0.11", + "com.unity.2d.pixel-perfect": "5.0.3", + "com.unity.2d.psdimporter": "6.0.7", + "com.unity.2d.sprite": "1.0.0", + "com.unity.2d.spriteshape": "7.0.7", + "com.unity.2d.tilemap": "1.0.0", + "com.unity.2d.tilemap.extras": "2.2.6", + "com.unity.2d.aseprite": "1.0.1" + } + }, + "com.unity.ide.rider": { + "version": "3.0.25", + "depth": 0, + "source": "registry", + "dependencies": { + "com.unity.ext.nunit": "1.0.6" + }, + "url": "https://packages.unity.com" + }, + "com.unity.ide.visualstudio": { + "version": "2.0.21", + "depth": 0, + "source": "registry", + "dependencies": { + "com.unity.test-framework": "1.1.9" + }, + "url": "https://packages.unity.com" + }, + "com.unity.ide.vscode": { + "version": "1.2.5", + "depth": 0, + "source": "registry", + "dependencies": {}, + "url": "https://packages.unity.com" + }, + "com.unity.mathematics": { + "version": "1.2.6", + "depth": 2, + "source": "registry", + "dependencies": {}, + "url": "https://packages.unity.com" + }, + "com.unity.test-framework": { + "version": "1.1.33", + "depth": 0, + "source": "registry", + "dependencies": { + "com.unity.ext.nunit": "1.0.6", + "com.unity.modules.imgui": "1.0.0", + "com.unity.modules.jsonserialize": "1.0.0" + }, + "url": "https://packages.unity.com" + }, + "com.unity.textmeshpro": { + "version": "3.0.6", + "depth": 0, + "source": "registry", + "dependencies": { + "com.unity.ugui": "1.0.0" + }, + "url": "https://packages.unity.com" + }, + "com.unity.timeline": { + "version": "1.6.5", + "depth": 0, + "source": "registry", + "dependencies": { + "com.unity.modules.audio": "1.0.0", + "com.unity.modules.director": "1.0.0", + "com.unity.modules.animation": "1.0.0", + "com.unity.modules.particlesystem": "1.0.0" + }, + "url": "https://packages.unity.com" + }, + "com.unity.ugui": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": { + "com.unity.modules.ui": "1.0.0", + "com.unity.modules.imgui": "1.0.0" + } + }, + "com.unity.visualscripting": { + "version": "1.9.1", + "depth": 0, + "source": "registry", + "dependencies": { + "com.unity.ugui": "1.0.0", + "com.unity.modules.jsonserialize": "1.0.0" + }, + "url": "https://packages.unity.com" + }, + "com.unity.modules.ai": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": {} + }, + "com.unity.modules.androidjni": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": {} + }, + "com.unity.modules.animation": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": {} + }, + "com.unity.modules.assetbundle": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": {} + }, + "com.unity.modules.audio": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": {} + }, + "com.unity.modules.cloth": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": { + "com.unity.modules.physics": "1.0.0" + } + }, + "com.unity.modules.director": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": { + "com.unity.modules.audio": "1.0.0", + "com.unity.modules.animation": "1.0.0" + } + }, + "com.unity.modules.imageconversion": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": {} + }, + "com.unity.modules.imgui": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": {} + }, + "com.unity.modules.jsonserialize": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": {} + }, + "com.unity.modules.particlesystem": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": {} + }, + "com.unity.modules.physics": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": {} + }, + "com.unity.modules.physics2d": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": {} + }, + "com.unity.modules.screencapture": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": { + "com.unity.modules.imageconversion": "1.0.0" + } + }, + "com.unity.modules.subsystems": { + "version": "1.0.0", + "depth": 1, + "source": "builtin", + "dependencies": { + "com.unity.modules.jsonserialize": "1.0.0" + } + }, + "com.unity.modules.terrain": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": {} + }, + "com.unity.modules.terrainphysics": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": { + "com.unity.modules.physics": "1.0.0", + "com.unity.modules.terrain": "1.0.0" + } + }, + "com.unity.modules.tilemap": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": { + "com.unity.modules.physics2d": "1.0.0" + } + }, + "com.unity.modules.ui": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": {} + }, + "com.unity.modules.uielements": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": { + "com.unity.modules.ui": "1.0.0", + "com.unity.modules.imgui": "1.0.0", + "com.unity.modules.jsonserialize": "1.0.0", + "com.unity.modules.uielementsnative": "1.0.0" + } + }, + "com.unity.modules.uielementsnative": { + "version": "1.0.0", + "depth": 1, + "source": "builtin", + "dependencies": { + "com.unity.modules.ui": "1.0.0", + "com.unity.modules.imgui": "1.0.0", + "com.unity.modules.jsonserialize": "1.0.0" + } + }, + "com.unity.modules.umbra": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": {} + }, + "com.unity.modules.unityanalytics": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": { + "com.unity.modules.unitywebrequest": "1.0.0", + "com.unity.modules.jsonserialize": "1.0.0" + } + }, + "com.unity.modules.unitywebrequest": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": {} + }, + "com.unity.modules.unitywebrequestassetbundle": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": { + "com.unity.modules.assetbundle": "1.0.0", + "com.unity.modules.unitywebrequest": "1.0.0" + } + }, + "com.unity.modules.unitywebrequestaudio": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": { + "com.unity.modules.unitywebrequest": "1.0.0", + "com.unity.modules.audio": "1.0.0" + } + }, + "com.unity.modules.unitywebrequesttexture": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": { + "com.unity.modules.unitywebrequest": "1.0.0", + "com.unity.modules.imageconversion": "1.0.0" + } + }, + "com.unity.modules.unitywebrequestwww": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": { + "com.unity.modules.unitywebrequest": "1.0.0", + "com.unity.modules.unitywebrequestassetbundle": "1.0.0", + "com.unity.modules.unitywebrequestaudio": "1.0.0", + "com.unity.modules.audio": "1.0.0", + "com.unity.modules.assetbundle": "1.0.0", + "com.unity.modules.imageconversion": "1.0.0" + } + }, + "com.unity.modules.vehicles": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": { + "com.unity.modules.physics": "1.0.0" + } + }, + "com.unity.modules.video": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": { + "com.unity.modules.audio": "1.0.0", + "com.unity.modules.ui": "1.0.0", + "com.unity.modules.unitywebrequest": "1.0.0" + } + }, + "com.unity.modules.vr": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": { + "com.unity.modules.jsonserialize": "1.0.0", + "com.unity.modules.physics": "1.0.0", + "com.unity.modules.xr": "1.0.0" + } + }, + "com.unity.modules.wind": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": {} + }, + "com.unity.modules.xr": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": { + "com.unity.modules.physics": "1.0.0", + "com.unity.modules.jsonserialize": "1.0.0", + "com.unity.modules.subsystems": "1.0.0" + } + } + } +} diff --git a/templates/unity/ProjectSettings/AudioManager.asset b/templates/unity/ProjectSettings/AudioManager.asset new file mode 100644 index 0000000000..27287fec5f --- /dev/null +++ b/templates/unity/ProjectSettings/AudioManager.asset @@ -0,0 +1,19 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!11 &1 +AudioManager: + m_ObjectHideFlags: 0 + serializedVersion: 2 + m_Volume: 1 + Rolloff Scale: 1 + Doppler Factor: 1 + Default Speaker Mode: 2 + m_SampleRate: 0 + m_DSPBufferSize: 1024 + m_VirtualVoiceCount: 512 + m_RealVoiceCount: 32 + m_SpatializerPlugin: + m_AmbisonicDecoderPlugin: + m_DisableAudio: 0 + m_VirtualizeEffects: 1 + m_RequestedDSPBufferSize: 0 diff --git a/templates/unity/ProjectSettings/ClusterInputManager.asset b/templates/unity/ProjectSettings/ClusterInputManager.asset new file mode 100644 index 0000000000..e7886b266a --- /dev/null +++ b/templates/unity/ProjectSettings/ClusterInputManager.asset @@ -0,0 +1,6 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!236 &1 +ClusterInputManager: + m_ObjectHideFlags: 0 + m_Inputs: [] diff --git a/templates/unity/ProjectSettings/DynamicsManager.asset b/templates/unity/ProjectSettings/DynamicsManager.asset new file mode 100644 index 0000000000..72d14303c9 --- /dev/null +++ b/templates/unity/ProjectSettings/DynamicsManager.asset @@ -0,0 +1,37 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!55 &1 +PhysicsManager: + m_ObjectHideFlags: 0 + serializedVersion: 13 + m_Gravity: {x: 0, y: -9.81, z: 0} + m_DefaultMaterial: {fileID: 0} + m_BounceThreshold: 2 + m_DefaultMaxDepenetrationVelocity: 10 + m_SleepThreshold: 0.005 + m_DefaultContactOffset: 0.01 + m_DefaultSolverIterations: 6 + m_DefaultSolverVelocityIterations: 1 + m_QueriesHitBackfaces: 0 + m_QueriesHitTriggers: 1 + m_EnableAdaptiveForce: 0 + m_ClothInterCollisionDistance: 0.1 + m_ClothInterCollisionStiffness: 0.2 + m_ContactsGeneration: 1 + m_LayerCollisionMatrix: ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff + m_AutoSimulation: 1 + m_AutoSyncTransforms: 0 + m_ReuseCollisionCallbacks: 1 + m_ClothInterCollisionSettingsToggle: 0 + m_ClothGravity: {x: 0, y: -9.81, z: 0} + m_ContactPairsMode: 0 + m_BroadphaseType: 0 + m_WorldBounds: + m_Center: {x: 0, y: 0, z: 0} + m_Extent: {x: 250, y: 250, z: 250} + m_WorldSubdivisions: 8 + m_FrictionType: 0 + m_EnableEnhancedDeterminism: 0 + m_EnableUnifiedHeightmaps: 1 + m_SolverType: 0 + m_DefaultMaxAngularSpeed: 50 diff --git a/templates/unity/ProjectSettings/EditorBuildSettings.asset b/templates/unity/ProjectSettings/EditorBuildSettings.asset new file mode 100644 index 0000000000..82ab0f5910 --- /dev/null +++ b/templates/unity/ProjectSettings/EditorBuildSettings.asset @@ -0,0 +1,11 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!1045 &1 +EditorBuildSettings: + m_ObjectHideFlags: 0 + serializedVersion: 2 + m_Scenes: + - enabled: 1 + path: Assets/Scenes/SampleScene.unity + guid: 2cda990e2423bbf4892e6590ba056729 + m_configObjects: {} diff --git a/templates/unity/ProjectSettings/EditorSettings.asset b/templates/unity/ProjectSettings/EditorSettings.asset new file mode 100644 index 0000000000..fa3ed49435 --- /dev/null +++ b/templates/unity/ProjectSettings/EditorSettings.asset @@ -0,0 +1,40 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!159 &1 +EditorSettings: + m_ObjectHideFlags: 0 + serializedVersion: 11 + m_SerializationMode: 2 + m_LineEndingsForNewScripts: 0 + m_DefaultBehaviorMode: 1 + m_PrefabRegularEnvironment: {fileID: 0} + m_PrefabUIEnvironment: {fileID: 0} + m_SpritePackerMode: 4 + m_SpritePackerPaddingPower: 1 + m_EtcTextureCompressorBehavior: 1 + m_EtcTextureFastCompressor: 1 + m_EtcTextureNormalCompressor: 2 + m_EtcTextureBestCompressor: 4 + m_ProjectGenerationIncludedExtensions: txt;xml;fnt;cd;asmdef;asmref;rsp + m_ProjectGenerationRootNamespace: + m_EnableTextureStreamingInEditMode: 1 + m_EnableTextureStreamingInPlayMode: 1 + m_AsyncShaderCompilation: 1 + m_CachingShaderPreprocessor: 1 + m_PrefabModeAllowAutoSave: 1 + m_EnterPlayModeOptionsEnabled: 0 + m_EnterPlayModeOptions: 3 + m_GameObjectNamingDigits: 1 + m_GameObjectNamingScheme: 0 + m_AssetNamingUsesSpace: 1 + m_UseLegacyProbeSampleCount: 0 + m_SerializeInlineMappingsOnOneLine: 1 + m_DisableCookiesInLightmapper: 1 + m_AssetPipelineMode: 1 + m_CacheServerMode: 0 + m_CacheServerEndpoint: + m_CacheServerNamespacePrefix: default + m_CacheServerEnableDownload: 1 + m_CacheServerEnableUpload: 1 + m_CacheServerEnableAuth: 0 + m_CacheServerEnableTls: 0 diff --git a/templates/unity/ProjectSettings/GraphicsSettings.asset b/templates/unity/ProjectSettings/GraphicsSettings.asset new file mode 100644 index 0000000000..c165afb2af --- /dev/null +++ b/templates/unity/ProjectSettings/GraphicsSettings.asset @@ -0,0 +1,64 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!30 &1 +GraphicsSettings: + m_ObjectHideFlags: 0 + serializedVersion: 13 + m_Deferred: + m_Mode: 1 + m_Shader: {fileID: 69, guid: 0000000000000000f000000000000000, type: 0} + m_DeferredReflections: + m_Mode: 1 + m_Shader: {fileID: 74, guid: 0000000000000000f000000000000000, type: 0} + m_ScreenSpaceShadows: + m_Mode: 1 + m_Shader: {fileID: 64, guid: 0000000000000000f000000000000000, type: 0} + m_LegacyDeferred: + m_Mode: 1 + m_Shader: {fileID: 63, guid: 0000000000000000f000000000000000, type: 0} + m_DepthNormals: + m_Mode: 1 + m_Shader: {fileID: 62, guid: 0000000000000000f000000000000000, type: 0} + m_MotionVectors: + m_Mode: 1 + m_Shader: {fileID: 75, guid: 0000000000000000f000000000000000, type: 0} + m_LightHalo: + m_Mode: 1 + m_Shader: {fileID: 105, guid: 0000000000000000f000000000000000, type: 0} + m_LensFlare: + m_Mode: 1 + m_Shader: {fileID: 102, guid: 0000000000000000f000000000000000, type: 0} + m_VideoShadersIncludeMode: 2 + m_AlwaysIncludedShaders: + - {fileID: 7, guid: 0000000000000000f000000000000000, type: 0} + - {fileID: 15104, guid: 0000000000000000f000000000000000, type: 0} + - {fileID: 15105, guid: 0000000000000000f000000000000000, type: 0} + - {fileID: 15106, guid: 0000000000000000f000000000000000, type: 0} + - {fileID: 10753, guid: 0000000000000000f000000000000000, type: 0} + - {fileID: 10770, guid: 0000000000000000f000000000000000, type: 0} + - {fileID: 10783, guid: 0000000000000000f000000000000000, type: 0} + m_PreloadedShaders: [] + m_SpritesDefaultMaterial: {fileID: 10754, guid: 0000000000000000f000000000000000, type: 0} + m_CustomRenderPipeline: {fileID: 0} + m_TransparencySortMode: 0 + m_TransparencySortAxis: {x: 0, y: 0, z: 1} + m_DefaultRenderingPath: 1 + m_DefaultMobileRenderingPath: 1 + m_TierSettings: [] + m_LightmapStripping: 0 + m_FogStripping: 0 + m_InstancingStripping: 0 + m_LightmapKeepPlain: 1 + m_LightmapKeepDirCombined: 1 + m_LightmapKeepDynamicPlain: 1 + m_LightmapKeepDynamicDirCombined: 1 + m_LightmapKeepShadowMask: 1 + m_LightmapKeepSubtractive: 1 + m_FogKeepLinear: 1 + m_FogKeepExp: 1 + m_FogKeepExp2: 1 + m_AlbedoSwatchInfos: [] + m_LightsUseLinearIntensity: 0 + m_LightsUseColorTemperature: 0 + m_DefaultRenderingLayerMask: 1 + m_LogWhenShaderIsCompiled: 0 diff --git a/templates/unity/ProjectSettings/InputManager.asset b/templates/unity/ProjectSettings/InputManager.asset new file mode 100644 index 0000000000..b16147e954 --- /dev/null +++ b/templates/unity/ProjectSettings/InputManager.asset @@ -0,0 +1,487 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!13 &1 +InputManager: + m_ObjectHideFlags: 0 + serializedVersion: 2 + m_Axes: + - serializedVersion: 3 + m_Name: Horizontal + descriptiveName: + descriptiveNegativeName: + negativeButton: left + positiveButton: right + altNegativeButton: a + altPositiveButton: d + gravity: 3 + dead: 0.001 + sensitivity: 3 + snap: 1 + invert: 0 + type: 0 + axis: 0 + joyNum: 0 + - serializedVersion: 3 + m_Name: Vertical + descriptiveName: + descriptiveNegativeName: + negativeButton: down + positiveButton: up + altNegativeButton: s + altPositiveButton: w + gravity: 3 + dead: 0.001 + sensitivity: 3 + snap: 1 + invert: 0 + type: 0 + axis: 0 + joyNum: 0 + - serializedVersion: 3 + m_Name: Fire1 + descriptiveName: + descriptiveNegativeName: + negativeButton: + positiveButton: left ctrl + altNegativeButton: + altPositiveButton: mouse 0 + gravity: 1000 + dead: 0.001 + sensitivity: 1000 + snap: 0 + invert: 0 + type: 0 + axis: 0 + joyNum: 0 + - serializedVersion: 3 + m_Name: Fire2 + descriptiveName: + descriptiveNegativeName: + negativeButton: + positiveButton: left alt + altNegativeButton: + altPositiveButton: mouse 1 + gravity: 1000 + dead: 0.001 + sensitivity: 1000 + snap: 0 + invert: 0 + type: 0 + axis: 0 + joyNum: 0 + - serializedVersion: 3 + m_Name: Fire3 + descriptiveName: + descriptiveNegativeName: + negativeButton: + positiveButton: left shift + altNegativeButton: + altPositiveButton: mouse 2 + gravity: 1000 + dead: 0.001 + sensitivity: 1000 + snap: 0 + invert: 0 + type: 0 + axis: 0 + joyNum: 0 + - serializedVersion: 3 + m_Name: Jump + descriptiveName: + descriptiveNegativeName: + negativeButton: + positiveButton: space + altNegativeButton: + altPositiveButton: + gravity: 1000 + dead: 0.001 + sensitivity: 1000 + snap: 0 + invert: 0 + type: 0 + axis: 0 + joyNum: 0 + - serializedVersion: 3 + m_Name: Mouse X + descriptiveName: + descriptiveNegativeName: + negativeButton: + positiveButton: + altNegativeButton: + altPositiveButton: + gravity: 0 + dead: 0 + sensitivity: 0.1 + snap: 0 + invert: 0 + type: 1 + axis: 0 + joyNum: 0 + - serializedVersion: 3 + m_Name: Mouse Y + descriptiveName: + descriptiveNegativeName: + negativeButton: + positiveButton: + altNegativeButton: + altPositiveButton: + gravity: 0 + dead: 0 + sensitivity: 0.1 + snap: 0 + invert: 0 + type: 1 + axis: 1 + joyNum: 0 + - serializedVersion: 3 + m_Name: Mouse ScrollWheel + descriptiveName: + descriptiveNegativeName: + negativeButton: + positiveButton: + altNegativeButton: + altPositiveButton: + gravity: 0 + dead: 0 + sensitivity: 0.1 + snap: 0 + invert: 0 + type: 1 + axis: 2 + joyNum: 0 + - serializedVersion: 3 + m_Name: Horizontal + descriptiveName: + descriptiveNegativeName: + negativeButton: + positiveButton: + altNegativeButton: + altPositiveButton: + gravity: 0 + dead: 0.19 + sensitivity: 1 + snap: 0 + invert: 0 + type: 2 + axis: 0 + joyNum: 0 + - serializedVersion: 3 + m_Name: Vertical + descriptiveName: + descriptiveNegativeName: + negativeButton: + positiveButton: + altNegativeButton: + altPositiveButton: + gravity: 0 + dead: 0.19 + sensitivity: 1 + snap: 0 + invert: 1 + type: 2 + axis: 1 + joyNum: 0 + - serializedVersion: 3 + m_Name: Fire1 + descriptiveName: + descriptiveNegativeName: + negativeButton: + positiveButton: joystick button 0 + altNegativeButton: + altPositiveButton: + gravity: 1000 + dead: 0.001 + sensitivity: 1000 + snap: 0 + invert: 0 + type: 0 + axis: 0 + joyNum: 0 + - serializedVersion: 3 + m_Name: Fire2 + descriptiveName: + descriptiveNegativeName: + negativeButton: + positiveButton: joystick button 1 + altNegativeButton: + altPositiveButton: + gravity: 1000 + dead: 0.001 + sensitivity: 1000 + snap: 0 + invert: 0 + type: 0 + axis: 0 + joyNum: 0 + - serializedVersion: 3 + m_Name: Fire3 + descriptiveName: + descriptiveNegativeName: + negativeButton: + positiveButton: joystick button 2 + altNegativeButton: + altPositiveButton: + gravity: 1000 + dead: 0.001 + sensitivity: 1000 + snap: 0 + invert: 0 + type: 0 + axis: 0 + joyNum: 0 + - serializedVersion: 3 + m_Name: Jump + descriptiveName: + descriptiveNegativeName: + negativeButton: + positiveButton: joystick button 3 + altNegativeButton: + altPositiveButton: + gravity: 1000 + dead: 0.001 + sensitivity: 1000 + snap: 0 + invert: 0 + type: 0 + axis: 0 + joyNum: 0 + - serializedVersion: 3 + m_Name: Submit + descriptiveName: + descriptiveNegativeName: + negativeButton: + positiveButton: return + altNegativeButton: + altPositiveButton: joystick button 0 + gravity: 1000 + dead: 0.001 + sensitivity: 1000 + snap: 0 + invert: 0 + type: 0 + axis: 0 + joyNum: 0 + - serializedVersion: 3 + m_Name: Submit + descriptiveName: + descriptiveNegativeName: + negativeButton: + positiveButton: enter + altNegativeButton: + altPositiveButton: space + gravity: 1000 + dead: 0.001 + sensitivity: 1000 + snap: 0 + invert: 0 + type: 0 + axis: 0 + joyNum: 0 + - serializedVersion: 3 + m_Name: Cancel + descriptiveName: + descriptiveNegativeName: + negativeButton: + positiveButton: escape + altNegativeButton: + altPositiveButton: joystick button 1 + gravity: 1000 + dead: 0.001 + sensitivity: 1000 + snap: 0 + invert: 0 + type: 0 + axis: 0 + joyNum: 0 + - serializedVersion: 3 + m_Name: Enable Debug Button 1 + descriptiveName: + descriptiveNegativeName: + negativeButton: + positiveButton: left ctrl + altNegativeButton: + altPositiveButton: joystick button 8 + gravity: 0 + dead: 0 + sensitivity: 0 + snap: 0 + invert: 0 + type: 0 + axis: 0 + joyNum: 0 + - serializedVersion: 3 + m_Name: Enable Debug Button 2 + descriptiveName: + descriptiveNegativeName: + negativeButton: + positiveButton: backspace + altNegativeButton: + altPositiveButton: joystick button 9 + gravity: 0 + dead: 0 + sensitivity: 0 + snap: 0 + invert: 0 + type: 0 + axis: 0 + joyNum: 0 + - serializedVersion: 3 + m_Name: Debug Reset + descriptiveName: + descriptiveNegativeName: + negativeButton: + positiveButton: left alt + altNegativeButton: + altPositiveButton: joystick button 1 + gravity: 0 + dead: 0 + sensitivity: 0 + snap: 0 + invert: 0 + type: 0 + axis: 0 + joyNum: 0 + - serializedVersion: 3 + m_Name: Debug Next + descriptiveName: + descriptiveNegativeName: + negativeButton: + positiveButton: page down + altNegativeButton: + altPositiveButton: joystick button 5 + gravity: 0 + dead: 0 + sensitivity: 0 + snap: 0 + invert: 0 + type: 0 + axis: 0 + joyNum: 0 + - serializedVersion: 3 + m_Name: Debug Previous + descriptiveName: + descriptiveNegativeName: + negativeButton: + positiveButton: page up + altNegativeButton: + altPositiveButton: joystick button 4 + gravity: 0 + dead: 0 + sensitivity: 0 + snap: 0 + invert: 0 + type: 0 + axis: 0 + joyNum: 0 + - serializedVersion: 3 + m_Name: Debug Validate + descriptiveName: + descriptiveNegativeName: + negativeButton: + positiveButton: return + altNegativeButton: + altPositiveButton: joystick button 0 + gravity: 0 + dead: 0 + sensitivity: 0 + snap: 0 + invert: 0 + type: 0 + axis: 0 + joyNum: 0 + - serializedVersion: 3 + m_Name: Debug Persistent + descriptiveName: + descriptiveNegativeName: + negativeButton: + positiveButton: right shift + altNegativeButton: + altPositiveButton: joystick button 2 + gravity: 0 + dead: 0 + sensitivity: 0 + snap: 0 + invert: 0 + type: 0 + axis: 0 + joyNum: 0 + - serializedVersion: 3 + m_Name: Debug Multiplier + descriptiveName: + descriptiveNegativeName: + negativeButton: + positiveButton: left shift + altNegativeButton: + altPositiveButton: joystick button 3 + gravity: 0 + dead: 0 + sensitivity: 0 + snap: 0 + invert: 0 + type: 0 + axis: 0 + joyNum: 0 + - serializedVersion: 3 + m_Name: Debug Horizontal + descriptiveName: + descriptiveNegativeName: + negativeButton: left + positiveButton: right + altNegativeButton: + altPositiveButton: + gravity: 1000 + dead: 0.001 + sensitivity: 1000 + snap: 0 + invert: 0 + type: 0 + axis: 0 + joyNum: 0 + - serializedVersion: 3 + m_Name: Debug Vertical + descriptiveName: + descriptiveNegativeName: + negativeButton: down + positiveButton: up + altNegativeButton: + altPositiveButton: + gravity: 1000 + dead: 0.001 + sensitivity: 1000 + snap: 0 + invert: 0 + type: 0 + axis: 0 + joyNum: 0 + - serializedVersion: 3 + m_Name: Debug Vertical + descriptiveName: + descriptiveNegativeName: + negativeButton: down + positiveButton: up + altNegativeButton: + altPositiveButton: + gravity: 1000 + dead: 0.001 + sensitivity: 1000 + snap: 0 + invert: 0 + type: 2 + axis: 6 + joyNum: 0 + - serializedVersion: 3 + m_Name: Debug Horizontal + descriptiveName: + descriptiveNegativeName: + negativeButton: left + positiveButton: right + altNegativeButton: + altPositiveButton: + gravity: 1000 + dead: 0.001 + sensitivity: 1000 + snap: 0 + invert: 0 + type: 2 + axis: 5 + joyNum: 0 diff --git a/templates/unity/ProjectSettings/MemorySettings.asset b/templates/unity/ProjectSettings/MemorySettings.asset new file mode 100644 index 0000000000..5b5facecac --- /dev/null +++ b/templates/unity/ProjectSettings/MemorySettings.asset @@ -0,0 +1,35 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!387306366 &1 +MemorySettings: + m_ObjectHideFlags: 0 + m_EditorMemorySettings: + m_MainAllocatorBlockSize: -1 + m_ThreadAllocatorBlockSize: -1 + m_MainGfxBlockSize: -1 + m_ThreadGfxBlockSize: -1 + m_CacheBlockSize: -1 + m_TypetreeBlockSize: -1 + m_ProfilerBlockSize: -1 + m_ProfilerEditorBlockSize: -1 + m_BucketAllocatorGranularity: -1 + m_BucketAllocatorBucketsCount: -1 + m_BucketAllocatorBlockSize: -1 + m_BucketAllocatorBlockCount: -1 + m_ProfilerBucketAllocatorGranularity: -1 + m_ProfilerBucketAllocatorBucketsCount: -1 + m_ProfilerBucketAllocatorBlockSize: -1 + m_ProfilerBucketAllocatorBlockCount: -1 + m_TempAllocatorSizeMain: -1 + m_JobTempAllocatorBlockSize: -1 + m_BackgroundJobTempAllocatorBlockSize: -1 + m_JobTempAllocatorReducedBlockSize: -1 + m_TempAllocatorSizeGIBakingWorker: -1 + m_TempAllocatorSizeNavMeshWorker: -1 + m_TempAllocatorSizeAudioWorker: -1 + m_TempAllocatorSizeCloudWorker: -1 + m_TempAllocatorSizeGfx: -1 + m_TempAllocatorSizeJobWorker: -1 + m_TempAllocatorSizeBackgroundWorker: -1 + m_TempAllocatorSizePreloadManager: -1 + m_PlatformMemorySettings: {} diff --git a/templates/unity/ProjectSettings/NavMeshAreas.asset b/templates/unity/ProjectSettings/NavMeshAreas.asset new file mode 100644 index 0000000000..ad2654e02e --- /dev/null +++ b/templates/unity/ProjectSettings/NavMeshAreas.asset @@ -0,0 +1,93 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!126 &1 +NavMeshProjectSettings: + m_ObjectHideFlags: 0 + serializedVersion: 2 + areas: + - name: Walkable + cost: 1 + - name: Not Walkable + cost: 1 + - name: Jump + cost: 2 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + m_LastAgentTypeID: -887442657 + m_Settings: + - serializedVersion: 2 + agentTypeID: 0 + agentRadius: 0.5 + agentHeight: 2 + agentSlope: 45 + agentClimb: 0.75 + ledgeDropHeight: 0 + maxJumpAcrossDistance: 0 + minRegionArea: 2 + manualCellSize: 0 + cellSize: 0.16666667 + manualTileSize: 0 + tileSize: 256 + accuratePlacement: 0 + maxJobWorkers: 0 + preserveTilesOutsideBounds: 0 + debug: + m_Flags: 0 + m_SettingNames: + - Humanoid diff --git a/templates/unity/ProjectSettings/NetworkManager.asset b/templates/unity/ProjectSettings/NetworkManager.asset new file mode 100644 index 0000000000..5dc6a831d9 --- /dev/null +++ b/templates/unity/ProjectSettings/NetworkManager.asset @@ -0,0 +1,8 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!149 &1 +NetworkManager: + m_ObjectHideFlags: 0 + m_DebugLevel: 0 + m_Sendrate: 15 + m_AssetToPrefab: {} diff --git a/templates/unity/ProjectSettings/PackageManagerSettings.asset b/templates/unity/ProjectSettings/PackageManagerSettings.asset new file mode 100644 index 0000000000..b3a65dda68 --- /dev/null +++ b/templates/unity/ProjectSettings/PackageManagerSettings.asset @@ -0,0 +1,44 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &1 +MonoBehaviour: + m_ObjectHideFlags: 61 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 13964, guid: 0000000000000000e000000000000000, type: 0} + m_Name: + m_EditorClassIdentifier: + m_EnablePreReleasePackages: 0 + m_EnablePackageDependencies: 0 + m_AdvancedSettingsExpanded: 1 + m_ScopedRegistriesSettingsExpanded: 1 + m_SeeAllPackageVersions: 0 + oneTimeWarningShown: 0 + m_Registries: + - m_Id: main + m_Name: + m_Url: https://packages.unity.com + m_Scopes: [] + m_IsDefault: 1 + m_Capabilities: 7 + m_UserSelectedRegistryName: + m_UserAddingNewScopedRegistry: 0 + m_RegistryInfoDraft: + m_ErrorMessage: + m_Original: + m_Id: + m_Name: + m_Url: + m_Scopes: [] + m_IsDefault: 0 + m_Capabilities: 0 + m_Modified: 0 + m_Name: + m_Url: + m_Scopes: + - + m_SelectedScopeIndex: 0 diff --git a/templates/unity/ProjectSettings/Physics2DSettings.asset b/templates/unity/ProjectSettings/Physics2DSettings.asset new file mode 100644 index 0000000000..6cfcddaacd --- /dev/null +++ b/templates/unity/ProjectSettings/Physics2DSettings.asset @@ -0,0 +1,56 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!19 &1 +Physics2DSettings: + m_ObjectHideFlags: 0 + serializedVersion: 5 + m_Gravity: {x: 0, y: -9.81} + m_DefaultMaterial: {fileID: 0} + m_VelocityIterations: 8 + m_PositionIterations: 3 + m_VelocityThreshold: 1 + m_MaxLinearCorrection: 0.2 + m_MaxAngularCorrection: 8 + m_MaxTranslationSpeed: 100 + m_MaxRotationSpeed: 360 + m_BaumgarteScale: 0.2 + m_BaumgarteTimeOfImpactScale: 0.75 + m_TimeToSleep: 0.5 + m_LinearSleepTolerance: 0.01 + m_AngularSleepTolerance: 2 + m_DefaultContactOffset: 0.01 + m_JobOptions: + serializedVersion: 2 + useMultithreading: 0 + useConsistencySorting: 0 + m_InterpolationPosesPerJob: 100 + m_NewContactsPerJob: 30 + m_CollideContactsPerJob: 100 + m_ClearFlagsPerJob: 200 + m_ClearBodyForcesPerJob: 200 + m_SyncDiscreteFixturesPerJob: 50 + m_SyncContinuousFixturesPerJob: 50 + m_FindNearestContactsPerJob: 100 + m_UpdateTriggerContactsPerJob: 100 + m_IslandSolverCostThreshold: 100 + m_IslandSolverBodyCostScale: 1 + m_IslandSolverContactCostScale: 10 + m_IslandSolverJointCostScale: 10 + m_IslandSolverBodiesPerJob: 50 + m_IslandSolverContactsPerJob: 50 + m_SimulationMode: 0 + m_QueriesHitTriggers: 1 + m_QueriesStartInColliders: 1 + m_CallbacksOnDisable: 1 + m_ReuseCollisionCallbacks: 1 + m_AutoSyncTransforms: 0 + m_AlwaysShowColliders: 0 + m_ShowColliderSleep: 1 + m_ShowColliderContacts: 0 + m_ShowColliderAABB: 0 + m_ContactArrowScale: 0.2 + m_ColliderAwakeColor: {r: 0.5686275, g: 0.95686275, b: 0.54509807, a: 0.7529412} + m_ColliderAsleepColor: {r: 0.5686275, g: 0.95686275, b: 0.54509807, a: 0.36078432} + m_ColliderContactColor: {r: 1, g: 0, b: 1, a: 0.6862745} + m_ColliderAABBColor: {r: 1, g: 1, b: 0, a: 0.2509804} + m_LayerCollisionMatrix: ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff diff --git a/templates/unity/ProjectSettings/PresetManager.asset b/templates/unity/ProjectSettings/PresetManager.asset new file mode 100644 index 0000000000..67a94daefe --- /dev/null +++ b/templates/unity/ProjectSettings/PresetManager.asset @@ -0,0 +1,7 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!1386491679 &1 +PresetManager: + m_ObjectHideFlags: 0 + serializedVersion: 2 + m_DefaultPresets: {} diff --git a/templates/unity/ProjectSettings/ProjectSettings.asset b/templates/unity/ProjectSettings/ProjectSettings.asset new file mode 100644 index 0000000000..d367bab888 --- /dev/null +++ b/templates/unity/ProjectSettings/ProjectSettings.asset @@ -0,0 +1,782 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!129 &1 +PlayerSettings: + m_ObjectHideFlags: 0 + serializedVersion: 24 + productGUID: 4ab987bef3577704db7ede380ba94997 + AndroidProfiler: 0 + AndroidFilterTouchesWhenObscured: 0 + AndroidEnableSustainedPerformanceMode: 0 + defaultScreenOrientation: 4 + targetDevice: 2 + useOnDemandResources: 0 + accelerometerFrequency: 60 + companyName: DefaultCompany + productName: AppwriteTemplateSDK + defaultCursor: {fileID: 0} + cursorHotspot: {x: 0, y: 0} + m_SplashScreenBackgroundColor: {r: 0.13725491, g: 0.12156863, b: 0.1254902, a: 1} + m_ShowUnitySplashScreen: 1 + m_ShowUnitySplashLogo: 1 + m_SplashScreenOverlayOpacity: 1 + m_SplashScreenAnimation: 1 + m_SplashScreenLogoStyle: 1 + m_SplashScreenDrawMode: 0 + m_SplashScreenBackgroundAnimationZoom: 1 + m_SplashScreenLogoAnimationZoom: 1 + m_SplashScreenBackgroundLandscapeAspect: 1 + m_SplashScreenBackgroundPortraitAspect: 1 + m_SplashScreenBackgroundLandscapeUvs: + serializedVersion: 2 + x: 0 + y: 0 + width: 1 + height: 1 + m_SplashScreenBackgroundPortraitUvs: + serializedVersion: 2 + x: 0 + y: 0 + width: 1 + height: 1 + m_SplashScreenLogos: [] + m_VirtualRealitySplashScreen: {fileID: 0} + m_HolographicTrackingLossScreen: {fileID: 0} + defaultScreenWidth: 1920 + defaultScreenHeight: 1080 + defaultScreenWidthWeb: 960 + defaultScreenHeightWeb: 600 + m_StereoRenderingPath: 0 + m_ActiveColorSpace: 0 + m_MTRendering: 1 + mipStripping: 0 + numberOfMipsStripped: 0 + m_StackTraceTypes: 010000000100000001000000010000000100000001000000 + iosShowActivityIndicatorOnLoading: -1 + androidShowActivityIndicatorOnLoading: -1 + iosUseCustomAppBackgroundBehavior: 0 + iosAllowHTTPDownload: 1 + allowedAutorotateToPortrait: 1 + allowedAutorotateToPortraitUpsideDown: 1 + allowedAutorotateToLandscapeRight: 1 + allowedAutorotateToLandscapeLeft: 1 + useOSAutorotation: 1 + use32BitDisplayBuffer: 1 + preserveFramebufferAlpha: 0 + disableDepthAndStencilBuffers: 0 + androidStartInFullscreen: 1 + androidRenderOutsideSafeArea: 1 + androidUseSwappy: 1 + androidBlitType: 0 + androidResizableWindow: 0 + androidDefaultWindowWidth: 1920 + androidDefaultWindowHeight: 1080 + androidMinimumWindowWidth: 400 + androidMinimumWindowHeight: 300 + androidFullscreenMode: 1 + defaultIsNativeResolution: 1 + macRetinaSupport: 1 + runInBackground: 0 + captureSingleScreen: 0 + muteOtherAudioSources: 0 + Prepare IOS For Recording: 0 + Force IOS Speakers When Recording: 0 + deferSystemGesturesMode: 0 + hideHomeButton: 0 + submitAnalytics: 1 + usePlayerLog: 1 + bakeCollisionMeshes: 0 + forceSingleInstance: 0 + useFlipModelSwapchain: 1 + resizableWindow: 0 + useMacAppStoreValidation: 0 + macAppStoreCategory: public.app-category.games + gpuSkinning: 0 + xboxPIXTextureCapture: 0 + xboxEnableAvatar: 0 + xboxEnableKinect: 0 + xboxEnableKinectAutoTracking: 0 + xboxEnableFitness: 0 + visibleInBackground: 1 + allowFullscreenSwitch: 1 + fullscreenMode: 1 + xboxSpeechDB: 0 + xboxEnableHeadOrientation: 0 + xboxEnableGuest: 0 + xboxEnablePIXSampling: 0 + metalFramebufferOnly: 0 + xboxOneResolution: 0 + xboxOneSResolution: 0 + xboxOneXResolution: 3 + xboxOneMonoLoggingLevel: 0 + xboxOneLoggingLevel: 1 + xboxOneDisableEsram: 0 + xboxOneEnableTypeOptimization: 0 + xboxOnePresentImmediateThreshold: 0 + switchQueueCommandMemory: 1048576 + switchQueueControlMemory: 16384 + switchQueueComputeMemory: 262144 + switchNVNShaderPoolsGranularity: 33554432 + switchNVNDefaultPoolsGranularity: 16777216 + switchNVNOtherPoolsGranularity: 16777216 + switchNVNMaxPublicTextureIDCount: 0 + switchNVNMaxPublicSamplerIDCount: 0 + switchMaxWorkerMultiple: 8 + stadiaPresentMode: 0 + stadiaTargetFramerate: 0 + vulkanNumSwapchainBuffers: 3 + vulkanEnableSetSRGBWrite: 0 + vulkanEnablePreTransform: 0 + vulkanEnableLateAcquireNextImage: 0 + vulkanEnableCommandBufferRecycling: 1 + m_SupportedAspectRatios: + 4:3: 1 + 5:4: 1 + 16:10: 1 + 16:9: 1 + Others: 1 + bundleVersion: 1.0 + preloadedAssets: [] + metroInputSource: 0 + wsaTransparentSwapchain: 0 + m_HolographicPauseOnTrackingLoss: 1 + xboxOneDisableKinectGpuReservation: 1 + xboxOneEnable7thCore: 1 + vrSettings: + enable360StereoCapture: 0 + isWsaHolographicRemotingEnabled: 0 + enableFrameTimingStats: 0 + enableOpenGLProfilerGPURecorders: 1 + useHDRDisplay: 0 + D3DHDRBitDepth: 0 + m_ColorGamuts: 00000000 + targetPixelDensity: 30 + resolutionScalingMode: 0 + resetResolutionOnWindowResize: 0 + androidSupportedAspectRatio: 1 + androidMaxAspectRatio: 2.1 + applicationIdentifier: + Standalone: com.DefaultCompany.2DProject + buildNumber: + Standalone: 0 + iPhone: 0 + tvOS: 0 + overrideDefaultApplicationIdentifier: 1 + AndroidBundleVersionCode: 1 + AndroidMinSdkVersion: 22 + AndroidTargetSdkVersion: 0 + AndroidPreferredInstallLocation: 1 + aotOptions: + stripEngineCode: 1 + iPhoneStrippingLevel: 0 + iPhoneScriptCallOptimization: 0 + ForceInternetPermission: 0 + ForceSDCardPermission: 0 + CreateWallpaper: 0 + APKExpansionFiles: 0 + keepLoadedShadersAlive: 0 + StripUnusedMeshComponents: 0 + VertexChannelCompressionMask: 4054 + iPhoneSdkVersion: 988 + iOSTargetOSVersionString: 12.0 + tvOSSdkVersion: 0 + tvOSRequireExtendedGameController: 0 + tvOSTargetOSVersionString: 12.0 + uIPrerenderedIcon: 0 + uIRequiresPersistentWiFi: 0 + uIRequiresFullScreen: 1 + uIStatusBarHidden: 1 + uIExitOnSuspend: 0 + uIStatusBarStyle: 0 + appleTVSplashScreen: {fileID: 0} + appleTVSplashScreen2x: {fileID: 0} + tvOSSmallIconLayers: [] + tvOSSmallIconLayers2x: [] + tvOSLargeIconLayers: [] + tvOSLargeIconLayers2x: [] + tvOSTopShelfImageLayers: [] + tvOSTopShelfImageLayers2x: [] + tvOSTopShelfImageWideLayers: [] + tvOSTopShelfImageWideLayers2x: [] + iOSLaunchScreenType: 0 + iOSLaunchScreenPortrait: {fileID: 0} + iOSLaunchScreenLandscape: {fileID: 0} + iOSLaunchScreenBackgroundColor: + serializedVersion: 2 + rgba: 0 + iOSLaunchScreenFillPct: 100 + iOSLaunchScreenSize: 100 + iOSLaunchScreenCustomXibPath: + iOSLaunchScreeniPadType: 0 + iOSLaunchScreeniPadImage: {fileID: 0} + iOSLaunchScreeniPadBackgroundColor: + serializedVersion: 2 + rgba: 0 + iOSLaunchScreeniPadFillPct: 100 + iOSLaunchScreeniPadSize: 100 + iOSLaunchScreeniPadCustomXibPath: + iOSLaunchScreenCustomStoryboardPath: + iOSLaunchScreeniPadCustomStoryboardPath: + iOSDeviceRequirements: [] + iOSURLSchemes: [] + macOSURLSchemes: [] + iOSBackgroundModes: 0 + iOSMetalForceHardShadows: 0 + metalEditorSupport: 1 + metalAPIValidation: 1 + iOSRenderExtraFrameOnPause: 0 + iosCopyPluginsCodeInsteadOfSymlink: 0 + appleDeveloperTeamID: + iOSManualSigningProvisioningProfileID: + tvOSManualSigningProvisioningProfileID: + iOSManualSigningProvisioningProfileType: 0 + tvOSManualSigningProvisioningProfileType: 0 + appleEnableAutomaticSigning: 0 + iOSRequireARKit: 0 + iOSAutomaticallyDetectAndAddCapabilities: 1 + appleEnableProMotion: 0 + shaderPrecisionModel: 0 + clonedFromGUID: 10ad67313f4034357812315f3c407484 + templatePackageId: com.unity.template.2d@6.1.2 + templateDefaultScene: Assets/Scenes/SampleScene.unity + useCustomMainManifest: 0 + useCustomLauncherManifest: 0 + useCustomMainGradleTemplate: 0 + useCustomLauncherGradleManifest: 0 + useCustomBaseGradleTemplate: 0 + useCustomGradlePropertiesTemplate: 0 + useCustomProguardFile: 0 + AndroidTargetArchitectures: 1 + AndroidTargetDevices: 0 + AndroidSplashScreenScale: 0 + androidSplashScreen: {fileID: 0} + AndroidKeystoreName: + AndroidKeyaliasName: + AndroidBuildApkPerCpuArchitecture: 0 + AndroidTVCompatibility: 0 + AndroidIsGame: 1 + AndroidEnableTango: 0 + androidEnableBanner: 1 + androidUseLowAccuracyLocation: 0 + androidUseCustomKeystore: 0 + m_AndroidBanners: + - width: 320 + height: 180 + banner: {fileID: 0} + androidGamepadSupportLevel: 0 + chromeosInputEmulation: 1 + AndroidMinifyWithR8: 0 + AndroidMinifyRelease: 0 + AndroidMinifyDebug: 0 + AndroidValidateAppBundleSize: 1 + AndroidAppBundleSizeToValidate: 150 + m_BuildTargetIcons: [] + m_BuildTargetPlatformIcons: + - m_BuildTarget: Android + m_Icons: + - m_Textures: [] + m_Width: 432 + m_Height: 432 + m_Kind: 2 + m_SubKind: + - m_Textures: [] + m_Width: 324 + m_Height: 324 + m_Kind: 2 + m_SubKind: + - m_Textures: [] + m_Width: 216 + m_Height: 216 + m_Kind: 2 + m_SubKind: + - m_Textures: [] + m_Width: 162 + m_Height: 162 + m_Kind: 2 + m_SubKind: + - m_Textures: [] + m_Width: 108 + m_Height: 108 + m_Kind: 2 + m_SubKind: + - m_Textures: [] + m_Width: 81 + m_Height: 81 + m_Kind: 2 + m_SubKind: + - m_Textures: [] + m_Width: 192 + m_Height: 192 + m_Kind: 1 + m_SubKind: + - m_Textures: [] + m_Width: 144 + m_Height: 144 + m_Kind: 1 + m_SubKind: + - m_Textures: [] + m_Width: 96 + m_Height: 96 + m_Kind: 1 + m_SubKind: + - m_Textures: [] + m_Width: 72 + m_Height: 72 + m_Kind: 1 + m_SubKind: + - m_Textures: [] + m_Width: 48 + m_Height: 48 + m_Kind: 1 + m_SubKind: + - m_Textures: [] + m_Width: 36 + m_Height: 36 + m_Kind: 1 + m_SubKind: + - m_Textures: [] + m_Width: 192 + m_Height: 192 + m_Kind: 0 + m_SubKind: + - m_Textures: [] + m_Width: 144 + m_Height: 144 + m_Kind: 0 + m_SubKind: + - m_Textures: [] + m_Width: 96 + m_Height: 96 + m_Kind: 0 + m_SubKind: + - m_Textures: [] + m_Width: 72 + m_Height: 72 + m_Kind: 0 + m_SubKind: + - m_Textures: [] + m_Width: 48 + m_Height: 48 + m_Kind: 0 + m_SubKind: + - m_Textures: [] + m_Width: 36 + m_Height: 36 + m_Kind: 0 + m_SubKind: + m_BuildTargetBatching: [] + m_BuildTargetShaderSettings: [] + m_BuildTargetGraphicsJobs: + - m_BuildTarget: MacStandaloneSupport + m_GraphicsJobs: 0 + - m_BuildTarget: Switch + m_GraphicsJobs: 0 + - m_BuildTarget: MetroSupport + m_GraphicsJobs: 0 + - m_BuildTarget: AppleTVSupport + m_GraphicsJobs: 0 + - m_BuildTarget: BJMSupport + m_GraphicsJobs: 0 + - m_BuildTarget: LinuxStandaloneSupport + m_GraphicsJobs: 0 + - m_BuildTarget: PS4Player + m_GraphicsJobs: 0 + - m_BuildTarget: iOSSupport + m_GraphicsJobs: 0 + - m_BuildTarget: WindowsStandaloneSupport + m_GraphicsJobs: 0 + - m_BuildTarget: XboxOnePlayer + m_GraphicsJobs: 0 + - m_BuildTarget: LuminSupport + m_GraphicsJobs: 0 + - m_BuildTarget: AndroidPlayer + m_GraphicsJobs: 0 + - m_BuildTarget: WebGLSupport + m_GraphicsJobs: 0 + m_BuildTargetGraphicsJobMode: [] + m_BuildTargetGraphicsAPIs: + - m_BuildTarget: AndroidPlayer + m_APIs: 150000000b000000 + m_Automatic: 1 + - m_BuildTarget: iOSSupport + m_APIs: 10000000 + m_Automatic: 1 + m_BuildTargetVRSettings: [] + m_DefaultShaderChunkSizeInMB: 16 + m_DefaultShaderChunkCount: 0 + openGLRequireES31: 0 + openGLRequireES31AEP: 0 + openGLRequireES32: 0 + m_TemplateCustomTags: {} + mobileMTRendering: + Android: 1 + iPhone: 1 + tvOS: 1 + m_BuildTargetGroupLightmapEncodingQuality: [] + m_BuildTargetGroupLightmapSettings: [] + m_BuildTargetNormalMapEncoding: [] + m_BuildTargetDefaultTextureCompressionFormat: + - m_BuildTarget: Android + m_Format: 3 + playModeTestRunnerEnabled: 0 + runPlayModeTestAsEditModeTest: 0 + actionOnDotNetUnhandledException: 1 + enableInternalProfiler: 0 + logObjCUncaughtExceptions: 1 + enableCrashReportAPI: 0 + cameraUsageDescription: + locationUsageDescription: + microphoneUsageDescription: + bluetoothUsageDescription: + switchNMETAOverride: + switchNetLibKey: + switchSocketMemoryPoolSize: 6144 + switchSocketAllocatorPoolSize: 128 + switchSocketConcurrencyLimit: 14 + switchScreenResolutionBehavior: 2 + switchUseCPUProfiler: 0 + switchEnableFileSystemTrace: 0 + switchUseGOLDLinker: 0 + switchLTOSetting: 0 + switchApplicationID: 0x01004b9000490000 + switchNSODependencies: + switchTitleNames_0: + switchTitleNames_1: + switchTitleNames_2: + switchTitleNames_3: + switchTitleNames_4: + switchTitleNames_5: + switchTitleNames_6: + switchTitleNames_7: + switchTitleNames_8: + switchTitleNames_9: + switchTitleNames_10: + switchTitleNames_11: + switchTitleNames_12: + switchTitleNames_13: + switchTitleNames_14: + switchTitleNames_15: + switchPublisherNames_0: + switchPublisherNames_1: + switchPublisherNames_2: + switchPublisherNames_3: + switchPublisherNames_4: + switchPublisherNames_5: + switchPublisherNames_6: + switchPublisherNames_7: + switchPublisherNames_8: + switchPublisherNames_9: + switchPublisherNames_10: + switchPublisherNames_11: + switchPublisherNames_12: + switchPublisherNames_13: + switchPublisherNames_14: + switchPublisherNames_15: + switchIcons_0: {fileID: 0} + switchIcons_1: {fileID: 0} + switchIcons_2: {fileID: 0} + switchIcons_3: {fileID: 0} + switchIcons_4: {fileID: 0} + switchIcons_5: {fileID: 0} + switchIcons_6: {fileID: 0} + switchIcons_7: {fileID: 0} + switchIcons_8: {fileID: 0} + switchIcons_9: {fileID: 0} + switchIcons_10: {fileID: 0} + switchIcons_11: {fileID: 0} + switchIcons_12: {fileID: 0} + switchIcons_13: {fileID: 0} + switchIcons_14: {fileID: 0} + switchIcons_15: {fileID: 0} + switchSmallIcons_0: {fileID: 0} + switchSmallIcons_1: {fileID: 0} + switchSmallIcons_2: {fileID: 0} + switchSmallIcons_3: {fileID: 0} + switchSmallIcons_4: {fileID: 0} + switchSmallIcons_5: {fileID: 0} + switchSmallIcons_6: {fileID: 0} + switchSmallIcons_7: {fileID: 0} + switchSmallIcons_8: {fileID: 0} + switchSmallIcons_9: {fileID: 0} + switchSmallIcons_10: {fileID: 0} + switchSmallIcons_11: {fileID: 0} + switchSmallIcons_12: {fileID: 0} + switchSmallIcons_13: {fileID: 0} + switchSmallIcons_14: {fileID: 0} + switchSmallIcons_15: {fileID: 0} + switchManualHTML: + switchAccessibleURLs: + switchLegalInformation: + switchMainThreadStackSize: 1048576 + switchPresenceGroupId: + switchLogoHandling: 0 + switchReleaseVersion: 0 + switchDisplayVersion: 1.0.0 + switchStartupUserAccount: 0 + switchSupportedLanguagesMask: 0 + switchLogoType: 0 + switchApplicationErrorCodeCategory: + switchUserAccountSaveDataSize: 0 + switchUserAccountSaveDataJournalSize: 0 + switchApplicationAttribute: 0 + switchCardSpecSize: -1 + switchCardSpecClock: -1 + switchRatingsMask: 0 + switchRatingsInt_0: 0 + switchRatingsInt_1: 0 + switchRatingsInt_2: 0 + switchRatingsInt_3: 0 + switchRatingsInt_4: 0 + switchRatingsInt_5: 0 + switchRatingsInt_6: 0 + switchRatingsInt_7: 0 + switchRatingsInt_8: 0 + switchRatingsInt_9: 0 + switchRatingsInt_10: 0 + switchRatingsInt_11: 0 + switchRatingsInt_12: 0 + switchLocalCommunicationIds_0: + switchLocalCommunicationIds_1: + switchLocalCommunicationIds_2: + switchLocalCommunicationIds_3: + switchLocalCommunicationIds_4: + switchLocalCommunicationIds_5: + switchLocalCommunicationIds_6: + switchLocalCommunicationIds_7: + switchParentalControl: 0 + switchAllowsScreenshot: 1 + switchAllowsVideoCapturing: 1 + switchAllowsRuntimeAddOnContentInstall: 0 + switchDataLossConfirmation: 0 + switchUserAccountLockEnabled: 0 + switchSystemResourceMemory: 16777216 + switchSupportedNpadStyles: 22 + switchNativeFsCacheSize: 32 + switchIsHoldTypeHorizontal: 0 + switchSupportedNpadCount: 8 + switchEnableTouchScreen: 1 + switchSocketConfigEnabled: 0 + switchTcpInitialSendBufferSize: 32 + switchTcpInitialReceiveBufferSize: 64 + switchTcpAutoSendBufferSizeMax: 256 + switchTcpAutoReceiveBufferSizeMax: 256 + switchUdpSendBufferSize: 9 + switchUdpReceiveBufferSize: 42 + switchSocketBufferEfficiency: 4 + switchSocketInitializeEnabled: 1 + switchNetworkInterfaceManagerInitializeEnabled: 1 + switchPlayerConnectionEnabled: 1 + switchUseNewStyleFilepaths: 0 + switchUseLegacyFmodPriorities: 1 + switchUseMicroSleepForYield: 1 + switchEnableRamDiskSupport: 0 + switchMicroSleepForYieldTime: 25 + switchRamDiskSpaceSize: 12 + ps4NPAgeRating: 12 + ps4NPTitleSecret: + ps4NPTrophyPackPath: + ps4ParentalLevel: 11 + ps4ContentID: ED1633-NPXX51362_00-0000000000000000 + ps4Category: 0 + ps4MasterVersion: 01.00 + ps4AppVersion: 01.00 + ps4AppType: 0 + ps4ParamSfxPath: + ps4VideoOutPixelFormat: 0 + ps4VideoOutInitialWidth: 1920 + ps4VideoOutBaseModeInitialWidth: 1920 + ps4VideoOutReprojectionRate: 60 + ps4PronunciationXMLPath: + ps4PronunciationSIGPath: + ps4BackgroundImagePath: + ps4StartupImagePath: + ps4StartupImagesFolder: + ps4IconImagesFolder: + ps4SaveDataImagePath: + ps4SdkOverride: + ps4BGMPath: + ps4ShareFilePath: + ps4ShareOverlayImagePath: + ps4PrivacyGuardImagePath: + ps4ExtraSceSysFile: + ps4NPtitleDatPath: + ps4RemotePlayKeyAssignment: -1 + ps4RemotePlayKeyMappingDir: + ps4PlayTogetherPlayerCount: 0 + ps4EnterButtonAssignment: 2 + ps4ApplicationParam1: 0 + ps4ApplicationParam2: 0 + ps4ApplicationParam3: 0 + ps4ApplicationParam4: 0 + ps4DownloadDataSize: 0 + ps4GarlicHeapSize: 2048 + ps4ProGarlicHeapSize: 2560 + playerPrefsMaxSize: 32768 + ps4Passcode: bi9UOuSpM2Tlh01vOzwvSikHFswuzleh + ps4pnSessions: 1 + ps4pnPresence: 1 + ps4pnFriends: 1 + ps4pnGameCustomData: 1 + playerPrefsSupport: 0 + enableApplicationExit: 0 + resetTempFolder: 1 + restrictedAudioUsageRights: 0 + ps4UseResolutionFallback: 0 + ps4ReprojectionSupport: 0 + ps4UseAudio3dBackend: 0 + ps4UseLowGarlicFragmentationMode: 1 + ps4SocialScreenEnabled: 0 + ps4ScriptOptimizationLevel: 2 + ps4Audio3dVirtualSpeakerCount: 14 + ps4attribCpuUsage: 0 + ps4PatchPkgPath: + ps4PatchLatestPkgPath: + ps4PatchChangeinfoPath: + ps4PatchDayOne: 0 + ps4attribUserManagement: 0 + ps4attribMoveSupport: 0 + ps4attrib3DSupport: 0 + ps4attribShareSupport: 0 + ps4attribExclusiveVR: 0 + ps4disableAutoHideSplash: 0 + ps4videoRecordingFeaturesUsed: 0 + ps4contentSearchFeaturesUsed: 0 + ps4CompatibilityPS5: 0 + ps4AllowPS5Detection: 0 + ps4GPU800MHz: 1 + ps4attribEyeToEyeDistanceSettingVR: 0 + ps4IncludedModules: [] + ps4attribVROutputEnabled: 0 + monoEnv: + splashScreenBackgroundSourceLandscape: {fileID: 0} + splashScreenBackgroundSourcePortrait: {fileID: 0} + blurSplashScreenBackground: 1 + spritePackerPolicy: + webGLMemorySize: 32 + webGLExceptionSupport: 1 + webGLNameFilesAsHashes: 0 + webGLDataCaching: 1 + webGLDebugSymbols: 0 + webGLEmscriptenArgs: + webGLModulesDirectory: + webGLTemplate: APPLICATION:Default + webGLAnalyzeBuildSize: 0 + webGLUseEmbeddedResources: 0 + webGLCompressionFormat: 0 + webGLWasmArithmeticExceptions: 0 + webGLLinkerTarget: 1 + webGLThreadsSupport: 0 + webGLDecompressionFallback: 0 + webGLPowerPreference: 2 + scriptingDefineSymbols: {} + additionalCompilerArguments: {} + platformArchitecture: {} + scriptingBackend: {} + il2cppCompilerConfiguration: {} + managedStrippingLevel: + EmbeddedLinux: 1 + GameCoreScarlett: 1 + GameCoreXboxOne: 1 + Lumin: 1 + Nintendo Switch: 1 + PS4: 1 + PS5: 1 + Stadia: 1 + WebGL: 1 + Windows Store Apps: 1 + XboxOne: 1 + iPhone: 1 + tvOS: 1 + incrementalIl2cppBuild: {} + suppressCommonWarnings: 1 + allowUnsafeCode: 0 + useDeterministicCompilation: 1 + enableRoslynAnalyzers: 1 + additionalIl2CppArgs: + scriptingRuntimeVersion: 1 + gcIncremental: 1 + assemblyVersionValidation: 1 + gcWBarrierValidation: 0 + apiCompatibilityLevelPerPlatform: {} + m_RenderingPath: 1 + m_MobileRenderingPath: 1 + metroPackageName: 2D_BuiltInRenderer + metroPackageVersion: + metroCertificatePath: + metroCertificatePassword: + metroCertificateSubject: + metroCertificateIssuer: + metroCertificateNotAfter: 0000000000000000 + metroApplicationDescription: 2D_BuiltInRenderer + wsaImages: {} + metroTileShortName: + metroTileShowName: 0 + metroMediumTileShowName: 0 + metroLargeTileShowName: 0 + metroWideTileShowName: 0 + metroSupportStreamingInstall: 0 + metroLastRequiredScene: 0 + metroDefaultTileSize: 1 + metroTileForegroundText: 2 + metroTileBackgroundColor: {r: 0.13333334, g: 0.17254902, b: 0.21568628, a: 0} + metroSplashScreenBackgroundColor: {r: 0.12941177, g: 0.17254902, b: 0.21568628, a: 1} + metroSplashScreenUseBackgroundColor: 0 + platformCapabilities: {} + metroTargetDeviceFamilies: {} + metroFTAName: + metroFTAFileTypes: [] + metroProtocolName: + vcxProjDefaultLanguage: + XboxOneProductId: + XboxOneUpdateKey: + XboxOneSandboxId: + XboxOneContentId: + XboxOneTitleId: + XboxOneSCId: + XboxOneGameOsOverridePath: + XboxOnePackagingOverridePath: + XboxOneAppManifestOverridePath: + XboxOneVersion: 1.0.0.0 + XboxOnePackageEncryption: 0 + XboxOnePackageUpdateGranularity: 2 + XboxOneDescription: + XboxOneLanguage: + - enus + XboxOneCapability: [] + XboxOneGameRating: {} + XboxOneIsContentPackage: 0 + XboxOneEnhancedXboxCompatibilityMode: 0 + XboxOneEnableGPUVariability: 1 + XboxOneSockets: {} + XboxOneSplashScreen: {fileID: 0} + XboxOneAllowedProductIds: [] + XboxOnePersistentLocalStorageSize: 0 + XboxOneXTitleMemory: 8 + XboxOneOverrideIdentityName: + XboxOneOverrideIdentityPublisher: + vrEditorSettings: {} + cloudServicesEnabled: {} + luminIcon: + m_Name: + m_ModelFolderPath: + m_PortalFolderPath: + luminCert: + m_CertPath: + m_SignPackage: 1 + luminIsChannelApp: 0 + luminVersion: + m_VersionCode: 1 + m_VersionName: + apiCompatibilityLevel: 6 + activeInputHandler: 0 + windowsGamepadBackendHint: 0 + cloudProjectId: a0b72f85-7dbc-4748-aad1-c91100eebf4c + framebufferDepthMemorylessMode: 0 + qualitySettingsNames: [] + projectName: AppwriteTemplateSDK + organizationId: comanda-a + cloudEnabled: 0 + legacyClampBlendShapeWeights: 0 + playerDataPath: + forceSRGBBlit: 1 + virtualTexturingSupportEnabled: 0 diff --git a/templates/unity/ProjectSettings/ProjectVersion.txt b/templates/unity/ProjectSettings/ProjectVersion.txt new file mode 100644 index 0000000000..16ee581cfe --- /dev/null +++ b/templates/unity/ProjectSettings/ProjectVersion.txt @@ -0,0 +1,2 @@ +m_EditorVersion: 2021.3.45f1 +m_EditorVersionWithRevision: 2021.3.45f1 (3409e2af086f) diff --git a/templates/unity/ProjectSettings/QualitySettings.asset b/templates/unity/ProjectSettings/QualitySettings.asset new file mode 100644 index 0000000000..bcd6706535 --- /dev/null +++ b/templates/unity/ProjectSettings/QualitySettings.asset @@ -0,0 +1,239 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!47 &1 +QualitySettings: + m_ObjectHideFlags: 0 + serializedVersion: 5 + m_CurrentQuality: 5 + m_QualitySettings: + - serializedVersion: 2 + name: Very Low + pixelLightCount: 0 + shadows: 0 + shadowResolution: 0 + shadowProjection: 1 + shadowCascades: 1 + shadowDistance: 15 + shadowNearPlaneOffset: 3 + shadowCascade2Split: 0.33333334 + shadowCascade4Split: {x: 0.06666667, y: 0.2, z: 0.46666667} + shadowmaskMode: 0 + skinWeights: 1 + textureQuality: 1 + anisotropicTextures: 0 + antiAliasing: 0 + softParticles: 0 + softVegetation: 0 + realtimeReflectionProbes: 0 + billboardsFaceCameraPosition: 0 + vSyncCount: 0 + lodBias: 0.3 + maximumLODLevel: 0 + streamingMipmapsActive: 0 + streamingMipmapsAddAllCameras: 1 + streamingMipmapsMemoryBudget: 512 + streamingMipmapsRenderersPerFrame: 512 + streamingMipmapsMaxLevelReduction: 2 + streamingMipmapsMaxFileIORequests: 1024 + particleRaycastBudget: 4 + asyncUploadTimeSlice: 2 + asyncUploadBufferSize: 16 + asyncUploadPersistentBuffer: 1 + resolutionScalingFixedDPIFactor: 1 + customRenderPipeline: {fileID: 0} + excludedTargetPlatforms: [] + - serializedVersion: 2 + name: Low + pixelLightCount: 0 + shadows: 0 + shadowResolution: 0 + shadowProjection: 1 + shadowCascades: 1 + shadowDistance: 20 + shadowNearPlaneOffset: 3 + shadowCascade2Split: 0.33333334 + shadowCascade4Split: {x: 0.06666667, y: 0.2, z: 0.46666667} + shadowmaskMode: 0 + skinWeights: 2 + textureQuality: 0 + anisotropicTextures: 0 + antiAliasing: 0 + softParticles: 0 + softVegetation: 0 + realtimeReflectionProbes: 0 + billboardsFaceCameraPosition: 0 + vSyncCount: 0 + lodBias: 0.4 + maximumLODLevel: 0 + streamingMipmapsActive: 0 + streamingMipmapsAddAllCameras: 1 + streamingMipmapsMemoryBudget: 512 + streamingMipmapsRenderersPerFrame: 512 + streamingMipmapsMaxLevelReduction: 2 + streamingMipmapsMaxFileIORequests: 1024 + particleRaycastBudget: 16 + asyncUploadTimeSlice: 2 + asyncUploadBufferSize: 16 + asyncUploadPersistentBuffer: 1 + resolutionScalingFixedDPIFactor: 1 + customRenderPipeline: {fileID: 0} + excludedTargetPlatforms: [] + - serializedVersion: 2 + name: Medium + pixelLightCount: 1 + shadows: 1 + shadowResolution: 0 + shadowProjection: 1 + shadowCascades: 1 + shadowDistance: 20 + shadowNearPlaneOffset: 3 + shadowCascade2Split: 0.33333334 + shadowCascade4Split: {x: 0.06666667, y: 0.2, z: 0.46666667} + shadowmaskMode: 0 + skinWeights: 2 + textureQuality: 0 + anisotropicTextures: 1 + antiAliasing: 0 + softParticles: 0 + softVegetation: 0 + realtimeReflectionProbes: 0 + billboardsFaceCameraPosition: 0 + vSyncCount: 1 + lodBias: 0.7 + maximumLODLevel: 0 + streamingMipmapsActive: 0 + streamingMipmapsAddAllCameras: 1 + streamingMipmapsMemoryBudget: 512 + streamingMipmapsRenderersPerFrame: 512 + streamingMipmapsMaxLevelReduction: 2 + streamingMipmapsMaxFileIORequests: 1024 + particleRaycastBudget: 64 + asyncUploadTimeSlice: 2 + asyncUploadBufferSize: 16 + asyncUploadPersistentBuffer: 1 + resolutionScalingFixedDPIFactor: 1 + customRenderPipeline: {fileID: 0} + excludedTargetPlatforms: [] + - serializedVersion: 2 + name: High + pixelLightCount: 2 + shadows: 2 + shadowResolution: 1 + shadowProjection: 1 + shadowCascades: 2 + shadowDistance: 40 + shadowNearPlaneOffset: 3 + shadowCascade2Split: 0.33333334 + shadowCascade4Split: {x: 0.06666667, y: 0.2, z: 0.46666667} + shadowmaskMode: 1 + skinWeights: 2 + textureQuality: 0 + anisotropicTextures: 1 + antiAliasing: 0 + softParticles: 0 + softVegetation: 1 + realtimeReflectionProbes: 1 + billboardsFaceCameraPosition: 1 + vSyncCount: 1 + lodBias: 1 + maximumLODLevel: 0 + streamingMipmapsActive: 0 + streamingMipmapsAddAllCameras: 1 + streamingMipmapsMemoryBudget: 512 + streamingMipmapsRenderersPerFrame: 512 + streamingMipmapsMaxLevelReduction: 2 + streamingMipmapsMaxFileIORequests: 1024 + particleRaycastBudget: 256 + asyncUploadTimeSlice: 2 + asyncUploadBufferSize: 16 + asyncUploadPersistentBuffer: 1 + resolutionScalingFixedDPIFactor: 1 + customRenderPipeline: {fileID: 0} + excludedTargetPlatforms: [] + - serializedVersion: 2 + name: Very High + pixelLightCount: 3 + shadows: 2 + shadowResolution: 2 + shadowProjection: 1 + shadowCascades: 2 + shadowDistance: 70 + shadowNearPlaneOffset: 3 + shadowCascade2Split: 0.33333334 + shadowCascade4Split: {x: 0.06666667, y: 0.2, z: 0.46666667} + shadowmaskMode: 1 + skinWeights: 4 + textureQuality: 0 + anisotropicTextures: 2 + antiAliasing: 2 + softParticles: 1 + softVegetation: 1 + realtimeReflectionProbes: 1 + billboardsFaceCameraPosition: 1 + vSyncCount: 1 + lodBias: 1.5 + maximumLODLevel: 0 + streamingMipmapsActive: 0 + streamingMipmapsAddAllCameras: 1 + streamingMipmapsMemoryBudget: 512 + streamingMipmapsRenderersPerFrame: 512 + streamingMipmapsMaxLevelReduction: 2 + streamingMipmapsMaxFileIORequests: 1024 + particleRaycastBudget: 1024 + asyncUploadTimeSlice: 2 + asyncUploadBufferSize: 16 + asyncUploadPersistentBuffer: 1 + resolutionScalingFixedDPIFactor: 1 + customRenderPipeline: {fileID: 0} + excludedTargetPlatforms: [] + - serializedVersion: 2 + name: Ultra + pixelLightCount: 4 + shadows: 2 + shadowResolution: 2 + shadowProjection: 1 + shadowCascades: 4 + shadowDistance: 150 + shadowNearPlaneOffset: 3 + shadowCascade2Split: 0.33333334 + shadowCascade4Split: {x: 0.06666667, y: 0.2, z: 0.46666667} + shadowmaskMode: 1 + skinWeights: 255 + textureQuality: 0 + anisotropicTextures: 2 + antiAliasing: 2 + softParticles: 1 + softVegetation: 1 + realtimeReflectionProbes: 1 + billboardsFaceCameraPosition: 1 + vSyncCount: 1 + lodBias: 2 + maximumLODLevel: 0 + streamingMipmapsActive: 0 + streamingMipmapsAddAllCameras: 1 + streamingMipmapsMemoryBudget: 512 + streamingMipmapsRenderersPerFrame: 512 + streamingMipmapsMaxLevelReduction: 2 + streamingMipmapsMaxFileIORequests: 1024 + particleRaycastBudget: 4096 + asyncUploadTimeSlice: 2 + asyncUploadBufferSize: 16 + asyncUploadPersistentBuffer: 1 + resolutionScalingFixedDPIFactor: 1 + customRenderPipeline: {fileID: 0} + excludedTargetPlatforms: [] + m_PerPlatformDefaultQuality: + Android: 2 + Lumin: 5 + GameCoreScarlett: 5 + GameCoreXboxOne: 5 + Nintendo Switch: 5 + PS4: 5 + PS5: 5 + Stadia: 5 + Standalone: 5 + WebGL: 3 + Windows Store Apps: 5 + XboxOne: 5 + iPhone: 2 + tvOS: 2 diff --git a/templates/unity/ProjectSettings/TagManager.asset b/templates/unity/ProjectSettings/TagManager.asset new file mode 100644 index 0000000000..1c92a7840e --- /dev/null +++ b/templates/unity/ProjectSettings/TagManager.asset @@ -0,0 +1,43 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!78 &1 +TagManager: + serializedVersion: 2 + tags: [] + layers: + - Default + - TransparentFX + - Ignore Raycast + - + - Water + - UI + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + m_SortingLayers: + - name: Default + uniqueID: 0 + locked: 0 diff --git a/templates/unity/ProjectSettings/TimeManager.asset b/templates/unity/ProjectSettings/TimeManager.asset new file mode 100644 index 0000000000..558a017e1f --- /dev/null +++ b/templates/unity/ProjectSettings/TimeManager.asset @@ -0,0 +1,9 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!5 &1 +TimeManager: + m_ObjectHideFlags: 0 + Fixed Timestep: 0.02 + Maximum Allowed Timestep: 0.33333334 + m_TimeScale: 1 + Maximum Particle Timestep: 0.03 diff --git a/templates/unity/ProjectSettings/UnityConnectSettings.asset b/templates/unity/ProjectSettings/UnityConnectSettings.asset new file mode 100644 index 0000000000..a88bee0f15 --- /dev/null +++ b/templates/unity/ProjectSettings/UnityConnectSettings.asset @@ -0,0 +1,36 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!310 &1 +UnityConnectSettings: + m_ObjectHideFlags: 0 + serializedVersion: 1 + m_Enabled: 0 + m_TestMode: 0 + m_EventOldUrl: https://api.uca.cloud.unity3d.com/v1/events + m_EventUrl: https://cdp.cloud.unity3d.com/v1/events + m_ConfigUrl: https://config.uca.cloud.unity3d.com + m_DashboardUrl: https://dashboard.unity3d.com + m_TestInitMode: 0 + CrashReportingSettings: + m_EventUrl: https://perf-events.cloud.unity3d.com + m_Enabled: 0 + m_LogBufferSize: 10 + m_CaptureEditorExceptions: 1 + UnityPurchasingSettings: + m_Enabled: 0 + m_TestMode: 0 + UnityAnalyticsSettings: + m_Enabled: 0 + m_TestMode: 0 + m_InitializeOnStartup: 1 + m_PackageRequiringCoreStatsPresent: 0 + UnityAdsSettings: + m_Enabled: 0 + m_InitializeOnStartup: 1 + m_TestMode: 0 + m_IosGameId: + m_AndroidGameId: + m_GameIds: {} + m_GameId: + PerformanceReportingSettings: + m_Enabled: 0 diff --git a/templates/unity/ProjectSettings/VFXManager.asset b/templates/unity/ProjectSettings/VFXManager.asset new file mode 100644 index 0000000000..46f38e16ee --- /dev/null +++ b/templates/unity/ProjectSettings/VFXManager.asset @@ -0,0 +1,14 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!937362698 &1 +VFXManager: + m_ObjectHideFlags: 0 + m_IndirectShader: {fileID: 0} + m_CopyBufferShader: {fileID: 0} + m_SortShader: {fileID: 0} + m_StripUpdateShader: {fileID: 0} + m_RenderPipeSettingsPath: + m_FixedTimeStep: 0.016666668 + m_MaxDeltaTime: 0.05 + m_CompiledVersion: 0 + m_RuntimeVersion: 0 diff --git a/templates/unity/ProjectSettings/VersionControlSettings.asset b/templates/unity/ProjectSettings/VersionControlSettings.asset new file mode 100644 index 0000000000..dca288142f --- /dev/null +++ b/templates/unity/ProjectSettings/VersionControlSettings.asset @@ -0,0 +1,8 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!890905787 &1 +VersionControlSettings: + m_ObjectHideFlags: 0 + m_Mode: Visible Meta Files + m_CollabEditorSettings: + inProgressEnabled: 1 diff --git a/templates/unity/ProjectSettings/XRSettings.asset b/templates/unity/ProjectSettings/XRSettings.asset new file mode 100644 index 0000000000..482590c196 --- /dev/null +++ b/templates/unity/ProjectSettings/XRSettings.asset @@ -0,0 +1,10 @@ +{ + "m_SettingKeys": [ + "VR Device Disabled", + "VR Device User Alert" + ], + "m_SettingValues": [ + "False", + "False" + ] +} \ No newline at end of file diff --git a/templates/unity/ProjectSettings/boot.config b/templates/unity/ProjectSettings/boot.config new file mode 100644 index 0000000000..e69de29bb2 diff --git a/templates/unity/README.md.twig b/templates/unity/README.md.twig new file mode 100644 index 0000000000..41a3d86537 --- /dev/null +++ b/templates/unity/README.md.twig @@ -0,0 +1,175 @@ +# {{ spec.title }} {{ sdk.name }} SDK + +![License](https://img.shields.io/github/license/{{ sdk.gitUserName|url_encode }}/{{ sdk.gitRepoName|url_encode }}.svg?style=flat-square) +![Version](https://img.shields.io/badge/api%20version-{{ spec.version|url_encode }}-blue.svg?style=flat-square) +![Unity](https://img.shields.io/badge/Unity-2021.3%2B-blue.svg) +[![Build Status](https://img.shields.io/travis/com/appwrite/sdk-generator?style=flat-square)](https://travis-ci.com/appwrite/sdk-generator) +{% if sdk.twitterHandle %} +[![Twitter Account](https://img.shields.io/twitter/follow/{{ sdk.twitterHandle }}?color=00acee&label=twitter&style=flat-square)](https://twitter.com/{{ sdk.twitterHandle }}) +{% endif %} +{% if sdk.discordChannel %} +[![Discord](https://img.shields.io/discord/{{ sdk.discordChannel }}?label=discord&style=flat-square)]({{ sdk.discordUrl }}) +{% endif %} +{% if sdk.warning %} + +{{ sdk.warning|raw }} +{% endif %} + +{{ sdk.description }} + +{% if sdk.logo %} +![{{ spec.title }}]({{ sdk.logo }}) +{% endif %} + +## Installation + +### Unity Package Manager (UPM) + +1. Open Unity and go to **Window > Package Manager** +2. Click the **+** button and select **Add package from git URL** +3. Enter the following URL: +``` +https://github.com/{{ sdk.gitUserName|url_encode }}/{{ sdk.gitRepoName|url_encode }}.git?path=Assets +``` +4. Click **Add** +5. In Unity, open the **Appwrite → Setup Assistant** menu and install the required dependencies +![](./.media/setup-assistant.png) +### Manual Installation + +1. Download the latest release from [GitHub](/releases) or zip +2. Import the Unity package into your project +3. In Unity, open the **Appwrite → Setup Assistant** menu and install the required dependencies + +## Dependencies + +This SDK requires the following Unity packages and libraries: + +- [**UniTask**](https://github.com/Cysharp/UniTask): For async/await support in Unity +- [**NativeWebSocket**](https://github.com/endel/NativeWebSocket): For WebSocket real-time subscriptions +- **System.Text.Json**: For JSON serialization (provided as a DLL in the project) + +You can also install UniTask and other required dependencies automatically via **Appwrite → Setup Assistant** in Unity. + +## Quick Start +> **Before you begin** +> First, create an Appwrite configuration: +> — via the **QuickStart** window in the **Appwrite Setup Assistant** +> — or through the menu **Appwrite → Create Configuration** +![](./.media/config.png) + +### Example: Unity Integration - Using AppwriteManager + +```csharp + [SerializeField] private AppwriteConfig config; + private AppwriteManager _manager; + + private async UniTask ExampleWithManager() + { + // Get or create manager + _manager = AppwriteManager.Instance ?? new GameObject("AppwriteManager").AddComponent(); + _manager.SetConfig(config); + + // Initialize + var success = await _manager.Initialize(); + if (!success) { Debug.LogError("Failed to initialize AppwriteManager"); return; } + + // Direct client access + var client = _manager.Client; + var pingResult = await client.Ping(); + Debug.Log($"Ping result: {pingResult}"); + + // Service creation through DI container + var account = _manager.GetService(); + var databases = _manager.GetService(); + + // Realtime example + var realtime = _manager.Realtime; + var subscription = realtime.Subscribe( + new[] { "databases.*.collections.*.documents" }, + response => Debug.Log($"Realtime event: {response.Events[0]}") + ); + } +``` + +### Example: Unity Integration - Using Client directly + +```csharp + [SerializeField] private AppwriteConfig config; + + private async UniTask ExampleWithDirectClient() + { + // Create and configure client + var client = new Client() + .SetEndpoint(config.Endpoint) + .SetProject(config.ProjectId); + + if (!string.IsNullOrEmpty(config.ApiKey)) + client.SetKey(config.ApiKey); + + if (!string.IsNullOrEmpty(config.RealtimeEndpoint)) + client.SetEndPointRealtime(config.RealtimeEndpoint); + + // Test connection + var pingResult = await client.Ping(); + Debug.Log($"Direct client ping: {pingResult}"); + + // Create services manually + var account = new Account(client); + var databases = new Databases(client); + + // Realtime example + // You need to create a Realtime instance manually or attach dependently + realtime.Initialize(client); + var subscription = realtime.Subscribe( + new[] { "databases.*.collections.*.documents" }, + response => Debug.Log($"Realtime event: {response.Events[0]}") + ); + } +``` +### Error Handling +```csharp +try +{ + var result = await client..Async(); +} +catch (AppwriteException ex) +{ + Debug.LogError($"Appwrite Error: {ex.Message}"); + Debug.LogError($"Status Code: {ex.Code}"); + Debug.LogError($"Response: {ex.Response}"); +} +``` + +## Preparing Models for Databases API + +When working with the Databases API in Unity, models should be prepared for serialization using the System.Text.Json library. By default, System.Text.Json converts property names from PascalCase to camelCase when serializing to JSON. If your Appwrite collection attributes are not in camelCase, this can cause errors due to mismatches between serialized property names and actual attribute names in your collection. + +To avoid this, add the `JsonPropertyName` attribute to each property in your model class to match the attribute name in Appwrite: + +```csharp +using System.Text.Json.Serialization; + +public class TestModel +{ + [JsonPropertyName("name")] + public string Name { get; set; } + + [JsonPropertyName("release_date")] + public System.DateTime ReleaseDate { get; set; } +} +``` + +The `JsonPropertyName` attribute ensures your data object is serialized with the correct attribute names for Appwrite databases. This approach works seamlessly in Unity with the included System.Text.Json DLL. + +## Contribution + +This library is auto-generated by the Appwrite [SDK Generator](https://github.com/appwrite/sdk-generator). To learn how you can help improve this SDK, please check the [contribution guide](https://github.com/appwrite/sdk-generator/blob/master/CONTRIBUTING.md) before sending a pull request. + +## License + +Please see the [{{spec.licenseName}} license]({{spec.licenseURL}}) file for more information. + +## Changelog + +Please see [CHANGELOG](CHANGELOG.md) for more information about recent changes. + diff --git a/templates/unity/base/requests/oauth.twig b/templates/unity/base/requests/oauth.twig new file mode 100644 index 0000000000..6841b6a39e --- /dev/null +++ b/templates/unity/base/requests/oauth.twig @@ -0,0 +1,31 @@ + var project = _client.Config.GetValueOrDefault("project"); + apiParameters["project"] = project; + + var queryString = apiParameters.ToQueryString(); + var authUrl = $"{_client.Endpoint}{apiPath}?{queryString}"; + + var callbackUri = await WebAuthComponent.Authenticate(authUrl); + + var query = callbackUri.Query.FromQueryString(); + var secret = query.GetValueOrDefault("secret"); + var key = query.GetValueOrDefault("key"); + var callbackDomain = query.GetValueOrDefault("domain"); // Get domain from callback + + if (string.IsNullOrEmpty(secret) || string.IsNullOrEmpty(key)) + { + var error = query.GetValueOrDefault("error") ?? "Unknown error"; + throw new {{ spec.title | caseUcfirst }}Exception($"Failed to get authentication credentials from callback. Error: {error}"); + } + + // Use domain from callback if available, otherwise fallback to endpoint host + var domain = !string.IsNullOrEmpty(callbackDomain) ? callbackDomain : new Uri(_client.Endpoint).Host; + var parsedDomain = domain.StartsWith(".") ? domain.Substring(1) : domain; + // Create a Set-Cookie header format and parse it + // This ensures consistent cookie processing with server responses + var setCookieHeader = $"{key}={secret}; Path=/; Domain={parsedDomain}; Secure; HttpOnly; Max-Age={30 * 24 * 60 * 60}"; + Debug.Log($"Setting session cookie for domain: {parsedDomain}"); + _client.CookieContainer.ParseSetCookieHeader(setCookieHeader, parsedDomain); + +#if UNITY_EDITOR + Debug.LogWarning("[{{ spec.title | caseUcfirst }}] OAuth authorization in Editor: you can open and authorize, but cookies cannot be obtained. The session will not be set."); +#endif diff --git a/templates/unity/docs/example.md.twig b/templates/unity/docs/example.md.twig new file mode 100644 index 0000000000..f03bc0a60e --- /dev/null +++ b/templates/unity/docs/example.md.twig @@ -0,0 +1,84 @@ +# {{method.name | caseUcfirst}} + +## Example + +```csharp +using {{ spec.title | caseUcfirst }}; +{% set addedEnum = false %} +{% for parameter in method.parameters.all %} +{% if parameter.enumValues | length > 0 and not addedEnum %} +using {{ spec.title | caseUcfirst }}.Enums; +{% set addedEnum = true %} +{% endif %} +{% endfor %} +using {{ spec.title | caseUcfirst }}.Models; +using {{ spec.title | caseUcfirst }}.Services; +using Cysharp.Threading.Tasks; +using UnityEngine; + +public class {{method.name | caseUcfirst}}Example : MonoBehaviour +{ + private Client client; + private {{service.name | caseUcfirst}} {{service.name | caseCamel}}; + + async void Start() + { + client = new Client() +{% if method.auth|length > 0 %} + .SetEndpoint("{{ spec.endpointDocs | raw }}") // Your API Endpoint +{% for node in method.auth %} +{% for key,header in node|keys %} + .Set{{header | caseUcfirst}}("{{node[header]['x-appwrite']['demo'] | raw }}"){% if loop.last %};{% endif %} // {{node[header].description}} +{% endfor %}{% endfor %}{% endif %} + + {{ service.name | caseCamel }} = new {{ service.name | caseUcfirst }}(client); + + await Example{{method.name | caseUcfirst}}(); + } + + async UniTask Example{{method.name | caseUcfirst}}() + { + try + { +{% if method.method != 'delete' and method.type != 'webAuth' %}{% if method.type == 'location' %} byte[] result = {% else %} {{ method.responseModel | caseUcfirst | overrideIdentifier }} result = {% endif %}{% endif %}await {{ service.name | caseCamel }}.{{ method.name | caseUcfirst }}({% if method.parameters.all | length == 0 %});{% endif %} +{%~ for parameter in method.parameters.all %} + + {{ parameter.name }}: {% if parameter.enumValues | length > 0%}{{ parameter.enumName }}.{{ (parameter.enumKeys[0] ?? parameter.enumValues[0]) | caseEnumKey }}{% else %}{{ parameter | paramExample }}{% endif %}{% if not loop.last %},{% endif %}{% if not parameter.required %} // optional{% endif %} +{%~ endfor %} + +{% if method.parameters.all | length > 0 %} );{% endif %} + +{% if method.method != 'delete' and method.type != 'webAuth' %} Debug.Log("Success: " + result); +{% else %} Debug.Log("Success"); +{% endif %} + } + catch ({{spec.title | caseUcfirst}}Exception ex) + { + Debug.LogError($"Error: {ex.Message} (Code: {ex.Code})"); + } + } +} +``` + +## Parameters + +{%~ for parameter in method.parameters.all %} +- **{{parameter.name | caseCamel}}** *{{parameter.type}}* - {{parameter.description}}{% if parameter.required %} *(required)* {% else %} *(optional)*{% endif %} + +{%~ endfor %} + +## Response + +{% if method.responseModel and method.responseModel != 'any' -%} +Returns `{{method.responseModel | caseUcfirst}}` object. +{%- else -%} +{% if method.type == "webAuth" -%} +None Returns +{%- else -%} +Returns response object. +{%- endif -%} +{%- endif %} + +## More Info + +{{method.description}} diff --git a/templates/unity/icon.png b/templates/unity/icon.png new file mode 100644 index 0000000000..dadbae8bab Binary files /dev/null and b/templates/unity/icon.png differ diff --git a/templates/unity/package.json.twig b/templates/unity/package.json.twig new file mode 100644 index 0000000000..0e585b9a48 --- /dev/null +++ b/templates/unity/package.json.twig @@ -0,0 +1,24 @@ +{ + "name": "com.fellmonkey.{{spec.title | caseLower}}-sdk", + "version": "{{sdk.version}}", + "displayName": "{{spec.title}} SDK", + "description": "{{spec.description}}", + "unity": "2021.3", + "documentationUrl": "https://appwrite.io/docs", + "keywords": [ + "{{spec.title | caseLower}}", + "backend", + "baas", + "api", + "database", + "authentication", + "storage", + "functions" + ], + "samples": [ + { + "displayName": "Example", + "description": "Appwrite Example", + "path": "Samples~/AppwriteExample" + } ] +} diff --git a/tests/Unity2021Test.php b/tests/Unity2021Test.php new file mode 100644 index 0000000000..ec0934826a --- /dev/null +++ b/tests/Unity2021Test.php @@ -0,0 +1,48 @@ + Unity_lic.ulf && /opt/unity/Editor/Unity -nographics -batchmode -manualLicenseFile Unity_lic.ulf -quit || true && /opt/unity/Editor/Unity -projectPath . -batchmode -nographics -runTests -testPlatform PlayMode -stackTraceLogType None -logFile - 2>/dev/null | sed -n \'/Test Started/,\$p\' | grep -v -E \'^(UnityEngine\\.|System\\.|Cysharp\\.|\\(Filename:|\\[.*\\]|##utp:|^\\s*\$|The header Origin is managed automatically|Connected to realtime:)\' | grep -v \'StackTraceUtility\'"'; + + public function testHTTPSuccess(): void + { + // Set Unity test mode to exclude problematic files + $GLOBALS['UNITY_TEST_MODE'] = true; + + parent::testHTTPSuccess(); + } + + protected array $expectedOutput = [ + ...Base::PING_RESPONSE, + ...Base::FOO_RESPONSES, + ...Base::BAR_RESPONSES, + ...Base::GENERAL_RESPONSES, + ...Base::UPLOAD_RESPONSES, + ...Base::DOWNLOAD_RESPONSES, + ...Base::ENUM_RESPONSES, + ...Base::MODEL_RESPONSES, + ...Base::EXCEPTION_RESPONSES, + ...Base::REALTIME_RESPONSES, + ...Base::COOKIE_RESPONSES, + ...Base::QUERY_HELPER_RESPONSES, + ...Base::PERMISSION_HELPER_RESPONSES, + ...Base::ID_HELPER_RESPONSES, + ...Base::CHANNEL_HELPER_RESPONSES, + ...Base::OPERATOR_HELPER_RESPONSES + ]; +} diff --git a/tests/languages/unity/Tests.asmdef b/tests/languages/unity/Tests.asmdef new file mode 100644 index 0000000000..4df3d5cb60 --- /dev/null +++ b/tests/languages/unity/Tests.asmdef @@ -0,0 +1,23 @@ +{ + "name": "Tests", + "rootNamespace": "AppwriteTests", + "references": [ + "UnityEngine.TestRunner", + "UnityEditor.TestRunner", + "Appwrite", + "Appwrite.Core", + "UniTask", + "endel.nativewebsocket" + ], + "includePlatforms": [], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": true, + "precompiledReferences": [ + "nunit.framework.dll" + ], + "autoReferenced": false, + "defineConstraints": [], + "versionDefines": [], + "noEngineReferences": false +} \ No newline at end of file diff --git a/tests/languages/unity/Tests.cs b/tests/languages/unity/Tests.cs new file mode 100644 index 0000000000..3b4feeb10f --- /dev/null +++ b/tests/languages/unity/Tests.cs @@ -0,0 +1,374 @@ +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Threading.Tasks; +using UnityEngine; +using UnityEngine.TestTools; + +using Appwrite; +using Appwrite.Models; +using Appwrite.Enums; +using Appwrite.Services; +using NUnit.Framework; + +namespace AppwriteTests +{ + public class Tests + { + [SetUp] + public void Setup() + { + Debug.Log("Test Started"); + } + + [UnityTest] + public IEnumerator Test1() + { + var task = RunAsyncTest(); + yield return new WaitUntil(() => task.IsCompleted); + + if (task.Exception != null) + { + Debug.LogError($"Test failed with exception: {task.Exception}"); + throw task.Exception; + } + } + private async Task WaitForRealtimeMessage(Realtime realtime, string[] channels, int timeoutMs = 5000, List queries = null) + { + var tcs = new TaskCompletionSource(); + var subscription = realtime.Subscribe(channels, (eventData) => + { + if (eventData.Payload != null && eventData.Payload.TryGetValue("response", out var value) && value != null) + { + tcs.TrySetResult(value.ToString()); + } + }, queries); + + var task = await Task.WhenAny(tcs.Task, Task.Delay(timeoutMs)); + subscription.Close(); + return task == tcs.Task ? await tcs.Task : "No realtime message received within timeout"; + } + + private async Task RunAsyncTest() + { + var client = new Client() + .SetProject("123456") + .AddHeader("Origin", "http://localhost") + .SetSelfSigned(true); + + var foo = new Foo(client); + var bar = new Bar(client); + var general = new General(client); + + client.SetProject("console"); + client.SetEndPointRealtime("wss://cloud.appwrite.io/v1"); + + // Create GameObject for Realtime MonoBehaviour + var realtimeObject = new GameObject("RealtimeTest"); + var realtime = realtimeObject.AddComponent(); + realtime.Initialize(client); + + await Task.Delay(5000); + + // Ping test + client.SetProject("123456"); + var ping = await client.Ping(); + Debug.Log(ping); + + // Reset a project for other tests + client.SetProject("console"); + + Mock mock; + // Foo Tests + mock = await foo.Get("string", 123, new List() { "string in array" }); + Debug.Log(mock.Result); + + mock = await foo.Post("string", 123, new List() { "string in array" }); + Debug.Log(mock.Result); + + mock = await foo.Put("string", 123, new List() { "string in array" }); + Debug.Log(mock.Result); + + mock = await foo.Patch("string", 123, new List() { "string in array" }); + Debug.Log(mock.Result); + + mock = await foo.Delete("string", 123, new List() { "string in array" }); + Debug.Log(mock.Result); + + // Bar Tests + mock = await bar.Get("string", 123, new List() { "string in array" }); + Debug.Log(mock.Result); + + mock = await bar.Post("string", 123, new List() { "string in array" }); + Debug.Log(mock.Result); + + mock = await bar.Put("string", 123, new List() { "string in array" }); + Debug.Log(mock.Result); + + mock = await bar.Patch("string", 123, new List() { "string in array" }); + Debug.Log(mock.Result); + + mock = await bar.Delete("string", 123, new List() { "string in array" }); + Debug.Log(mock.Result); + + // General Tests + var result = await general.Redirect(); + Debug.Log((result as Dictionary)["result"]); + + mock = await general.Upload("string", 123, new List() { "string in array" }, InputFile.FromPath("../../resources/file.png")); + Debug.Log(mock.Result); + + mock = await general.Upload("string", 123, new List() { "string in array" }, InputFile.FromPath("../../resources/large_file.mp4")); + Debug.Log(mock.Result); + + var info = new FileInfo("../../resources/file.png"); + mock = await general.Upload("string", 123, new List() { "string in array" }, InputFile.FromStream(info.OpenRead(), "file.png", "image/png")); + Debug.Log(mock.Result); + + info = new FileInfo("../../resources/large_file.mp4"); + mock = await general.Upload("string", 123, new List() { "string in array" }, InputFile.FromStream(info.OpenRead(), "large_file.mp4", "video/mp4")); + Debug.Log(mock.Result); + + // Download test + var downloadResult = await general.Download(); + if (downloadResult != null) + { + var downloadString = System.Text.Encoding.UTF8.GetString(downloadResult); + Debug.Log(downloadString); + } + + mock = await general.Enum(MockType.First); + Debug.Log(mock.Result); + + // Request model tests + mock = await general.CreatePlayer(new Player("player1", "John Doe", 100)); + Debug.Log(mock.Result); + + mock = await general.CreatePlayers(new List { + new Player("player1", "John Doe", 100), + new Player("player2", "Jane Doe", 200) + }); + Debug.Log(mock.Result); + + try + { + await general.Error400(); + } + catch (AppwriteException e) + { + Debug.Log(e.Message); + Debug.Log(e.Response); + } + + try + { + await general.Error500(); + } + catch (AppwriteException e) + { + Debug.Log(e.Message); + Debug.Log(e.Response); + } + + try + { + await general.Error502(); + } + catch (AppwriteException e) + { + Debug.Log(e.Message); + Debug.Log(e.Response); + } + + try + { + client.SetEndpoint("htp://cloud.appwrite.io/v1"); + } + catch (AppwriteException e) + { + Debug.Log(e.Message); + } + + // Realtime tests + var realtimeNoQueryResponse = await WaitForRealtimeMessage(realtime, new[] { "tests" }); + Debug.Log(realtimeNoQueryResponse); + + var realtimeWithQueryResponse = await WaitForRealtimeMessage(realtime, new[] { "tests" }, queries: new List { Query.Equal("response", new List { "WS:/v1/realtime:passed" }) }); + Debug.Log(realtimeWithQueryResponse); + + var realtimeFailureResponse = await WaitForRealtimeMessage(realtime, new[] { "tests" }, queries: new List { Query.Equal("response", new List { "failed" }) }); + if (realtimeFailureResponse == "No realtime message received within timeout") + { + Debug.Log("Realtime failed!"); + } + else + { + Debug.Log("Realtime passed! (unexpected)"); + } + + // Cookie tests + mock = await general.SetCookie(); + Debug.Log(mock.Result); + + mock = await general.GetCookie(); + Debug.Log(mock.Result); + + // Query helper tests + Debug.Log(Query.Equal("released", new List { true })); + Debug.Log(Query.Equal("title", new List { "Spiderman", "Dr. Strange" })); + Debug.Log(Query.NotEqual("title", "Spiderman")); + Debug.Log(Query.LessThan("releasedYear", 1990)); + Debug.Log(Query.GreaterThan("releasedYear", 1990)); + Debug.Log(Query.Search("name", "john")); + Debug.Log(Query.IsNull("name")); + Debug.Log(Query.IsNotNull("name")); + Debug.Log(Query.Between("age", 50, 100)); + Debug.Log(Query.Between("age", 50.5, 100.5)); + Debug.Log(Query.Between("name", "Anna", "Brad")); + Debug.Log(Query.StartsWith("name", "Ann")); + Debug.Log(Query.EndsWith("name", "nne")); + Debug.Log(Query.Select(new List { "name", "age" })); + Debug.Log(Query.OrderAsc("title")); + Debug.Log(Query.OrderDesc("title")); + Debug.Log(Query.OrderRandom()); + Debug.Log(Query.CursorAfter("my_movie_id")); + Debug.Log(Query.CursorBefore("my_movie_id")); + Debug.Log(Query.Limit(50)); + Debug.Log(Query.Offset(20)); + Debug.Log(Query.Contains("title", "Spider")); + Debug.Log(Query.Contains("labels", "first")); + Debug.Log(Query.ContainsAny("labels", new List { "first", "second" })); + Debug.Log(Query.ContainsAll("labels", new List { "first", "second" })); + + // New query methods + Debug.Log(Query.NotContains("title", "Spider")); + Debug.Log(Query.NotSearch("name", "john")); + Debug.Log(Query.NotBetween("age", 50, 100)); + Debug.Log(Query.NotStartsWith("name", "Ann")); + Debug.Log(Query.NotEndsWith("name", "nne")); + Debug.Log(Query.CreatedBefore("2023-01-01")); + Debug.Log(Query.CreatedAfter("2023-01-01")); + Debug.Log(Query.CreatedBetween("2023-01-01", "2023-12-31")); + Debug.Log(Query.UpdatedBefore("2023-01-01")); + Debug.Log(Query.UpdatedAfter("2023-01-01")); + Debug.Log(Query.UpdatedBetween("2023-01-01", "2023-12-31")); + + // Spatial Distance query tests + Debug.Log(Query.DistanceEqual("location", new List { new List { 40.7128, -74 }, new List { 40.7128, -74 } }, 1000)); + Debug.Log(Query.DistanceEqual("location", new List { 40.7128, -74 }, 1000, true)); + Debug.Log(Query.DistanceNotEqual("location", new List { 40.7128, -74 }, 1000)); + Debug.Log(Query.DistanceNotEqual("location", new List { 40.7128, -74 }, 1000, true)); + Debug.Log(Query.DistanceGreaterThan("location", new List { 40.7128, -74 }, 1000)); + Debug.Log(Query.DistanceGreaterThan("location", new List { 40.7128, -74 }, 1000, true)); + Debug.Log(Query.DistanceLessThan("location", new List { 40.7128, -74 }, 1000)); + Debug.Log(Query.DistanceLessThan("location", new List { 40.7128, -74 }, 1000, true)); + + // Spatial query tests + Debug.Log(Query.Intersects("location", new List { 40.7128, -74 })); + Debug.Log(Query.NotIntersects("location", new List { 40.7128, -74 })); + Debug.Log(Query.Crosses("location", new List { 40.7128, -74 })); + Debug.Log(Query.NotCrosses("location", new List { 40.7128, -74 })); + Debug.Log(Query.Overlaps("location", new List { 40.7128, -74 })); + Debug.Log(Query.NotOverlaps("location", new List { 40.7128, -74 })); + Debug.Log(Query.Touches("location", new List { 40.7128, -74 })); + Debug.Log(Query.NotTouches("location", new List { 40.7128, -74 })); + Debug.Log(Query.Contains("location", new List { new List { 40.7128, -74 }, new List { 40.7128, -74 } })); + Debug.Log(Query.NotContains("location", new List { new List { 40.7128, -74 }, new List { 40.7128, -74 } })); + Debug.Log(Query.Equal("location", new List { new List { 40.7128, -74 }, new List { 40.7128, -74 } })); + Debug.Log(Query.NotEqual("location", new List { new List { 40.7128, -74 }, new List { 40.7128, -74 } })); + + Debug.Log(Query.Or(new List { Query.Equal("released", true), Query.LessThan("releasedYear", 1990) })); + Debug.Log(Query.And(new List { Query.Equal("released", false), Query.GreaterThan("releasedYear", 2015) })); + + // regex, exists, notExists, elemMatch + Debug.Log(Query.Regex("name", "pattern.*")); + Debug.Log(Query.Exists(new List { "attr1", "attr2" })); + Debug.Log(Query.NotExists(new List { "attr1", "attr2" })); + Debug.Log(Query.ElemMatch("friends", new List { + Query.Equal("name", "Alice"), + Query.GreaterThan("age", 18) + })); + // Permission & Roles helper tests + Debug.Log(Permission.Read(Role.Any())); + Debug.Log(Permission.Write(Role.User(ID.Custom("userid")))); + Debug.Log(Permission.Create(Role.Users())); + Debug.Log(Permission.Update(Role.Guests())); + Debug.Log(Permission.Delete(Role.Team("teamId", "owner"))); + Debug.Log(Permission.Delete(Role.Team("teamId"))); + Debug.Log(Permission.Create(Role.Member("memberId"))); + Debug.Log(Permission.Update(Role.Users("verified"))); + Debug.Log(Permission.Update(Role.User(ID.Custom("userid"), "unverified"))); + Debug.Log(Permission.Create(Role.Label("admin"))); + + // ID helper tests + Debug.Log(ID.Unique()); + Debug.Log(ID.Custom("custom_id")); + + // Channel helper tests + Debug.Log(Channel.Database().Collection().Document().ToString()); + Debug.Log(Channel.Database("db1").Collection("col1").Document("doc1").ToString()); + Debug.Log(Channel.Database("db1").Collection("col1").Document("doc1").Create().ToString()); + Debug.Log(Channel.Database("db1").Collection("col1").Document("doc1").Upsert().ToString()); + Debug.Log(Channel.TablesDB().Table().Row().ToString()); + Debug.Log(Channel.TablesDB("db1").Table("table1").Row("row1").ToString()); + Debug.Log(Channel.TablesDB("db1").Table("table1").Row("row1").Update().ToString()); + Debug.Log(Channel.Account()); + Debug.Log(Channel.Bucket().File().ToString()); + Debug.Log(Channel.Bucket("bucket1").File("file1").ToString()); + Debug.Log(Channel.Bucket("bucket1").File("file1").Delete().ToString()); + Debug.Log(Channel.Function().ToString()); + Debug.Log(Channel.Function("func1").ToString()); + Debug.Log(Channel.Execution().ToString()); + Debug.Log(Channel.Execution("exec1").ToString()); + Debug.Log(Channel.Documents()); + Debug.Log(Channel.Rows()); + Debug.Log(Channel.Files()); + Debug.Log(Channel.Executions()); + Debug.Log(Channel.Teams()); + Debug.Log(Channel.Team().ToString()); + Debug.Log(Channel.Team("team1").ToString()); + Debug.Log(Channel.Team("team1").Create().ToString()); + Debug.Log(Channel.Memberships()); + Debug.Log(Channel.Membership().ToString()); + Debug.Log(Channel.Membership("membership1").ToString()); + Debug.Log(Channel.Membership("membership1").Update().ToString()); + + // Operator helper tests + Debug.Log(Operator.Increment(1)); + Debug.Log(Operator.Increment(5, 100)); + Debug.Log(Operator.Decrement(1)); + Debug.Log(Operator.Decrement(3, 0)); + Debug.Log(Operator.Multiply(2)); + Debug.Log(Operator.Multiply(3, 1000)); + Debug.Log(Operator.Divide(2)); + Debug.Log(Operator.Divide(4, 1)); + Debug.Log(Operator.Modulo(5)); + Debug.Log(Operator.Power(2)); + Debug.Log(Operator.Power(3, 100)); + Debug.Log(Operator.ArrayAppend(new List { "item1", "item2" })); + Debug.Log(Operator.ArrayPrepend(new List { "first", "second" })); + Debug.Log(Operator.ArrayInsert(0, "newItem")); + Debug.Log(Operator.ArrayRemove("oldItem")); + Debug.Log(Operator.ArrayUnique()); + Debug.Log(Operator.ArrayIntersect(new List { "a", "b", "c" })); + Debug.Log(Operator.ArrayDiff(new List { "x", "y" })); + Debug.Log(Operator.ArrayFilter(Condition.Equal, "test")); + Debug.Log(Operator.StringConcat("suffix")); + Debug.Log(Operator.StringReplace("old", "new")); + Debug.Log(Operator.Toggle()); + Debug.Log(Operator.DateAddDays(7)); + Debug.Log(Operator.DateSubDays(3)); + Debug.Log(Operator.DateSetNow()); + + mock = await general.Headers(); + Debug.Log(mock.Result); + + // Cleanup Realtime GameObject + if (realtimeObject) + { + Object.DestroyImmediate(realtimeObject); + } + + } + } +}