commit 7362ca0f027b899b7a26b6eeac004e61fddb25f3 Author: Dominic Date: Sat Oct 3 21:28:56 2026 +0200 Initial Void Bastion 0.20 prototype diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e6b0021 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +.godot/ +server/nakama/.env +server/nakama/backups/ +tools/godot-agent-tools-mcp/node_modules/ diff --git a/.mcp.json b/.mcp.json new file mode 100644 index 0000000..a7cd62a --- /dev/null +++ b/.mcp.json @@ -0,0 +1,14 @@ +{ + "mcpServers": { + "godot-agent-tools": { + "command": "C:\\Users\\Domin\\Documents\\Codex\\tools\\node-v24.19.0-win-x64\\node.exe", + "args": [ + "C:\\Users\\Domin\\Documents\\Codex\\2026-10-02\\ich\\outputs\\void-bastion\\tools\\godot-agent-tools-mcp\\server.mjs" + ], + "env": { + "GODOT_AGENT_HOST": "127.0.0.1", + "GODOT_AGENT_PORT": "9920" + } + } + } +} diff --git a/ART_ASSETS.md b/ART_ASSETS.md new file mode 100644 index 0000000..9687b64 --- /dev/null +++ b/ART_ASSETS.md @@ -0,0 +1,178 @@ +# Grafik-Prompts + +Erzeugt mit dem integrierten OpenAI-Bildgenerator. Die Sprite-Sheets besitzen echte Transparenz. + +## orbital_platform.png + +```text +Use case: stylized-concept +Asset type: 2D game battlefield background +Primary request: a realistic science-fiction orbital defense platform viewed directly from above, designed as a tower-defense map +Scene/backdrop: deep space with subtle blue nebula, distant stars, dark armored station plates forming a broad central combat zone, one clearly readable curved enemy flight corridor from the upper-left edge toward a glowing reactor core on the right +Style/medium: polished realistic 2D game environment art, physically plausible hard-surface materials, cinematic but readable +Composition/framing: exact top-down orthographic view, 16:9 landscape, gameplay area remains visually uncluttered, route readable without arrows or text +Lighting/mood: cold blue space light, restrained cyan reactor glow, high contrast silhouettes +Color palette: graphite, gunmetal, navy, cold cyan, small amber hazard lights +Constraints: no text, no logos, no watermark, no UI, no ships, no turrets, no perspective tilt +``` + +## defense_towers.png + +```text +Use case: stylized-concept +Asset type: transparent 2D game tower sprite sheet +Primary request: three separate realistic science-fiction defensive turrets: rapid kinetic cannon, focused energy laser emitter, heavy missile launcher +Subject: each turret is a complete circular-base installation with a distinct readable silhouette, viewed directly from above +Style/medium: polished realistic 2D game sprites, hard-surface military spacecraft design, consistent scale and lighting +Composition/framing: exact top-down orthographic view, three objects evenly spaced in one horizontal row, generous transparent padding, no overlap +Lighting/mood: cold blue rim light with subtle cyan emissive details +Color palette: gunmetal, graphite, muted navy, cyan accents, tiny amber details +Constraints: genuinely transparent background, no labels, no text, no logos, no watermark, no shadows extending far from objects, no perspective tilt +``` + +## enemy_ships.png + +```text +Use case: stylized-concept +Asset type: transparent 2D game enemy spaceship sprite sheet +Primary request: three separate hostile spacecraft: fast interceptor, armored assault frigate, heavy command cruiser +Subject: complete ships with increasingly large and armored silhouettes, noses pointing to the right, viewed directly from above +Style/medium: polished realistic 2D game sprites, plausible mechanical panels and engines, consistent faction design +Composition/framing: exact top-down orthographic view, three objects evenly spaced in one horizontal row, generous transparent padding, no overlap +Lighting/mood: cold space lighting, restrained red-orange engine and weapon glow +Color palette: dark titanium, worn steel, deep red markings, red-orange emissive details +Constraints: genuinely transparent background, no labels, no text, no logos, no watermark, no cropped parts, no perspective tilt +``` + +## app_icon.png + +```text +Use case: logo-brand +Asset type: mobile and desktop game app icon +Primary request: original icon for a realistic science-fiction tower-defense game, showing a cyan glowing orbital reactor core protected by three dark gunmetal shield segments while a small red hostile ship approaches +Style/medium: polished realistic 2D game icon, hard-surface science fiction, strong readable silhouette at small size +Composition/framing: square, centered emblem, close crop, generous safe margin for rounded app-icon masks +Lighting/mood: cold cyan reactor glow against deep space, restrained red threat accent +Color palette: graphite, navy, cyan, small red-orange accent +Constraints: no text, no letters, no logos or trademarks, no watermark, no thin fragile details, no transparent background +``` + +## orbital_shipyard.png + +```text +Use case: stylized-concept +Asset type: 2D tower-defense game map background +Primary request: Create a realistic cinematic top-down orbital shipyard map for a science-fiction tower-defense game. +Scene/backdrop: Huge rectangular orbital industrial platform floating in deep space, full landscape composition, broad playable deck filling most of the frame. +Subject: One single unmistakable enemy roadway begins at the far left edge, travels horizontally, bends downward through the middle, bends back upward, and ends at a bright cyan reactor at the far right edge. The roadway must remain continuously visible, wide, unobstructed, and visually separated from buildable deck areas. +Style/medium: realistic high-detail hard-surface science-fiction concept art, orthographic top-down game map, dark steel, subtle cyan and amber lighting. +Composition/framing: landscape 16:9, edge-to-edge map, large open construction zones above and below the roadway, no perspective tilt. +Constraints: no towers, no ships, no enemies, no characters, no text, no labels, no UI, no watermark; exactly one continuous route; reactor only at route end. +``` + +## asteroid_refinery.png + +```text +Use case: stylized-concept +Asset type: 2D tower-defense game map background +Primary request: Create a realistic cinematic top-down asteroid refinery map for a science-fiction tower-defense game. +Scene/backdrop: Massive mining and refinery station built across a rust-red asteroid field, full landscape composition, broad playable metal deck filling most of the frame. +Subject: One single unmistakable armored convoy roadway begins at the upper-left edge, curves down around a central circular refinery pit, continues along the lower middle, then turns upward and ends at a bright orange-white reactor gate on the far right edge. The roadway must be continuously visible, wide, unobstructed, and clearly distinct from buildable deck areas. +Style/medium: realistic high-detail hard-surface science-fiction concept art, orthographic top-down game map, dark iron, copper, rust-red rock, restrained orange and teal lights. +Composition/framing: landscape 16:9, edge-to-edge map, generous open construction zones around the route, no perspective tilt. +Constraints: no towers, no ships, no enemies, no characters, no text, no labels, no UI, no watermark; exactly one continuous route; reactor only at route end. +``` + +## HD-Neuaufbereitung der drei Karten + +Für `orbital_platform_hd.png`, `orbital_shipyard_hd.png` und `asteroid_refinery_hd.png` wurde jeweils das zugehörige vorhandene Kartenbild als Editierziel verwendet. Der Kartenname im folgenden Prompt wurde pro Motiv angepasst: + +```text +Use case: precise-object-edit +Asset type: high-resolution top-down 2D tower-defense map background for Windows PC +Primary request: Re-render and upscale the supplied map to a crisp 4K-quality 16:9 game background while preserving the exact road route, reactor placement, platform layout, camera angle, colors, and overall composition. +Style/medium: realistic high-detail hard-surface science-fiction environment, sharp physically plausible metal, readable panel seams, clean road edges. +Composition/framing: exact same top-down framing and aspect ratio as the input; every road bend and gameplay-relevant platform must remain at the same relative coordinates. +Constraints: preserve gameplay geometry exactly; improve fine texture detail and edge clarity for display at 2800x1575 and larger; no text, no UI, no grid, no towers, no enemies, no watermark. +Avoid: changing the route, adding or removing road branches, changing the reactor position, blur, painterly texture, depth-of-field, perspective tilt. +``` + +## Verlängerte Routen für Version 0.7 + +Für `orbital_platform_long.png` und `orbital_shipyard_long.png` wurde die jeweilige HD-Karte als Editierziel verwendet. Die Pyrax-Raffinerie behält ihre vorhandene große Ringschleife, da sie den Bauraum bereits sinnvoll erschließt. + +Gemeinsamer Rahmen: + +```text +Use case: precise-object-edit +Asset type: full-screen top-down 2D tower-defense battle map +Style/medium: preserve the supplied realistic high-detail hard-surface science-fiction station, exact top-down orthographic camera, crisp metal panels and readable road edges. +Composition/framing: preserve the 16:9 frame, existing station silhouette, environment, lighting palette, and reactor identity; the entire route must be visible at once on PC and mobile. +Constraints: change the roadway and adjacent deck only as needed; exactly one continuous road from the left entrance to the right reactor; road width consistent everywhere; long bends with no crossings or branches; generous buildable metal areas always within tower range of at least one road segment; no UI, grid, towers, enemies, text, arrows, labels, or watermark. +Avoid: short direct paths, dead ends, route crossings, hidden road sections, perspective tilt, blur. +``` + +Kartenspezifische Routenanweisungen: + +```text +Bastion Prime: Replace the current short roadway with one single much longer non-crossing three-pass serpentine road: it enters from the far left in the upper fifth, crosses far toward the right, makes a broad 180-degree bend downward, returns left across the middle, makes a second broad 180-degree bend downward, then crosses right across the lower-middle and connects to the existing cyan reactor on the far right. Keep large buildable deck islands inside and around the bends. + +Helios-Werft: Replace the current roadway with one single longer non-crossing industrial S-route using three horizontal firing lanes: enter at the far left upper-middle, travel far right, make a broad downward U-turn, travel back left through the center, make another downward U-turn, then travel right along the lower-middle and curve upward into the existing cyan reactor at the far right. Use most of the station deck while leaving clear buildable islands between the lanes. +``` + +## Straßenfreie Rasterbasen für Version 0.12 + +Für die drei final verwendeten Dateien `orbital_platform_grid_base.png`, `orbital_shipyard_grid_base.png` und `asteroid_refinery_grid_base.png` wurde jeweils die vorhandene Karte als Editierziel verwendet. Die Straße wird nicht mehr in das Bild eingebacken, sondern im Spiel exakt aus dem 80-Pixel-Raster erzeugt. + +### Bastion Prime + +```text +Use case: precise-object-edit +Asset type: top-down 2D tower-defense game map background +Primary request: Remove every baked-in roadway, racetrack lane, glowing road border, road center line, and curved driving route from this space-station map. Replace all former road areas with continuous realistic dark steel station decking and modular armored platform panels that match the surrounding construction surfaces. +Input image: edit target; preserve the map's identity. +Style/medium: realistic cinematic top-down science-fiction industrial station, crisp game-ready detail. +Composition/framing: preserve the exact camera, top-down framing, outer station silhouette, blue space background, right-side blue reactor, machinery, bridges, lighting direction, and overall color palette. +Constraints: the final surface must be a neutral continuous buildable deck with absolutely no implied travel route; preserve the reactor and major station structures; no towers, enemies, grid, UI, text, symbols, arrows, tracks, roads, lanes, curves, path markings, or watermark. +``` + +### Helios-Werft + +```text +Use case: precise-object-edit +Asset type: top-down 2D tower-defense game map background +Primary request: Remove every baked-in roadway, racetrack lane, glowing road border, road center line, and curved driving route from this orbital shipyard map. Replace all former road areas with continuous realistic dark steel shipyard decking and modular armored platform panels matching the surrounding station. +Input image: edit target; preserve the Helios shipyard identity. +Style/medium: realistic cinematic top-down science-fiction industrial shipyard, crisp game-ready detail. +Composition/framing: preserve the exact camera, top-down framing, outer station silhouette, space backdrop, right-side blue reactor, gantries, towers, machinery, warm industrial lighting, and palette. +Constraints: neutral continuous buildable deck with absolutely no implied travel route; preserve the reactor and major structures; no towers, enemies, grid, UI, text, symbols, arrows, tracks, roads, lanes, curves, path markings, or watermark. +``` + +### Pyrax-Raffinerie + +```text +Use case: precise-object-edit +Asset type: top-down 2D tower-defense game map background +Primary request: Remove every baked-in roadway, curved vehicle lane, glowing road border, road center line, and implied driving route from this orange asteroid refinery map. Replace former roads with continuous realistic rugged refinery decking, armored steel plates, industrial grating, pipes, and platform panels matching the surrounding buildable areas. +Input image: edit target; preserve the Pyrax refinery identity. +Style/medium: realistic cinematic top-down science-fiction asteroid refinery, crisp game-ready detail. +Composition/framing: preserve the exact camera, top-down framing, outer refinery silhouette, orange lava-space environment, right-side orange reactor, central open furnace/reactor chamber, machinery, industrial lighting, and palette. +Constraints: neutral continuous platform deck with absolutely no implied travel route; preserve both reactor areas and major structures; no towers, enemies, grid, UI, text, symbols, arrows, tracks, roads, lanes, curves, path markings, or watermark. +``` + +## Beleuchtete Straßenoberfläche für Version 0.13 + +Die Datei `road_surface_material_v1.png` wurde mit dem integrierten OpenAI-Bildgenerator als realistische Materialquelle erzeugt. Das Spiel setzt Ausschnitte dieser Textur ausschließlich innerhalb der exakt rastergebundenen Straßenmodule ein; Form und Laufweg werden weiterhin deterministisch von Godot vorgegeben. + +```text +Use case: stylized-concept +Asset type: seamless material texture for modular top-down 2D tower-defense roadway tiles +Primary request: Create a highly detailed realistic science-fiction orbital-station roadway surface material with the rich cinematic finish of premium game environment art. +Subject: edge-to-edge brushed titanium and gunmetal road decking with layered armored panels, fine recessed seams, subtle wear, small rivets, narrow cyan energy conduits, warm amber embedded navigation lights, soft pools of orange light, restrained blue reflections, and localized luminous rays across the metal. +Style/medium: photorealistic cinematic hard-surface science-fiction game texture, crisp high-resolution PBR-like detail, exact orthographic top-down view. +Composition/framing: square seamless tileable material swatch; continuous surface reaches every edge; balanced details without a central object; suitable for repeatedly tiling inside straight and curved road masks. +Lighting/mood: active inhabited station, bright readable metal, warm amber practical lights mixed with cool cyan technology glow, subtle volumetric light streaks and reflections, high contrast but not dark. +Color palette: silver titanium, graphite, cool steel blue, cyan accents, amber-orange lights. +Constraints: seamless/tileable on all four edges; exact top-down orthographic view; uniform scale; no road shape, no curve, no perspective, no background scenery, no space, no vehicles, no towers, no enemies, no text, no symbols, no arrows, no UI, no watermark. +Avoid: flat gray fill, large empty areas, obvious central composition, thick yellow outline, cartoon look, painterly blur, perspective tilt, lane borders that would conflict with a mask. +``` diff --git a/ONLINE_SETUP.md b/ONLINE_SETUP.md new file mode 100644 index 0000000..a83a70e --- /dev/null +++ b/ONLINE_SETUP.md @@ -0,0 +1,44 @@ +# Void Bastion Online 0.19 + +## Fertiger Stand + +Das Spiel nutzt den offiziellen Nakama-Godot-Client 3.4.0. Beim normalen Start wird automatisch ein app-eigenes Gastkonto angemeldet. Das lokale Profil bleibt auch offline nutzbar; neue Bestwerte werden gespeichert und bei der nächsten erfolgreichen Verbindung nachgereicht. + +Der Unraid-Stack besteht aus: + +- Nakama 3.37.0 mit eigener JavaScript-Serverlogik +- PostgreSQL 16 ohne öffentlich freigegebenen Datenbank-Port +- täglichem komprimiertem Backup mit 14 Tagen Aufbewahrung +- ausschließlich lokal erreichbarer Nakama-Verwaltungskonsole +- getrennten zufälligen Schlüsseln für Sitzungen, Sitzungserneuerung, Verwaltung und Server-RPCs + +## Ranglisten + +Es existieren 36 autoritative Ranglisten: vier Spielarten mal drei Karten mal drei Schwierigkeitsgrade. Der Client kann nicht direkt in diese Ranglisten schreiben. Ergebnisse laufen durch `submit_run`; Karte, Schwierigkeit, Modus, Welle und eine großzügige Plausibilitätsgrenze für die Punktzahl werden vor dem Eintrag geprüft. + +Die Kennungen folgen diesem Muster: `mission_map0_difficulty0`. + +Mögliche Spielarten sind `mission`, `endless`, `coop_ai` und `coop_online`. + +## Multiplayer + +Die Godot-Anbindung öffnet nach der Anmeldung eine WSS/WS-Echtzeitverbindung. Die schnelle Spielersuche vermittelt exakt zwei Spieler mit derselben Suchart in eine serverautoritative Koop-Sitzung. Der Matchserver verwaltet Teilnehmer, gemeinsame Sitzungsdaten und die Weiterleitung zulässiger Spielnachrichten. + +Der lokale KI-Koop bleibt unabhängig davon spielbar. Die vollständige Synchronisation von Türmen, Gegnern und gemeinsamem Reaktor zwischen zwei sichtbaren Spielclients ist der nächste Gameplay-Schritt; Konto, Transport, Vermittlung und Server-Match sind bereits funktionsfähig. + +## Adressen + +- Produktion: `https://api.void-bastion.gergames.de` und `wss://api.void-bastion.gergames.de` +- Entwicklung im Heimnetz: `http://192.168.178.20:7350` und `ws://192.168.178.20:7350` + +In Debug-Builds versucht das Spiel nach einer fehlgeschlagenen Produktionsverbindung automatisch den Heimnetz-Server. Release-Builds verwenden ausschließlich HTTPS/WSS. + +Die öffentliche Freischaltung ist abgeschlossen: Der autoritative DNS-Eintrag `api.void-bastion.gergames.de` zeigt als CNAME auf `bs6o73mrdi3e60p0.myfritz.net`; Reverse Proxy, HTTPS/WSS und das automatisch erneuerte Let’s-Encrypt-Zertifikat sind aktiv. + +## Tests + +- Offline-/Gameplaytest: `godot --headless --path . --script res://tests/smoke_test.gd` +- echter Onlinetest im Heimnetz: `godot --headless --path . --script res://tests/online_integration_test.gd` +- Produktionstest: `VOID_BASTION_TEST_HOST=api.void-bastion.gergames.de VOID_BASTION_TEST_SCHEME=https VOID_BASTION_TEST_PORT=443 godot --headless --path . --script res://tests/online_integration_test.gd` + +Der Onlinetest prüft zwei Gastanmeldungen, RPC, Ergebnisannahme, Ranglistenabruf, zwei WebSocket-Verbindungen, Matchmaking und den Beitritt beider Clients zur selben autoritativen Partie. diff --git a/README.md b/README.md new file mode 100644 index 0000000..d5b9012 --- /dev/null +++ b/README.md @@ -0,0 +1,149 @@ +# Void Bastion – spielbarer Prototyp 0.19 + +Ein plattformübergreifender 2D-Tower-Defense-Prototyp für Godot 4. Der aktuelle Stand ist für Windows, Android und später iOS ausgelegt. + +## Enthalten + +- drei auswählbare realistische Weltraumkarten im Querformat +- vollständig sichtbare Einsatzkarten ohne Scrollen, Minikarte oder Richtungstasten +- hellere, kontrastreichere Stationskarten und eigens gestaltete modulare Metallfahrbahnen mit realistischen Materialtexturen auf Geraden und Kurven, Führungsschienen, gebürsteten Paneelen, cyanfarbenen Energieleitungen, farbigen Reflexionen, Schraubpunkten, warmen Positionslichtern und dezenten Lichtkegeln +- feste Benutzeroberfläche mit 1600 × 900 Referenzauflösung +- eine exakt eine Rasterzelle (80 Pixel) breite Straße, die direkt aus den Wegdaten gezeichnet wird +- sichtbare Straßenmitte, Gegner-Lauflinie und gesperrte Bauzellen verwenden auf jeder Karte dieselbe Datenquelle und können deshalb nicht mehr gegeneinander verrutschen +- deutlich längere Wendeschleifen auf Bastion Prime und der Helios-Werft, damit mehr Baufläche taktisch relevant wird +- sichtbares 80-Pixel-Bauraster mit automatischem Einrasten der Türme +- jede geeignete Plattformzelle ist bebaubar; ausschließlich Wegzellen, Reaktoren und Bereiche außerhalb der Plattform sind gesperrt +- neu erzeugte, straßenfreie HD-Kartenbasen mit 3344 × 1882 Pixeln und Mipmaps für eine schärfere Windows-Darstellung +- drei Türme: Kinetik, Laser und Rakete +- drei Gegnerklassen mit unterschiedlichen Werten +- 15 automatisch skalierende Missionswellen +- drei wählbare Schwierigkeitsgrade: Leicht entspricht dem bisherigen Balancing, Mittel ist der neue Standard und Schwer erhöht Gegnerhülle, Tempo und Anzahl deutlich +- höhere Schwierigkeitsgrade beginnen mit knapperen Ressourcen und belohnen einen Kartensieg mit mehr Kernkristallen +- freiwilliger Endlosmodus nach Welle 15 mit unbegrenzt weiter skalierenden Gegnern +- das Ende eines Endloslaufs zählt ausdrücklich nicht als gescheiterte Mission +- Kommandoschiff-Boss in jeder fünften Welle +- Platzierung per Maus oder Touch +- dreistufiges interaktives Einstiegstutorial +- Platzierungsvorschau mit gültiger beziehungsweise ungültiger Bauposition +- Turmverbesserungen bis Stufe 3 und Verkauf +- sichtbarer Reichweitenkreis bereits vor der Platzierung +- Turmkarten mit Schaden, Schüssen pro Sekunde, Reichweite sowie Einzel- oder Flächenschaden +- Upgrade-Vorschau mit dem konkreten Zuwachs an Schaden, Feuerrate und Reichweite +- kompaktes Turm-Informationsfenster, damit möglichst wenig von der Karte verdeckt wird +- korrigierte Zielausrichtung des Kinetik-Geschützes +- zusätzliche sinnvolle Bauplätze auf Bastion Prime, insbesondere zwischen den Wegschleifen und auf der linken Plattform +- zusätzliche Bauplätze zwischen den parallelen Fahrbahnen der Helios-Werft +- feste Reaktor-Sperrzonen auf allen Karten; auf Reaktoren und offenen Reaktorkammern wird kein Bauraster angezeigt und es können dort keine Türme platziert werden +- auf eine Rasterzelle verkleinerte Turmdarstellung, damit angrenzende Türme nicht in die Fahrbahn hineinragen +- dauerhaft gespeicherte Kernkristalle als spätere Freischaltwährung +- dauerhaftes lokales Gastprofil mit stabiler Spieler-ID als Grundlage für Apple-, Google- und Windows-Anmeldung +- eigene Profilansicht mit lokalen Missions- und Endlosbestwerten pro Karte +- wartende, lokal gespeicherte Highscore-Einträge für die spätere sichere Serverübertragung +- eigener Multiplayerbereich mit schnellem Spiel, Freundeseinladung und privatem Raum als vorbereitete Sucharten +- Vorschau einer symmetrisch gespiegelten Zwei-Spieler-Arena mit gemeinsamem Reaktor +- getrennte Ressourcenmodelle für beide Koop-Spieler sowie gemeinsam geteilte Reaktorleben +- sichtbarer, abbrechbarer Vorbereitungszustand für die spätere servergestützte Spielersuche +- bereits spielbarer lokaler Koop-Modus mit einem KI-Partner auf der gespiegelten Kartenhälfte +- nahezu verdoppelte Koop-Weltbreite: Beide Spieler erhalten eine vollständige Kartenhälfte in ursprünglicher Größe +- Spiegelachse direkt durch den vorhandenen Kartenreaktor, sodass Hintergrundreaktor, gemeinsamer Zielpunkt und Laufwege wieder zusammenpassen +- eigener Ansichtswechsel zwischen der unverkleinerten Spieler- und KI-Kartenhälfte +- zwei gleichwertige Gegnerströme von den äußeren Kartenrändern zum gemeinsamen Reaktor +- die KI baut und verbessert selbstständig Kinetik-, Laser- und Raketentürme ausschließlich auf ihrer Seite +- Abschussbelohnungen und Baukosten werden dem jeweiligen Spieler beziehungsweise KI-Partner getrennt zugerechnet +- farblich getrennte Bauflächen und markierte KI-Türme zur schnellen Zuordnung im Gefecht +- fünf Kernkristalle als Belohnung für jede erfolgreich abgeschlossene Karte +- normale und doppelte Spielgeschwindigkeit +- Pause, Missionssieg, Niederlage und lokaler Wellenrekord +- jederzeit sichtbarer Zurück-Button zum Abbrechen der laufenden Runde und Wechseln ins Kartenmenü +- lokaler Speicherstand für Rekord und Tutorialstatus +- für Mobilgeräte geeigneter Compatibility-Renderer +- vorbereitete AdMob-Anbindung für Android und iOS mit Einwilligungsabfrage +- freiwillige Belohnungswerbung für einmalig 150 Notfallenergie pro Mission +- zurückhaltende Vollbildwerbung frühestens nach drei Missionen und mit 15 Minuten Mindestabstand +- keine Bannerwerbung und keine erzwungene Werbung während einer Angriffswelle + +## Starten + +1. Godot 4.x installieren. +2. Im Godot-Projektmanager `project.godot` aus diesem Ordner importieren. +3. Das Projekt öffnen und **F6** oder **F5** drücken. + +Die Oberfläche ist für 1600 × 900 entworfen und skaliert automatisch auf andere Querformat-Auflösungen. + +Beim ersten Öffnen einer frisch entpackten Kopie kann der Import der drei großen HD-Karten je nach PC ungefähr eine Minute dauern. Danach startet das Projekt normal. + +Der Prototyp wurde mit der offiziellen Godot-Version 4.7.2 importiert und im automatisierten Headless-Test ohne Skript- oder Laufzeitfehler gestartet. + +Der mitgelieferte Logiktest lässt sich bei Bedarf mit `godot --headless --path . --script res://tests/smoke_test.gd` erneut ausführen. + +## Werbung und Datenschutz + +Die mobile Werbeanbindung ist technisch vorbereitet, aber absichtlich noch nicht für echte Werbung freigeschaltet: + +- `is_real` steht auf `false`; Android und iOS verwenden ausschließlich Googles offizielle Test-App- und Test-Anzeigen-IDs. +- Unter Windows bleibt das Werbesystem vollständig deaktiviert. +- Ein Belohnungsvideo erscheint nur nach einem freiwilligen Tipp auf **Video: +150 Notfallenergie** und höchstens einmal pro Mission. +- Vollbildwerbung kann nur am Ende einer Mission erscheinen, frühestens nach drei Missionen und niemals häufiger als alle 15 Minuten. +- Banner, App-Open-Anzeigen und Rewarded-Interstitials werden nicht verwendet. +- Vor der Initialisierung der Anzeigen wird auf Mobilgeräten Googles Consent-Information aktualisiert. Wenn eine Einwilligung erforderlich ist, wird zuerst das UMP-Formular angezeigt. +- Apples App-Tracking-Transparency-Abfrage ist derzeit deaktiviert. Sie wird erst aktiviert, wenn später tatsächlich Tracking beziehungsweise personalisierte Werbung benötigt und rechtlich geprüft wird. + +Für die spätere Veröffentlichung müssen im AdMob-Konto eigene App- und Anzeigen-IDs angelegt, in `ads_manager.gd` beziehungsweise den beiden Export-Konfigurationsdateien eingetragen und `is_real` bewusst auf `true` gesetzt werden. Zusätzlich brauchen wir eine veröffentlichte Datenschutzerklärung, die korrekte Datenschutzdeklaration in App Store Connect und Google Play sowie eine erreichbare `app-ads.txt`-Datei auf der Entwickler-Webseite. Bis diese Punkte erledigt und echte Geräte getestet sind, dürfen die Test-IDs nicht ersetzt werden. + +Verwendet wird **Godot AdMob Plugin 7.0** aus `godot-sdk-integrations/godot-admob`. Die Android- und iOS-Erweiterungen liegen mit im Projekt; ein iOS-Build benötigt später weiterhin macOS, Xcode und ein Apple-Developer-Konto. + +## Online-Backend + +Unter `server/nakama` liegt der reproduzierbare Docker-Compose-Stack für das selbst gehostete Online-Backend. Er besteht aus Nakama 3.37.0, einer eigenen internen PostgreSQL-16-Datenbank und einem täglichen Backup-Dienst mit 14 Tagen Aufbewahrung. + +Der Stack ist auf dem vorgesehenen Unraid-Server eingerichtet. Nakamas Spiel-API läuft auf Port 7350, die Verwaltungskonsole ist ausschließlich an den Loopback-Port 7351 gebunden und PostgreSQL besitzt keinen veröffentlichten Host-Port. Echte Geheimnisse werden nur in der geschützten `.env`-Datei auf Unraid gespeichert und durch `.gitignore` ausdrücklich aus dem Quellpaket ausgeschlossen. + +Die öffentliche Spiel-API ist unter `api.void-bastion.gergames.de` erreichbar. DNS, HTTPS/WSS über den vorhandenen Nginx Proxy Manager und die Godot-Nakama-Clientanbindung sind eingerichtet. Website und Spielserver verwenden damit getrennte Adressen. + +## Steuerung + +> **Online-Stand 0.19:** Der offizielle Nakama-Godot-Client, automatische Gastanmeldung, 36 autoritative Ranglisten, geprüfte Ergebnisübermittlung, Echtzeit-Sockets und das serverautoritative Zwei-Spieler-Matchmaking sind eingebaut und gegen Unraid getestet. Die öffentliche API ist über `https://api.void-bastion.gergames.de` mit einem gültigen Let’s-Encrypt-Zertifikat freigeschaltet. Details stehen in `ONLINE_SETUP.md`. + +- Turm unten auswählen und auf eine freie Plattformfläche klicken oder tippen. +- Am PC folgt der Reichweitenkreis vor dem Bau dem Mauszeiger. Auf Mobilgeräten markiert der erste Tipp den Bauplatz samt Reichweite; der zweite Tipp auf dasselbe Feld bestätigt den Bau. +- Nur die markierten Rasterfelder sind bebaubar; Türme rasten automatisch in der Feldmitte ein. Die sichtbare Straße wird exakt auf denselben Rasterzellen und derselben Mittellinie gezeichnet, denen auch die Gegner folgen. +- Die Einsatzkarte wird vor dem Start im Hauptmenü gewählt. +- Im Hauptmenü wird außerdem der Schwierigkeitsgrad gewählt; die eingeblendete Kurzbeschreibung zeigt die wichtigsten Auswirkungen und den Kristallbonus. +- Die komplette Einsatzkarte bleibt auf PC und Mobilgeräten sichtbar; ein Verschieben ist nicht notwendig. +- Einen vorhandenen Turm auswählen, um ihn zu verbessern oder zu verkaufen. +- Das Informationsfeld rechts oben zeigt dabei die aktuellen Werte und den Mehrwert der nächsten Stufe. +- `Nächste Welle` startet den Angriff. +- `1×` beziehungsweise `2×` ändert die Spielgeschwindigkeit. +- `P`, Escape oder die Pause-Schaltfläche pausiert das Spiel. +- Nach Welle 15 kann die erfolgreiche Mission beendet oder freiwillig im Endlosmodus fortgesetzt werden. +- Unter **Multiplayer → Mit KI-Partner** startet die gespiegelte Koop-Arena sofort. Du baust auf der vollständigen linken Karte, die KI auf deren vollständiger Spiegelung; beide Seiten verteidigen denselben Reaktor. Mit **KI >** und **< ICH** wird ohne Verkleinerung zwischen beiden Seiten gewechselt. + +## Nächste Produktionsschritte + +1. Balancing sowie Testanzeigen auf echten Android- und iOS-Geräten prüfen +2. Soundeffekte, Musik und zusätzliche Trefferanimationen +3. Kampagnenfortschritt und zusätzliche Bossvarianten +4. Freischaltmenü für neue Türme und Karten mit den bereits gespeicherten Kernkristallen +5. Plattformkonten mit einer eigenen plattformübergreifenden Spieler-ID verbinden +6. Autoritativen Koop-Spielserver, Räume, Freundeseinladungen und Spielersuche anbinden +7. Die gespiegelte Koop-Arena spielbar machen und anschließend über echte Geräte synchron testen +8. Store-Käufe erst nach stabiler Offline- und Koop-Kernversion + +## Godot Agent Tools MCP + +Das Projekt enthält das MIT-lizenzierte Godot-Addon **Agent Tools 0.3.1** unter `addons/agent_tools` sowie die lokale MCP-Brücke unter `tools/godot-agent-tools-mcp`. + +Der Plugin-Eintrag ist in `project.godot` aktiviert. Beim geöffneten Godot-Editor lauscht die lokale Brücke ausschließlich auf `127.0.0.1:9920`. Codex wurde systemweit mit dem Namen `godot-agent-tools` auf diese lokale Brücke konfiguriert. + +Geprüfter Zustand: + +- 70 MCP-Werkzeuge erkannt +- `res://main.tscn` als aktuelle Szene erkannt +- Szenenwurzel `VoidBastion` vom Typ `Node2D` erkannt +- 57 Projektverweise geprüft, keine Probleme gefunden + +Nach einem Neustart beziehungsweise einer neuen lokalen Codex-Sitzung stehen die Godot-Werkzeuge automatisch zur Verfügung, solange dieses Projekt im Godot-Editor geöffnet ist. + +## Grafikherkunft + +Die Karten und Sprite-Grafiken im Ordner `assets` wurden für dieses Projekt mit dem integrierten OpenAI-Bildgenerator erzeugt. Für Version 0.12 wurden die drei Karten als straßenfreie, hochauflösende Stationsdecks neu erzeugt und auf 3344 × 1882 Pixel aufbereitet. Die exakten Rasterstraßen entstehen nun im Spiel aus denselben Wegdaten wie Gegnerlinie und Bausperre. Die final verwendeten Prompts stehen in `ART_ASSETS.md`. diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md new file mode 100644 index 0000000..6de23e4 --- /dev/null +++ b/THIRD_PARTY_NOTICES.md @@ -0,0 +1,20 @@ +# Drittanbieter-Hinweise + +## Nakama Godot Client + +Dieses Projekt enthält den offiziellen Nakama Godot Client 3.4.0 von Heroic Labs. +Der Client steht unter der Apache License 2.0. Der vollständige Lizenztext liegt unter `addons/com.heroiclabs.nakama/LICENSE`. + +## Godot AdMob Plugin + +Dieses Projekt enthält Version 7.0 des Projekts `godot-sdk-integrations/godot-admob`. + +Copyright (c) 2024-present cengiz-pz und Mitwirkende + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/account_manager.gd b/account_manager.gd new file mode 100644 index 0000000..67d7436 --- /dev/null +++ b/account_manager.gd @@ -0,0 +1,154 @@ +class_name AccountManager +extends Node + +signal profile_changed + +const ACCOUNT_PATH := "user://void_bastion_account.json" + +var player_id := "" +var display_name := "" +var primary_provider := "guest" +var linked_identities: Array = [] +var highscores: Dictionary = {} +var pending_submissions: Array = [] +var created_at := 0 +var storage_enabled := true + + +func _ready() -> void: + _load_or_create_profile() + + +func _load_or_create_profile() -> void: + if FileAccess.file_exists(ACCOUNT_PATH): + var file := FileAccess.open(ACCOUNT_PATH, FileAccess.READ) + if file: + var data = JSON.parse_string(file.get_as_text()) + if data is Dictionary: + player_id = str(data.get("player_id", "")) + display_name = str(data.get("display_name", "")) + primary_provider = str(data.get("primary_provider", "guest")) + linked_identities = data.get("linked_identities", []) + highscores = data.get("highscores", {}) + pending_submissions = data.get("pending_submissions", []) + created_at = int(data.get("created_at", 0)) + if player_id.is_empty(): + _create_guest_profile() + if display_name.is_empty(): + display_name = "PILOT-%s" % player_id.right(4).to_upper() + _save_profile() + + +func _create_guest_profile() -> void: + var random_bytes := Crypto.new().generate_random_bytes(16) + player_id = "guest_%s" % random_bytes.hex_encode() + display_name = "PILOT-%s" % player_id.right(4).to_upper() + primary_provider = "guest" + linked_identities = [] + highscores = {} + pending_submissions = [] + created_at = int(Time.get_unix_time_from_system()) + + +func _save_profile() -> void: + if not storage_enabled: + return + var file := FileAccess.open(ACCOUNT_PATH, FileAccess.WRITE) + if not file: + return + file.store_string(JSON.stringify({ + "player_id": player_id, + "display_name": display_name, + "primary_provider": primary_provider, + "linked_identities": linked_identities, + "highscores": highscores, + "pending_submissions": pending_submissions, + "created_at": created_at + })) + + +func record_run(map_index: int, difficulty_index: int, endless: bool, reached_wave: int, score: int) -> bool: + var mode := "endless" if endless else "mission" + var key := "%d:%d:%s" % [map_index, difficulty_index, mode] + var previous: Dictionary = highscores.get(key, {}) + if int(previous.get("score", -1)) >= score: + return false + var record := { + "map": map_index, + "difficulty": difficulty_index, + "mode": mode, + "wave": reached_wave, + "score": score, + "recorded_at": int(Time.get_unix_time_from_system()) + } + highscores[key] = record + pending_submissions.append(record.duplicate(true)) + _save_profile() + profile_changed.emit() + return true + + +func attach_online_identity(online_user_id: String, online_username: String) -> void: + if online_user_id.is_empty(): + return + var identity := {"provider": "nakama", "user_id": online_user_id} + if not linked_identities.has(identity): + linked_identities.append(identity) + if not online_username.is_empty() and display_name.begins_with("PILOT-"): + display_name = online_username + _save_profile() + profile_changed.emit() + + +func mark_submission_complete(record: Dictionary) -> void: + for index in range(pending_submissions.size() - 1, -1, -1): + var queued = pending_submissions[index] + if queued is Dictionary and _same_run(queued, record): + pending_submissions.remove_at(index) + _save_profile() + profile_changed.emit() + return + + +func _same_run(left: Dictionary, right: Dictionary) -> bool: + return int(left.get("map", -1)) == int(right.get("map", -2)) \ + and int(left.get("difficulty", -1)) == int(right.get("difficulty", -2)) \ + and str(left.get("mode", "")) == str(right.get("mode", "_")) \ + and int(left.get("wave", -1)) == int(right.get("wave", -2)) \ + and int(left.get("score", -1)) == int(right.get("score", -2)) + + +func get_map_best_wave(map_index: int, endless := false) -> int: + var mode := "endless" if endless else "mission" + var result := 0 + for difficulty_index in 3: + var key := "%d:%d:%s" % [map_index, difficulty_index, mode] + var record: Dictionary = highscores.get(key, {}) + result = maxi(result, int(record.get("wave", 0))) + return result + + +func get_map_best_score(map_index: int, endless := false) -> int: + var mode := "endless" if endless else "mission" + var result := 0 + for difficulty_index in 3: + var key := "%d:%d:%s" % [map_index, difficulty_index, mode] + var record: Dictionary = highscores.get(key, {}) + result = maxi(result, int(record.get("score", 0))) + return result + + +func provider_label() -> String: + match primary_provider: + "apple": + return "APPLE / GAME CENTER" + "google": + return "GOOGLE PLAY GAMES" + "email": + return "E-MAIL" + _: + return "GASTPROFIL • NUR DIESES GERÄT" + + +func short_id() -> String: + return player_id.right(12).to_upper() diff --git a/account_manager.gd.uid b/account_manager.gd.uid new file mode 100644 index 0000000..c06fbd4 --- /dev/null +++ b/account_manager.gd.uid @@ -0,0 +1 @@ +uid://0vxrdsrx3qj2 diff --git a/addons/AdmobPlugin/Admob.gd b/addons/AdmobPlugin/Admob.gd new file mode 100644 index 0000000..81374db --- /dev/null +++ b/addons/AdmobPlugin/Admob.gd @@ -0,0 +1,1544 @@ +# +# © 2024-present https://github.com/cengiz-pz +# + +@tool +@icon("icon.png") +class_name Admob +extends Node + +signal initialization_completed(status_data: InitializationStatus) +signal banner_ad_loaded(ad_info: AdInfo, response_info: ResponseInfo) +signal banner_ad_failed_to_load(ad_info: AdInfo, error_data: LoadAdError) +signal banner_ad_refreshed(ad_info: AdInfo, response_info: ResponseInfo) +signal banner_ad_impression(ad_info: AdInfo) +signal banner_ad_size_measured(ad_info: AdInfo) +signal banner_ad_clicked(ad_info: AdInfo) +signal banner_ad_opened(ad_info: AdInfo) +signal banner_ad_closed(ad_info: AdInfo) +signal interstitial_ad_loaded(ad_info: AdInfo, response_info: ResponseInfo) +signal interstitial_ad_failed_to_load(ad_info: AdInfo, error_data: LoadAdError) +signal interstitial_ad_refreshed(ad_info: AdInfo, response_info: ResponseInfo) +signal interstitial_ad_impression(ad_info: AdInfo) +signal interstitial_ad_clicked(ad_info: AdInfo) +signal interstitial_ad_showed_full_screen_content(ad_info: AdInfo) +signal interstitial_ad_failed_to_show_full_screen_content(ad_info: AdInfo, error_data: AdError) +signal interstitial_ad_dismissed_full_screen_content(ad_info: AdInfo) +signal rewarded_ad_loaded(ad_info: AdInfo, response_info: ResponseInfo) +signal rewarded_ad_failed_to_load(ad_info: AdInfo, error_data: LoadAdError) +signal rewarded_ad_impression(ad_info: AdInfo) +signal rewarded_ad_clicked(ad_info: AdInfo) +signal rewarded_ad_showed_full_screen_content(ad_info: AdInfo) +signal rewarded_ad_failed_to_show_full_screen_content(ad_info: AdInfo, error_data: AdError) +signal rewarded_ad_dismissed_full_screen_content(ad_info: AdInfo) +signal rewarded_ad_user_earned_reward(ad_info: AdInfo, reward_data: RewardItem) +signal rewarded_interstitial_ad_loaded(ad_info: AdInfo, response_info: ResponseInfo) +signal rewarded_interstitial_ad_failed_to_load(ad_info: AdInfo, error_data: LoadAdError) +signal rewarded_interstitial_ad_impression(ad_info: AdInfo) +signal rewarded_interstitial_ad_clicked(ad_info: AdInfo) +signal rewarded_interstitial_ad_showed_full_screen_content(ad_info: AdInfo) +signal rewarded_interstitial_ad_failed_to_show_full_screen_content(ad_info: AdInfo, error_data: AdError) +signal rewarded_interstitial_ad_dismissed_full_screen_content(ad_info: AdInfo) +signal rewarded_interstitial_ad_user_earned_reward(ad_info: AdInfo, reward_data: RewardItem) +signal app_open_ad_loaded(ad_info: AdInfo, response_info: ResponseInfo) +signal app_open_ad_failed_to_load(ad_info: AdInfo, error_data: LoadAdError) +signal app_open_ad_impression(ad_info: AdInfo) +signal app_open_ad_clicked(ad_info: AdInfo) +signal app_open_ad_showed_full_screen_content(ad_info: AdInfo) +signal app_open_ad_failed_to_show_full_screen_content(ad_info: AdInfo, error_data: AdError) +signal app_open_ad_dismissed_full_screen_content(ad_info: AdInfo) +signal native_ad_loaded(ad_info: AdInfo, response_info: ResponseInfo) +signal native_ad_failed_to_load(ad_info: AdInfo, error_data: LoadAdError) +signal native_ad_impression(ad_info: AdInfo) +signal native_ad_size_measured(ad_info: AdInfo) +signal native_ad_clicked(ad_info: AdInfo) +signal native_ad_swipe_gesture_clicked(ad_info: AdInfo) +signal native_ad_opened(ad_info: AdInfo) +signal native_ad_closed(ad_info: AdInfo) +signal consent_form_loaded +signal consent_form_dismissed(error_data: FormError) +signal consent_form_failed_to_load(error_data: FormError) +signal consent_info_updated +signal consent_info_update_failed(error_data: FormError) +signal tracking_authorization_granted +signal tracking_authorization_denied + +const PLUGIN_SINGLETON_NAME: String = "AdmobPlugin" + +const ANDROID_BANNER_DEMO_AD_UNIT_ID: String = "ca-app-pub-3940256099942544/2014213617" +const ANDROID_INTERSTITIAL_DEMO_AD_UNIT_ID: String = "ca-app-pub-3940256099942544/1033173712" +const ANDROID_REWARDED_DEMO_AD_UNIT_ID: String = "ca-app-pub-3940256099942544/5224354917" +const ANDROID_REWARDED_INTERSTITIAL_DEMO_AD_UNIT_ID: String = "ca-app-pub-3940256099942544/5354046379" +const ANDROID_APP_OPEN_DEMO_AD_UNIT_ID: String = "ca-app-pub-3940256099942544/9257395921" +const ANDROID_NATIVE_DEMO_AD_UNIT_ID: String = "ca-app-pub-3940256099942544/2247696110" + +const IOS_BANNER_DEMO_AD_UNIT_ID: String = "ca-app-pub-3940256099942544/8388050270" +const IOS_INTERSTITIAL_DEMO_AD_UNIT_ID: String = "ca-app-pub-3940256099942544/4411468910" +const IOS_REWARDED_DEMO_AD_UNIT_ID: String = "ca-app-pub-3940256099942544/1712485313" +const IOS_REWARDED_INTERSTITIAL_DEMO_AD_UNIT_ID: String = "ca-app-pub-3940256099942544/6978759866" +const IOS_APP_OPEN_DEMO_AD_UNIT_ID: String = "ca-app-pub-3940256099942544/5575463023" +const IOS_NATIVE_DEMO_AD_UNIT_ID: String = "ca-app-pub-3940256099942544/3986624511" + +const MINIMUM_CACHE_SIZE: int = 1 +const MAXIMUM_CACHE_SIZE: int = 1000 + +@export_category("General") +## The plugin will use real app and ad IDs if true; debug IDs will be used otherwise. +@export var is_real: bool: + set = set_is_real + +## Setting used by publishers in Google's advertising platforms to specify the maximum content maturity level for the +## ads allowed to be served in their apps. +@export var max_ad_content_rating: AdmobConfig.ContentRating = AdmobConfig.ContentRating.G: + set = set_max_ad_content_rating + +## TFCD is a parameter that publishers and app developers use to indicate to Google that their content should be +## treated as child-directed for the purposes of the Children's Online Privacy Protection Act (COPPA). +@export +var child_directed: AdmobConfig.TagForChildDirectedTreatment = AdmobConfig.TagForChildDirectedTreatment.UNSPECIFIED: + set = set_child_directed + +## TFUA is a technical parameter to indicate that a user is under the digital age of consent in the European Economic +## Area (EEA), the UK, and Switzerland. +@export var under_age_of_consent: AdmobConfig.TagForUnderAgeOfConsent = AdmobConfig.TagForUnderAgeOfConsent.UNSPECIFIED: + set = set_under_age_of_consent + +## A configuration option that controls the use of first-party IDs for tracking user interactions. +@export var first_party_id_enabled: bool = true: + set = set_first_party_id_enabled + +## A parameter used to determine whether a user can receive personalized ads, non-personalized ads, or limited ads +## based on their consent choices. +@export var personalization_state: AdmobConfig.PersonalizationState = AdmobConfig.PersonalizationState.DEFAULT: + set = set_personalization_state + +## A string used to identify the origin of an ad request when a third-party library or platform is used to mediate +## requests to the Google Mobile Ads SDK. +@export var request_agent: String = PLUGIN_SINGLETON_NAME: + set = set_request_agent + +## Whether the plugin should automatically apply the configuration after initialization has been completed. +@export var auto_configure_on_initialize: bool = true + +@export_group("Global Settings", "global_") + +## Sets the ads' audio volume. Minimum value is 0.0 and maximum value is 1.0. +@export_range(0.0, 1.0, 0.01) var global_ad_volume: float = 1.0 + +## Whether or not the ads' audio will be muted. +@export var global_ads_muted: bool = false + +## Whether or not the global settings will be reapplied at app restart. +@export var global_apply_at_startup: bool = false + +@export_category("Ad Type-specific") +@export_group("Banner", "banner_") +## Part of the screen where the banner ad will be anchored at. +@export var banner_position: LoadAdRequest.AdPosition = LoadAdRequest.AdPosition.TOP: + set = set_banner_position + +## The size of the banner ad. +@export var banner_size: LoadAdRequest.RequestedAdSize = LoadAdRequest.RequestedAdSize.BANNER: + set = set_banner_size + +## If not set to disabled, then specifies the direction towards which the banner ad will collapse. +@export var banner_collapsible_position: LoadAdRequest.CollapsiblePosition = LoadAdRequest.CollapsiblePosition.DISABLED: + set = set_banner_collapsible_position + +## When enabled, the banner ad will be positioned within the device’s safe area, leaving space at the top or bottom to +## avoid UI elements such as notches, rounded corners, and home indicator bars. When disabled, the banner will be +## anchored directly to the top or bottom edge of the screen, ignoring safe area insets. +@export var banner_anchor_to_safe_area: bool = false + +@export_group("App Open") +## Specifies whether app open ad will be displayed when users return the app to the foreground state. +@export var auto_show_on_resume: bool = false: + set = set_auto_show_on_resume + +@export_category("Android-specific") +@export_group("Android Application IDs", "android_") +## Specifies the AdMob application ID used when testing the app on Android. +@export var android_debug_application_id: String = "" + +## Specifies the AdMob application ID used after releasing the Android app to production. +@export var android_real_application_id: String = "" + +@export_group("Android Debug Ad Unit IDs", "android_debug_") +## Specifies the AdMob banner ad ID used when testing the app on Android. +@export var android_debug_banner_id: String = ANDROID_BANNER_DEMO_AD_UNIT_ID + +## Specifies the AdMob interstitial ad ID used when testing the app on Android. +@export var android_debug_interstitial_id: String = ANDROID_INTERSTITIAL_DEMO_AD_UNIT_ID + +## Specifies the AdMob rewarded ad ID used when testing the app on Android. +@export var android_debug_rewarded_id: String = ANDROID_REWARDED_DEMO_AD_UNIT_ID + +## Specifies the AdMob rewarded-interstitial ad ID used when testing the app on Android. +@export var android_debug_rewarded_interstitial_id: String = ANDROID_REWARDED_INTERSTITIAL_DEMO_AD_UNIT_ID + +## Specifies the AdMob app open ad ID used when testing the app on Android. +@export var android_debug_app_open_id: String = ANDROID_APP_OPEN_DEMO_AD_UNIT_ID + +## Specifies the AdMob native ad ID used when testing the app on Android. +@export var android_debug_native_id: String = ANDROID_NATIVE_DEMO_AD_UNIT_ID + +@export_group("Android Real Ad Unit IDs", "android_real_") +## Specifies the AdMob banner ad ID used after releasing the Android app to production. +@export var android_real_banner_id: String = "" + +## Specifies the AdMob interstitial ad ID used after releasing the Android app to production. +@export var android_real_interstitial_id: String = "" + +## Specifies the AdMob rewarded ad ID used after releasing the Android app to production. +@export var android_real_rewarded_id: String = "" + +## Specifies the AdMob rewarded-interstitial ad ID used after releasing the Android app to production. +@export var android_real_rewarded_interstitial_id: String = "" + +## Specifies the AdMob app open ad ID used after releasing the Android app to production. +@export var android_real_app_open_id: String = "" + +## Specifies the AdMob native ad ID used after releasing the Android app to production. +@export var android_real_native_id: String = "" + +@export_category("iOS-specific") +@export_group("iOS Application IDs", "ios_") +## Specifies the AdMob application ID used when testing the app on iOS. +@export var ios_debug_application_id: String = "" + +## Specifies the AdMob application ID used after releasing the iOS app to production. +@export var ios_real_application_id: String = "" + +@export_group("iOS Debug Ad Unit IDs", "ios_debug_") +## Specifies the AdMob banner ad ID used when testing the app on iOS. +@export var ios_debug_banner_id: String = IOS_BANNER_DEMO_AD_UNIT_ID + +## Specifies the AdMob interstitial ad ID used when testing the app on iOS. +@export var ios_debug_interstitial_id: String = IOS_INTERSTITIAL_DEMO_AD_UNIT_ID + +## Specifies the AdMob rewarded ad ID used when testing the app on iOS. +@export var ios_debug_rewarded_id: String = IOS_REWARDED_DEMO_AD_UNIT_ID + +## Specifies the AdMob rewarded-interstitial ad ID used when testing the app on iOS. +@export var ios_debug_rewarded_interstitial_id: String = IOS_REWARDED_INTERSTITIAL_DEMO_AD_UNIT_ID + +## Specifies the AdMob app open ad ID used when testing the app on iOS. +@export var ios_debug_app_open_id: String = IOS_APP_OPEN_DEMO_AD_UNIT_ID + +## Specifies the AdMob native ad ID used when testing the app on ios. +@export var ios_debug_native_id: String = IOS_NATIVE_DEMO_AD_UNIT_ID + +@export_group("iOS Real Ad Unit IDs", "ios_real_") +## Specifies the AdMob banner ad ID used after releasing the iOS app to production. +@export var ios_real_banner_id: String = "" + +## Specifies the AdMob interstitial ad ID used after releasing the iOS app to production. +@export var ios_real_interstitial_id: String = "" + +## Specifies the AdMob rewarded ad ID used after releasing the iOS app to production. +@export var ios_real_rewarded_id: String = "" + +## Specifies the AdMob rewarded-interstitial ad ID used after releasing the iOS app to production. +@export var ios_real_rewarded_interstitial_id: String = "" + +## Specifies the AdMob app open ad ID used after releasing the iOS app to production. +@export var ios_real_app_open_id: String = "" + +## Specifies the AdMob native ad ID used after releasing the iOS app to production. +@export var ios_real_native_id: String = "" + +@export_group("App Tracking Transparency") +## Whether the iOS-specific App Tracking Transparency (ATT) work flow is enabled for the app. +@export var att_enabled: bool = false: + get: + return att_enabled + set(value): + att_enabled = value + +## The iOS-specific custom message explaining why your app is requesting tracking permission, to be displayed within +## the App Tracking Transparency (ATT) dialog presented to the user. +@export_multiline var att_text: String = "": + set = set_att_text + +@export_category("Mediation") +@export_group("Networks") +## List of ad networks whose SDKs will be attached to the exported app. +@export_flags(" ") var enabled_networks = 0 # Networks populated in _validate_property() function + +@export_group("Network Extras") +## Allows passing of additional, network-specific parameters from the app to an ad network's adapter during an ad +## request. +@export var network_extras: Array[NetworkExtras] = [] + +@export_category("Cache") +@export_group("Limits") +## Maximum number of banner ads to keep in the cache before removing old ads. +@export_range(MINIMUM_CACHE_SIZE, MAXIMUM_CACHE_SIZE) var max_banner_ad_cache: int = 10: + set = set_max_banner_ad_cache + +## Maximum number of interstitial ads to keep in the cache before removing old ads. +@export_range(MINIMUM_CACHE_SIZE, MAXIMUM_CACHE_SIZE) var max_interstitial_ad_cache: int = 3: + set = set_max_interstitial_ad_cache + +## Maximum number of rewarded ads to keep in the cache before removing old ads. +@export_range(MINIMUM_CACHE_SIZE, MAXIMUM_CACHE_SIZE) var max_rewarded_ad_cache: int = 3: + set = set_max_rewarded_ad_cache + +## Maximum number of rewarded-interstitial ads to keep in the cache before removing old ads. +@export_range(MINIMUM_CACHE_SIZE, MAXIMUM_CACHE_SIZE) var max_rewarded_interstitial_ad_cache: int = 3: + set = set_max_rewarded_interstitial_ad_cache + +## Maximum number of native ads to keep in the cache before removing old ads. +@export_range(MINIMUM_CACHE_SIZE, MAXIMUM_CACHE_SIZE) var max_native_ad_cache: int = 10: + set = set_max_native_ad_cache + +@export_group("Cleanup After Ad Displayed") # For single-use ad types. Banner ads are multi-use. +## Cleanup cached interstitial ads after they are displayed. Interstitial ads are single-use. +@export var remove_interstitial_ads_after_displayed: bool = false + +## Cleanup cached rewarded ads after they are displayed. Rewarded ads are single-use. +@export var remove_rewarded_ads_after_displayed: bool = true + +## Cleanup cached rewarded-interstitial ads after they are displayed. Rewarded-interstitial ads are single-use. +@export var remove_rewarded_interstitial_ads_after_displayed: bool = true + +@export_group("Cleanup After Scene Exit") +## Cleanup cached banner ads when the Admob node is destroyed. +@export var remove_banner_ads_after_scene: bool = true + +## Cleanup cached interstitial ads when the Admob node is destroyed. +@export var remove_interstitial_ads_after_scene: bool = true + +## Cleanup cached rewarded ads when the Admob node is destroyed. +@export var remove_rewarded_ads_after_scene: bool = true + +## Cleanup cached rewarded-interstitial ads when the Admob node is destroyed. +@export var remove_rewarded_interstitial_ads_after_scene: bool = true + +@export_category("Debug") +@export_group("Settings") +## The user location used when testing the consent management functionality. +@export var debug_geography: ConsentRequestParameters.DebugGeography = ConsentRequestParameters.DebugGeography.NOT_SET: + set = set_debug_geography + +## A list of SHA-256–hashed device IDs that should be registered as test devices in AdMob. +@export var test_device_hashed_ids: Array[String] = [] + +var is_initialization_completed: bool + +var _banner_id: String +var _interstitial_id: String +var _rewarded_id: String +var _rewarded_interstitial_id: String +var _app_open_id: String +var _native_id: String + +var _plugin_singleton: Object + +var _active_banner_ads: AdCache +var _active_interstitial_ads: AdCache +var _active_rewarded_ads: AdCache +var _active_rewarded_interstitial_ads: AdCache +var _active_native_ads: AdCache + +var _native_control_bindings: Dictionary = {} + + +func _init() -> void: + is_initialization_completed = false + + _active_banner_ads = AdCache.new(max_banner_ad_cache, "banner_ad", remove_banner_ad) + _active_interstitial_ads = ( + AdCache + . new( + max_interstitial_ad_cache, + "interstitial_ad", + remove_interstitial_ad, + ) + ) + _active_rewarded_ads = AdCache.new(max_rewarded_ad_cache, "rewarded_ad", remove_rewarded_ad) + _active_rewarded_interstitial_ads = ( + AdCache + . new( + max_rewarded_interstitial_ad_cache, + "rewarded_interstitial_ad", + remove_rewarded_interstitial_ad, + ) + ) + _active_native_ads = AdCache.new(max_native_ad_cache, "native_ad", remove_native_ad) + + +func _validate_property(property: Dictionary) -> void: + if property.name == "enabled_networks": + property.hint_string = ",".join(MediationNetwork.MEDIATION_NETWORK_TAGS.keys()) + + +func _ready() -> void: + if OS.has_feature("ios"): + if is_real: + _banner_id = ios_real_banner_id + _interstitial_id = ios_real_interstitial_id + _rewarded_id = ios_real_rewarded_id + _rewarded_interstitial_id = ios_real_rewarded_interstitial_id + _app_open_id = ios_real_app_open_id + _native_id = ios_real_native_id + else: + _banner_id = ios_debug_banner_id + _interstitial_id = ios_debug_interstitial_id + _rewarded_id = ios_debug_rewarded_id + _rewarded_interstitial_id = ios_debug_rewarded_interstitial_id + _app_open_id = ios_debug_app_open_id + _native_id = ios_debug_native_id + else: + if is_real: + _banner_id = android_real_banner_id + _interstitial_id = android_real_interstitial_id + _rewarded_id = android_real_rewarded_id + _rewarded_interstitial_id = android_real_rewarded_interstitial_id + _app_open_id = android_real_app_open_id + _native_id = android_real_native_id + else: + _banner_id = android_debug_banner_id + _interstitial_id = android_debug_interstitial_id + _rewarded_id = android_debug_rewarded_id + _rewarded_interstitial_id = android_debug_rewarded_interstitial_id + _app_open_id = android_debug_app_open_id + _native_id = android_debug_native_id + + _update_plugin() + + +func _exit_tree() -> void: + if _plugin_singleton: + if remove_banner_ads_after_scene: + for __ad_id in _active_banner_ads.all_keys(): + remove_banner_ad(__ad_id) + + if remove_interstitial_ads_after_scene: + for __ad_id in _active_interstitial_ads.all_keys(): + remove_interstitial_ad(__ad_id) + + if remove_rewarded_ads_after_scene: + for __ad_id in _active_rewarded_ads.all_keys(): + remove_rewarded_ad(__ad_id) + + if remove_rewarded_interstitial_ads_after_scene: + for __ad_id in _active_rewarded_interstitial_ads.all_keys(): + remove_rewarded_interstitial_ad(__ad_id) + + +func _notification(a_what: int) -> void: + if a_what == NOTIFICATION_APPLICATION_RESUMED: + _update_plugin() + + +func _update_plugin() -> void: + if _plugin_singleton == null: + if Engine.has_singleton(PLUGIN_SINGLETON_NAME): + _plugin_singleton = Engine.get_singleton(PLUGIN_SINGLETON_NAME) + _connect_signals() + elif not Engine.is_editor_hint(): + GmpLogger.log_error("%s singleton not found!" % PLUGIN_SINGLETON_NAME) + + +func _connect_signals() -> void: + _plugin_singleton.connect("initialization_completed", _on_initialization_completed) + _plugin_singleton.connect("banner_ad_loaded", _on_banner_ad_loaded) + _plugin_singleton.connect("banner_ad_failed_to_load", _on_banner_ad_failed_to_load) + _plugin_singleton.connect("banner_ad_refreshed", _on_banner_ad_refreshed) + _plugin_singleton.connect("banner_ad_impression", _on_banner_ad_impression) + _plugin_singleton.connect("banner_ad_size_measured", _on_banner_ad_size_measured) + _plugin_singleton.connect("banner_ad_clicked", _on_banner_ad_clicked) + _plugin_singleton.connect("banner_ad_opened", _on_banner_ad_opened) + _plugin_singleton.connect("banner_ad_closed", _on_banner_ad_closed) + _plugin_singleton.connect("interstitial_ad_loaded", _on_interstitial_ad_loaded) + _plugin_singleton.connect("interstitial_ad_failed_to_load", _on_interstitial_ad_failed_to_load) + _plugin_singleton.connect("interstitial_ad_refreshed", _on_interstitial_ad_refreshed) + _plugin_singleton.connect("interstitial_ad_impression", _on_interstitial_ad_impression) + _plugin_singleton.connect("interstitial_ad_clicked", _on_interstitial_ad_clicked) + ( + _plugin_singleton + . connect( + "interstitial_ad_showed_full_screen_content", + _on_interstitial_ad_showed_full_screen_content, + ) + ) + ( + _plugin_singleton + . connect( + "interstitial_ad_failed_to_show_full_screen_content", + _on_interstitial_ad_failed_to_show_full_screen_content, + ) + ) + ( + _plugin_singleton + . connect( + "interstitial_ad_dismissed_full_screen_content", + _on_interstitial_ad_dismissed_full_screen_content, + ) + ) + _plugin_singleton.connect("rewarded_ad_loaded", _on_rewarded_ad_loaded) + _plugin_singleton.connect("rewarded_ad_failed_to_load", _on_rewarded_ad_failed_to_load) + _plugin_singleton.connect("rewarded_ad_impression", _on_rewarded_ad_impression) + _plugin_singleton.connect("rewarded_ad_clicked", _on_rewarded_ad_clicked) + _plugin_singleton.connect("rewarded_ad_showed_full_screen_content", _on_rewarded_ad_showed_full_screen_content) + ( + _plugin_singleton + . connect( + "rewarded_ad_failed_to_show_full_screen_content", + _on_rewarded_ad_failed_to_show_full_screen_content, + ) + ) + ( + _plugin_singleton + . connect( + "rewarded_ad_dismissed_full_screen_content", + _on_rewarded_ad_dismissed_full_screen_content, + ) + ) + _plugin_singleton.connect("rewarded_ad_user_earned_reward", _on_rewarded_ad_user_earned_reward) + _plugin_singleton.connect("rewarded_interstitial_ad_loaded", _on_rewarded_interstitial_ad_loaded) + _plugin_singleton.connect("rewarded_interstitial_ad_failed_to_load", _on_rewarded_interstitial_ad_failed_to_load) + _plugin_singleton.connect("rewarded_interstitial_ad_impression", _on_rewarded_interstitial_ad_impression) + _plugin_singleton.connect("rewarded_interstitial_ad_clicked", _on_rewarded_interstitial_ad_clicked) + ( + _plugin_singleton + . connect( + "rewarded_interstitial_ad_showed_full_screen_content", + _on_rewarded_interstitial_ad_showed_full_screen_content, + ) + ) + ( + _plugin_singleton + . connect( + "rewarded_interstitial_ad_failed_to_show_full_screen_content", + _on_rewarded_interstitial_ad_failed_to_show_full_screen_content, + ) + ) + ( + _plugin_singleton + . connect( + "rewarded_interstitial_ad_dismissed_full_screen_content", + _on_rewarded_interstitial_ad_dismissed_full_screen_content, + ) + ) + ( + _plugin_singleton + . connect( + "rewarded_interstitial_ad_user_earned_reward", + _on_rewarded_interstitial_ad_user_earned_reward, + ) + ) + _plugin_singleton.connect("app_open_ad_loaded", _on_app_open_ad_loaded) + _plugin_singleton.connect("app_open_ad_failed_to_load", _on_app_open_ad_failed_to_load) + _plugin_singleton.connect("app_open_ad_impression", _on_app_open_ad_impression) + _plugin_singleton.connect("app_open_ad_clicked", _on_app_open_ad_clicked) + _plugin_singleton.connect("app_open_ad_showed_full_screen_content", _on_app_open_ad_showed_full_screen_content) + ( + _plugin_singleton + . connect( + "app_open_ad_failed_to_show_full_screen_content", + _on_app_open_ad_failed_to_show_full_screen_content, + ) + ) + ( + _plugin_singleton + . connect( + "app_open_ad_dismissed_full_screen_content", + _on_app_open_ad_dismissed_full_screen_content, + ) + ) + _plugin_singleton.connect("native_ad_loaded", _on_native_ad_loaded) + _plugin_singleton.connect("native_ad_failed_to_load", _on_native_ad_failed_to_load) + _plugin_singleton.connect("native_ad_impression", _on_native_ad_impression) + _plugin_singleton.connect("native_ad_size_measured", _on_native_ad_size_measured) + _plugin_singleton.connect("native_ad_clicked", _on_native_ad_clicked) + _plugin_singleton.connect("native_ad_swipe_gesture_clicked", _on_native_ad_swipe_gesture_clicked) + _plugin_singleton.connect("native_ad_opened", _on_native_ad_opened) + _plugin_singleton.connect("native_ad_closed", _on_native_ad_closed) + _plugin_singleton.connect("consent_form_loaded", _on_consent_form_loaded) + _plugin_singleton.connect("consent_form_dismissed", _on_consent_form_dismissed) + _plugin_singleton.connect("consent_form_failed_to_load", _on_consent_form_failed_to_load) + _plugin_singleton.connect("consent_info_updated", _on_consent_info_updated) + _plugin_singleton.connect("consent_info_update_failed", _on_consent_info_update_failed) + if _plugin_singleton.has_signal("tracking_authorization_granted"): + _plugin_singleton.connect("tracking_authorization_granted", _on_tracking_authorization_granted) + if _plugin_singleton.has_signal("tracking_authorization_denied"): + _plugin_singleton.connect("tracking_authorization_denied", _on_tracking_authorization_denied) + + +func initialize() -> void: + if _plugin_singleton == null: + GmpLogger.log_error("%s plugin not initialized" % PLUGIN_SINGLETON_NAME) + else: + _plugin_singleton.initialize() + + +func get_initialization_status() -> InitializationStatus: + var __status: InitializationStatus + + if _plugin_singleton == null: + GmpLogger.log_error("%s plugin not initialized" % PLUGIN_SINGLETON_NAME) + else: + __status = InitializationStatus.new(_plugin_singleton.get_initialization_status()) + + return __status + + +func set_is_real(a_value: bool) -> void: + is_real = a_value + + +func set_max_ad_content_rating(a_value: AdmobConfig.ContentRating) -> void: + max_ad_content_rating = a_value + + +func set_child_directed(a_value: AdmobConfig.TagForChildDirectedTreatment) -> void: + child_directed = a_value + + +func set_under_age_of_consent(a_value: AdmobConfig.TagForUnderAgeOfConsent) -> void: + under_age_of_consent = a_value + + +func set_first_party_id_enabled(a_value: bool) -> void: + first_party_id_enabled = a_value + + +func set_personalization_state(a_value: AdmobConfig.PersonalizationState) -> void: + personalization_state = a_value + + +func set_request_agent(a_value: String) -> void: + request_agent = a_value + + +func set_att_text(a_value: String) -> void: + att_text = a_value + + +func set_banner_position(a_value: LoadAdRequest.AdPosition) -> void: + banner_position = a_value + + +func set_banner_size(a_value: LoadAdRequest.RequestedAdSize) -> void: + banner_size = a_value + + +func set_banner_collapsible_position(a_value: LoadAdRequest.CollapsiblePosition) -> void: + banner_collapsible_position = a_value + + +func set_auto_show_on_resume(a_value: bool) -> void: + auto_show_on_resume = a_value + + +func set_max_banner_ad_cache(a_value: int) -> void: + max_banner_ad_cache = clampi(a_value, MINIMUM_CACHE_SIZE, MAXIMUM_CACHE_SIZE) + + +func set_max_interstitial_ad_cache(a_value: int) -> void: + max_interstitial_ad_cache = clampi(a_value, MINIMUM_CACHE_SIZE, MAXIMUM_CACHE_SIZE) + + +func set_max_rewarded_ad_cache(a_value: int) -> void: + max_rewarded_ad_cache = clampi(a_value, MINIMUM_CACHE_SIZE, MAXIMUM_CACHE_SIZE) + + +func set_max_rewarded_interstitial_ad_cache(a_value: int) -> void: + max_rewarded_interstitial_ad_cache = clampi(a_value, MINIMUM_CACHE_SIZE, MAXIMUM_CACHE_SIZE) + + +func set_max_native_ad_cache(a_value: int) -> void: + max_native_ad_cache = clampi(a_value, MINIMUM_CACHE_SIZE, MAXIMUM_CACHE_SIZE) + + +func set_debug_geography(a_value: ConsentRequestParameters.DebugGeography) -> void: + debug_geography = a_value + + +func is_mediation_enabled() -> bool: + return enabled_networks > 0 + + +func create_request_configuration() -> AdmobConfig: + var __config := AdmobConfig.new() + __config.set_is_real(is_real) + __config.set_max_ad_content_rating(max_ad_content_rating) + __config.set_child_directed_treatment(child_directed) + __config.set_under_age_of_consent(under_age_of_consent) + __config.set_first_party_id_enabled(first_party_id_enabled) + __config.set_personalization_state(personalization_state) + + return __config + + +func set_request_configuration(a_config: AdmobConfig = null) -> void: + if _plugin_singleton != null: + if a_config == null: + a_config = AdmobConfig.new().set_is_real(is_real).set_max_ad_content_rating(max_ad_content_rating) + a_config.set_child_directed_treatment(child_directed).set_under_age_of_consent(under_age_of_consent) + a_config.set_first_party_id_enabled(first_party_id_enabled).set_personalization_state(personalization_state) + + _plugin_singleton.set_request_configuration(a_config.get_raw_data()) + else: + GmpLogger.log_error("%s plugin not initialized" % PLUGIN_SINGLETON_NAME) + + +func set_app_pause_on_background(a_pause: bool) -> void: + if _plugin_singleton != null: + _plugin_singleton.set_app_pause_on_background(a_pause) + else: + GmpLogger.log_error("%s plugin not initialized" % PLUGIN_SINGLETON_NAME) + + +func get_global_settings() -> AdmobSettings: + var __settings: AdmobSettings + + if _plugin_singleton != null: + __settings = AdmobSettings.new(_plugin_singleton.get_global_settings()) + else: + GmpLogger.log_error("%s plugin not initialized" % PLUGIN_SINGLETON_NAME) + + return __settings + + +func set_global_settings(a_settings: AdmobSettings = null) -> void: + if _plugin_singleton != null: + if a_settings == null: + a_settings = AdmobSettings.new().set_ad_volume(global_ad_volume).set_ads_muted(global_ads_muted) + a_settings.set_apply_at_startup(global_apply_at_startup) + + _plugin_singleton.set_global_settings(a_settings.get_raw_data()) + else: + GmpLogger.log_error("%s plugin not initialized" % PLUGIN_SINGLETON_NAME) + + +func create_basic_ad_request() -> LoadAdRequest: + var __request: LoadAdRequest + + __request = LoadAdRequest.new().set_request_agent(request_agent) + + if is_mediation_enabled(): + __request.set_network_extras(NetworkExtras.build_raw_data_array(network_extras)) + + return __request + + +func create_banner_ad_request() -> LoadAdRequest: + var __request: LoadAdRequest = create_basic_ad_request().set_ad_unit_id(_banner_id).set_ad_position(banner_position) + __request.set_ad_size(banner_size).set_collapsible_position(banner_collapsible_position) + __request.set_anchor_to_safe_area(banner_anchor_to_safe_area) + return __request + + +func load_banner_ad(a_request: LoadAdRequest = null) -> void: + if _plugin_singleton != null: + if a_request == null: + a_request = create_banner_ad_request() + _plugin_singleton.load_banner_ad(a_request.get_raw_data()) + else: + GmpLogger.log_error("%s plugin not initialized" % PLUGIN_SINGLETON_NAME) + + +func is_banner_ad_loaded() -> bool: + if _plugin_singleton != null: + return _active_banner_ads.is_empty() == false + else: + GmpLogger.log_error("%s plugin not initialized" % PLUGIN_SINGLETON_NAME) + + return false + + +func show_banner_ad(a_ad_id: String = "") -> void: + if _plugin_singleton == null: + GmpLogger.log_error("%s plugin not initialized" % PLUGIN_SINGLETON_NAME) + else: + if a_ad_id.is_empty(): + if _active_banner_ads.is_empty(): + GmpLogger.log_error("Cannot show banner ad. No banner ads loaded.") + else: + _plugin_singleton.show_banner_ad(_active_banner_ads.last_key()) # show last ad to load + else: + if _active_banner_ads.has_key(a_ad_id): + _plugin_singleton.show_banner_ad(a_ad_id) + else: + GmpLogger.log_error("Cannot show banner. Ad with ID '%s' not found." % a_ad_id) + + +func hide_banner_ad(a_ad_id: String = "") -> void: + if _plugin_singleton == null: + GmpLogger.log_error("%s plugin not initialized" % PLUGIN_SINGLETON_NAME) + else: + if a_ad_id.is_empty(): + if _active_banner_ads.is_empty(): + GmpLogger.log_error("Cannot hide banner ad. No banner ads loaded.") + else: + _plugin_singleton.hide_banner_ad(_active_banner_ads.last_key()) # hide last ad to load + else: + if _active_banner_ads.has_key(a_ad_id): + _plugin_singleton.hide_banner_ad(a_ad_id) + else: + GmpLogger.log_error("Cannot hide banner. Ad with ID '%s' not found." % a_ad_id) + + +func remove_banner_ad(a_ad_id: String = "") -> void: + if _plugin_singleton == null: + GmpLogger.log_error("%s plugin not initialized" % PLUGIN_SINGLETON_NAME) + else: + if a_ad_id.is_empty(): + if _active_banner_ads.is_empty(): + GmpLogger.log_error("Cannot remove banner ad. No banner ads loaded.") + else: + _plugin_singleton.remove_banner_ad(_active_banner_ads.erase_last()) # remove last ad to load + else: + if _active_banner_ads.has_key(a_ad_id): + _active_banner_ads.erase(a_ad_id) + _plugin_singleton.remove_banner_ad(a_ad_id) + else: + GmpLogger.log_error("Cannot remove banner ad. Ad with ID '%s' not found." % a_ad_id) + + +func move_banner_ad(a_ad_id: String, a_x: float, a_y: float) -> void: + if _plugin_singleton == null: + GmpLogger.log_error("%s plugin not initialized" % PLUGIN_SINGLETON_NAME) + else: + _plugin_singleton.move_banner_ad(a_ad_id, a_x, a_y) + + +func get_banner_dimension(a_ad_id: String = "") -> Vector2: + if _plugin_singleton == null: + GmpLogger.log_error("%s plugin not initialized" % PLUGIN_SINGLETON_NAME) + else: + if a_ad_id.is_empty(): + if _active_banner_ads.is_empty(): + GmpLogger.log_error("Cannot get banner ad dimensions. No banner ads loaded.") + else: + var last_loaded_banner_ad_id = _active_banner_ads.last_key() + return Vector2( + _plugin_singleton.get_banner_width(last_loaded_banner_ad_id), + _plugin_singleton.get_banner_height(last_loaded_banner_ad_id), + ) + else: + return Vector2( + _plugin_singleton.get_banner_width(a_ad_id), + _plugin_singleton.get_banner_height(a_ad_id), + ) + + return Vector2.ZERO + + +func get_banner_dimension_in_pixels(a_ad_id: String = "") -> Vector2: + if _plugin_singleton == null: + GmpLogger.log_error("%s plugin not initialized" % PLUGIN_SINGLETON_NAME) + else: + if a_ad_id.is_empty(): + if _active_banner_ads.is_empty(): + GmpLogger.log_error("Cannot get banner ad dimensions. No banner ads loaded.") + else: + var last_loaded_banner_ad_id = _active_banner_ads.last_key() + return Vector2( + _plugin_singleton.get_banner_width_in_pixels(last_loaded_banner_ad_id), + _plugin_singleton.get_banner_height_in_pixels(last_loaded_banner_ad_id), + ) + else: + return Vector2( + _plugin_singleton.get_banner_width_in_pixels(a_ad_id), + _plugin_singleton.get_banner_height_in_pixels(a_ad_id), + ) + + return Vector2.ZERO + + +func get_current_adaptive_banner_size(a_width: int) -> AdSize: + var __ad_size: AdSize + + if _plugin_singleton == null: + GmpLogger.log_error("%s plugin not initialized" % PLUGIN_SINGLETON_NAME) + else: + return AdSize.new(_plugin_singleton.get_current_adaptive_banner_size(a_width)) + + return __ad_size + + +func get_portrait_adaptive_banner_size(a_width: int) -> AdSize: + var __ad_size: AdSize + + if _plugin_singleton == null: + GmpLogger.log_error("%s plugin not initialized" % PLUGIN_SINGLETON_NAME) + else: + return AdSize.new(_plugin_singleton.get_portrait_adaptive_banner_size(a_width)) + + return __ad_size + + +func get_landscape_adaptive_banner_size(a_width: int) -> AdSize: + var __ad_size: AdSize + + if _plugin_singleton == null: + GmpLogger.log_error("%s plugin not initialized" % PLUGIN_SINGLETON_NAME) + else: + return AdSize.new(_plugin_singleton.get_landscape_adaptive_banner_size(a_width)) + + return __ad_size + + +func create_interstitial_ad_request() -> LoadAdRequest: + return create_basic_ad_request().set_ad_unit_id(_interstitial_id) + + +func load_interstitial_ad(a_request: LoadAdRequest = null) -> void: + if _plugin_singleton != null: + if a_request == null: + a_request = create_interstitial_ad_request() + _plugin_singleton.load_interstitial_ad(a_request.get_raw_data()) + else: + GmpLogger.log_error("%s plugin not initialized" % PLUGIN_SINGLETON_NAME) + + +func is_interstitial_ad_loaded() -> bool: + if _plugin_singleton != null: + return _active_interstitial_ads.is_empty() == false + else: + GmpLogger.log_error("%s plugin not initialized" % PLUGIN_SINGLETON_NAME) + + return false + + +func show_interstitial_ad(a_ad_id: String = "") -> void: + if _plugin_singleton == null: + GmpLogger.log_error("%s plugin not initialized" % PLUGIN_SINGLETON_NAME) + else: + if a_ad_id.is_empty(): + if _active_interstitial_ads.is_empty(): + GmpLogger.log_error("Cannot show interstitial ad. No interstitial ads loaded.") + else: + _plugin_singleton.show_interstitial_ad(_active_interstitial_ads.last_key()) # show last ad to load + else: + _plugin_singleton.show_interstitial_ad(a_ad_id) + + +func remove_interstitial_ad(a_ad_id: String = "") -> void: + if _plugin_singleton == null: + GmpLogger.log_error("%s plugin not initialized" % PLUGIN_SINGLETON_NAME) + else: + if a_ad_id.is_empty(): + if _active_interstitial_ads.is_empty(): + GmpLogger.log_error("Cannot remove interstitial ad. No interstitial ads loaded.") + else: + _plugin_singleton.remove_interstitial_ad(_active_interstitial_ads.erase_last()) # remove last ad to load + else: + if _active_interstitial_ads.has_key(a_ad_id): + _active_interstitial_ads.erase(a_ad_id) + _plugin_singleton.remove_interstitial_ad(a_ad_id) + else: + GmpLogger.log_error("Cannot remove interstitial ad. Ad with ID '%s' not found." % a_ad_id) + + +func create_rewarded_ad_request() -> LoadAdRequest: + return create_basic_ad_request().set_ad_unit_id(_rewarded_id) + + +func load_rewarded_ad(a_request: LoadAdRequest = null) -> void: + if _plugin_singleton != null: + if a_request == null: + a_request = create_rewarded_ad_request() + _plugin_singleton.load_rewarded_ad(a_request.get_raw_data()) + else: + GmpLogger.log_error("%s plugin not initialized" % PLUGIN_SINGLETON_NAME) + + +func is_rewarded_ad_loaded() -> bool: + if _plugin_singleton != null: + return _active_rewarded_ads.is_empty() == false + else: + GmpLogger.log_error("%s plugin not initialized" % PLUGIN_SINGLETON_NAME) + + return false + + +func show_rewarded_ad(a_ad_id: String = "") -> void: + if _plugin_singleton == null: + GmpLogger.log_error("%s plugin not initialized" % PLUGIN_SINGLETON_NAME) + else: + if a_ad_id.is_empty(): + if _active_rewarded_ads.is_empty(): + GmpLogger.log_error("Cannot show rewarded ad. No rewarded ads loaded.") + else: + _plugin_singleton.show_rewarded_ad(_active_rewarded_ads.last_key()) # show last ad to load + else: + _plugin_singleton.show_rewarded_ad(a_ad_id) + + +func remove_rewarded_ad(a_ad_id: String = "") -> void: + if _plugin_singleton == null: + GmpLogger.log_error("%s plugin not initialized" % PLUGIN_SINGLETON_NAME) + else: + if a_ad_id.is_empty(): + if _active_rewarded_ads.is_empty(): + GmpLogger.log_error("Cannot remove rewarded ad. No rewarded ads loaded.") + else: + _plugin_singleton.remove_rewarded_ad(_active_rewarded_ads.erase_last()) # remove last ad to load + else: + if _active_rewarded_ads.has_key(a_ad_id): + _active_rewarded_ads.erase(a_ad_id) + _plugin_singleton.remove_rewarded_ad(a_ad_id) + else: + GmpLogger.log_error("Cannot remove rewarded ad. Ad with ID '%s' not found." % a_ad_id) + + +func create_rewarded_interstitial_ad_request() -> LoadAdRequest: + return create_basic_ad_request().set_ad_unit_id(_rewarded_interstitial_id) + + +func load_rewarded_interstitial_ad(a_request: LoadAdRequest = null) -> void: + if _plugin_singleton != null: + if a_request == null: + a_request = create_rewarded_interstitial_ad_request() + _plugin_singleton.load_rewarded_interstitial_ad(a_request.get_raw_data()) + else: + GmpLogger.log_error("%s plugin not initialized" % PLUGIN_SINGLETON_NAME) + + +func is_rewarded_interstitial_ad_loaded() -> bool: + if _plugin_singleton != null: + return _active_rewarded_interstitial_ads.is_empty() == false + else: + GmpLogger.log_error("%s plugin not initialized" % PLUGIN_SINGLETON_NAME) + + return false + + +func show_rewarded_interstitial_ad(a_ad_id: String = "") -> void: + if _plugin_singleton == null: + GmpLogger.log_error("%s plugin not initialized" % PLUGIN_SINGLETON_NAME) + else: + if a_ad_id.is_empty(): + if _active_rewarded_interstitial_ads.is_empty(): + GmpLogger.log_error("Cannot show rewarded interstitial ad. No rewarded interstitial ads loaded.") + else: + # show last ad to load + _plugin_singleton.show_rewarded_interstitial_ad(_active_rewarded_interstitial_ads.last_key()) + else: + _plugin_singleton.show_rewarded_interstitial_ad(a_ad_id) + + +func remove_rewarded_interstitial_ad(a_ad_id: String = "") -> void: + if _plugin_singleton == null: + GmpLogger.log_error("%s plugin not initialized" % PLUGIN_SINGLETON_NAME) + else: + if a_ad_id.is_empty(): + if _active_rewarded_interstitial_ads.is_empty(): + GmpLogger.log_error("Cannot remove rewarded interstitial ad. No rewarded interstitial ads loaded.") + else: + # remove last ad to load + _plugin_singleton.remove_rewarded_interstitial_ad(_active_rewarded_interstitial_ads.erase_last()) + else: + if _active_rewarded_interstitial_ads.has_key(a_ad_id): + _active_rewarded_interstitial_ads.erase(a_ad_id) + _plugin_singleton.remove_rewarded_interstitial_ad(a_ad_id) + else: + GmpLogger.log_error("Cannot remove rewarded interstitial ad. Ad with ID '%s' not found." % a_ad_id) + + +func load_app_open_ad(a_request: LoadAdRequest = null) -> void: + if _plugin_singleton == null: + GmpLogger.log_error("%s plugin not initialized" % PLUGIN_SINGLETON_NAME) + else: + if a_request == null: + a_request = LoadAdRequest.new().set_ad_unit_id(_app_open_id).set_request_agent(request_agent) + if is_mediation_enabled(): + a_request.set_network_extras(NetworkExtras.build_raw_data_array(network_extras)) + _plugin_singleton.load_app_open_ad(a_request.get_raw_data(), auto_show_on_resume) + + +func show_app_open_ad() -> void: + if _plugin_singleton == null: + GmpLogger.log_error("%s plugin not initialized" % PLUGIN_SINGLETON_NAME) + else: + _plugin_singleton.show_app_open_ad() + + +func is_app_open_ad_available() -> bool: + if _plugin_singleton == null: + GmpLogger.log_error("%s plugin not initialized" % PLUGIN_SINGLETON_NAME) + return false + else: + return _plugin_singleton.is_app_open_ad_available() + + +func create_native_ad_request() -> LoadAdRequest: + return create_basic_ad_request().set_ad_unit_id(_native_id) + + +func load_native_ad(a_request: LoadAdRequest = null) -> void: + if _plugin_singleton != null: + if a_request == null: + a_request = create_native_ad_request() + _plugin_singleton.load_native_ad(a_request.get_raw_data()) + else: + GmpLogger.log_error("%s plugin not initialized" % PLUGIN_SINGLETON_NAME) + + +func is_native_ad_loaded() -> bool: + if _plugin_singleton != null: + return _active_native_ads.is_empty() == false + else: + GmpLogger.log_error("%s plugin not initialized" % PLUGIN_SINGLETON_NAME) + + return false + + +func show_native_ad(a_ad_id: String = "") -> void: + if _plugin_singleton == null: + GmpLogger.log_error("%s plugin not initialized" % PLUGIN_SINGLETON_NAME) + else: + if a_ad_id.is_empty(): + if _active_native_ads.is_empty(): + GmpLogger.log_error("Cannot show native ad. No native ads loaded.") + else: + _plugin_singleton.show_native_ad(_active_native_ads.last_key()) + else: + if _active_native_ads.has_key(a_ad_id): + _plugin_singleton.show_native_ad(a_ad_id) + else: + GmpLogger.log_error("Cannot show native ad. Ad with ID '%s' not found." % a_ad_id) + + +func hide_native_ad(a_ad_id: String = "") -> void: + if _plugin_singleton == null: + GmpLogger.log_error("%s plugin not initialized" % PLUGIN_SINGLETON_NAME) + else: + if a_ad_id.is_empty(): + if _active_native_ads.is_empty(): + GmpLogger.log_error("Cannot hide native ad. No native ads loaded.") + else: + _plugin_singleton.hide_native_ad(_active_native_ads.last_key()) + else: + if _active_native_ads.has_key(a_ad_id): + _plugin_singleton.hide_native_ad(a_ad_id) + else: + GmpLogger.log_error("Cannot hide native ad. Ad with ID '%s' not found." % a_ad_id) + + +func remove_native_ad(a_ad_id: String = "") -> void: + if _plugin_singleton == null: + GmpLogger.log_error("%s plugin not initialized" % PLUGIN_SINGLETON_NAME) + else: + if a_ad_id.is_empty(): + if _active_native_ads.is_empty(): + GmpLogger.log_error("Cannot remove native ad. No native ads loaded.") + else: + var _ad_id = _active_native_ads.erase_last() + detach_native_ad(_ad_id) + _plugin_singleton.remove_native_ad(_ad_id) + else: + if _active_native_ads.has_key(a_ad_id): + _active_native_ads.erase(a_ad_id) + detach_native_ad(a_ad_id) + _plugin_singleton.remove_native_ad(a_ad_id) + else: + GmpLogger.log_error("Cannot remove native ad. Ad with ID '%s' not found." % a_ad_id) + + +## Attaches a native ad to a Control node. The native ad will follow the Control's position, size and visibility. +func attach_native_ad_to_control(ad_id: String, control: Control) -> void: + if _plugin_singleton == null: + GmpLogger.log_error("%s plugin not initialized" % PLUGIN_SINGLETON_NAME) + else: + if ad_id.is_empty() or control == null: + ( + GmpLogger + . log_error( + "Cannot attach native ad to control. Either Ad with ID '%s' not found, or control is null" % ad_id, + ) + ) + else: + if _native_control_bindings.has(ad_id): + GmpLogger.log_info("Native ad %s re-attached to new control" % ad_id) + _native_control_bindings[ad_id] = control + _sync_native_ad_with_control(ad_id, control) # Initial sync + + +func detach_native_ad(ad_id: String) -> void: + if _native_control_bindings.has(ad_id): + _native_control_bindings.erase(ad_id) + + +func _sync_native_ad_with_control(ad_id: String, control: Control) -> void: + if not is_instance_valid(control): + detach_native_ad(ad_id) + return + + var viewport := control.get_viewport() + var vp_rect := viewport.get_visible_rect() + var window_size := DisplayServer.window_get_size() + + var scale_x := window_size.x / vp_rect.size.x + var scale_y := window_size.y / vp_rect.size.y + + var canvas_pos := control.get_global_transform_with_canvas().origin + var canvas_size := control.get_rect().size + + var win_x := (canvas_pos.x - vp_rect.position.x) * scale_x + var win_y := (canvas_pos.y - vp_rect.position.y) * scale_y + var win_w := canvas_size.x * scale_x + var win_h := canvas_size.y * scale_y + ( + _plugin_singleton + . update_native_ad_layout( + ad_id, + int(win_x), + int(win_y), + int(win_w), + int(win_h), + control.is_visible_in_tree(), + ) + ) + + +func _process(_delta): + for ad_id in _native_control_bindings.keys(): + var control = _native_control_bindings[ad_id] + _sync_native_ad_with_control(ad_id, control) + + +func load_consent_form() -> void: + if _plugin_singleton == null: + GmpLogger.log_error("%s plugin not initialized" % PLUGIN_SINGLETON_NAME) + else: + _plugin_singleton.load_consent_form() + + +func show_consent_form() -> void: + if _plugin_singleton == null: + GmpLogger.log_error("%s plugin not initialized" % PLUGIN_SINGLETON_NAME) + else: + _plugin_singleton.show_consent_form() + + +func get_consent_status() -> UserConsent: + var __result: String + + if _plugin_singleton == null: + GmpLogger.log_error("%s plugin not initialized" % PLUGIN_SINGLETON_NAME) + else: + __result = _plugin_singleton.get_consent_status() + + return UserConsent.new(__result) if __result else null + + +func is_consent_form_available() -> bool: + if _plugin_singleton == null: + GmpLogger.log_error("%s plugin not initialized" % PLUGIN_SINGLETON_NAME) + else: + return _plugin_singleton.is_consent_form_available() + return false + + +func update_consent_info(a_parameters: ConsentRequestParameters = null) -> void: + if _plugin_singleton == null: + GmpLogger.log_error("%s plugin not initialized" % PLUGIN_SINGLETON_NAME) + else: + if a_parameters == null: + a_parameters = ConsentRequestParameters.new() + + if under_age_of_consent != AdmobConfig.TagForUnderAgeOfConsent.UNSPECIFIED: + a_parameters.set_tag_for_under_age_of_consent(under_age_of_consent) + + if not is_real: + if debug_geography != ConsentRequestParameters.DebugGeography.NOT_SET: + a_parameters.set_debug_geography(debug_geography) + + for __device_id in test_device_hashed_ids: + a_parameters.add_test_device_hashed_id(__device_id) + + a_parameters.set_is_real(is_real) + + _plugin_singleton.update_consent_info(a_parameters.get_raw_data()) + + +func reset_consent_info() -> void: + if _plugin_singleton == null: + GmpLogger.log_error("%s plugin not initialized" % PLUGIN_SINGLETON_NAME) + else: + _plugin_singleton.reset_consent_info() + + +func set_mediation_privacy_settings(privacySettings: NetworkPrivacySettings) -> void: + if _plugin_singleton == null: + GmpLogger.log_error("%s plugin not initialized" % PLUGIN_SINGLETON_NAME) + else: + var __tags := MediationNetwork.get_all_enabled_tags(enabled_networks) + _plugin_singleton.set_mediation_privacy_settings(privacySettings.set_enabled_networks(__tags).get_raw_data()) + + +func request_tracking_authorization() -> void: + if _plugin_singleton == null: + GmpLogger.log_error("%s plugin not initialized" % PLUGIN_SINGLETON_NAME) + else: + if _plugin_singleton.has_method("request_tracking_authorization"): + _plugin_singleton.request_tracking_authorization() + else: + GmpLogger.log_error("request_tracking_authorization() method is not supported") + + +func open_app_settings() -> void: + if _plugin_singleton == null: + GmpLogger.log_error("%s plugin not initialized" % PLUGIN_SINGLETON_NAME) + else: + if _plugin_singleton.has_method("open_app_settings"): + _plugin_singleton.open_app_settings() + else: + GmpLogger.log_error("open_app_settings() method is not supported") + + +func _on_initialization_completed(status_data: Dictionary) -> void: + if auto_configure_on_initialize: + set_request_configuration() + + is_initialization_completed = true + initialization_completed.emit(InitializationStatus.new(status_data)) + + +func _on_banner_ad_loaded(a_ad_data: Dictionary, a_response_info: Dictionary) -> void: + var __ad_info: AdInfo = AdInfo.new(a_ad_data) + var __response_info: ResponseInfo = ResponseInfo.new(a_response_info) + _active_banner_ads.cache(__ad_info.get_ad_id(), __response_info) + banner_ad_loaded.emit(__ad_info, __response_info) + + +func _on_banner_ad_failed_to_load(a_ad_data: Dictionary, error_data: Dictionary) -> void: + banner_ad_failed_to_load.emit(AdInfo.new(a_ad_data), LoadAdError.new(error_data)) + + +func _on_banner_ad_refreshed(a_ad_data: Dictionary, a_response_info: Dictionary) -> void: + banner_ad_refreshed.emit(AdInfo.new(a_ad_data), ResponseInfo.new(a_response_info)) + + +func _on_banner_ad_impression(a_ad_data: Dictionary) -> void: + banner_ad_impression.emit(AdInfo.new(a_ad_data)) + + +func _on_banner_ad_size_measured(a_ad_data: Dictionary) -> void: + banner_ad_size_measured.emit(AdInfo.new(a_ad_data)) + + +func _on_banner_ad_clicked(a_ad_data: Dictionary) -> void: + banner_ad_clicked.emit(AdInfo.new(a_ad_data)) + + +func _on_banner_ad_opened(a_ad_data: Dictionary) -> void: + banner_ad_opened.emit(AdInfo.new(a_ad_data)) + + +func _on_banner_ad_closed(a_ad_data: Dictionary) -> void: + banner_ad_closed.emit(AdInfo.new(a_ad_data)) + + +func _on_interstitial_ad_loaded(a_ad_data: Dictionary, a_response_info: Dictionary) -> void: + var __ad_info: AdInfo = AdInfo.new(a_ad_data) + var __response_info: ResponseInfo = ResponseInfo.new(a_response_info) + _active_interstitial_ads.cache(__ad_info.get_ad_id(), __response_info) + interstitial_ad_loaded.emit(__ad_info, __response_info) + + +func _on_interstitial_ad_failed_to_load(a_ad_data: Dictionary, error_data: Dictionary) -> void: + interstitial_ad_failed_to_load.emit(AdInfo.new(a_ad_data), LoadAdError.new(error_data)) + + +func _on_interstitial_ad_refreshed(a_ad_data: Dictionary, a_response_info: Dictionary) -> void: + interstitial_ad_refreshed.emit(AdInfo.new(a_ad_data), ResponseInfo.new(a_response_info)) + + +func _on_interstitial_ad_impression(a_ad_data: Dictionary) -> void: + interstitial_ad_impression.emit(AdInfo.new(a_ad_data)) + + +func _on_interstitial_ad_clicked(a_ad_data: Dictionary) -> void: + interstitial_ad_clicked.emit(AdInfo.new(a_ad_data)) + + +func _on_interstitial_ad_showed_full_screen_content(a_ad_data: Dictionary) -> void: + interstitial_ad_showed_full_screen_content.emit(AdInfo.new(a_ad_data)) + + +func _on_interstitial_ad_failed_to_show_full_screen_content(a_ad_data: Dictionary, error_data: Dictionary) -> void: + interstitial_ad_failed_to_show_full_screen_content.emit(AdInfo.new(a_ad_data), AdError.new(error_data)) + + +func _on_interstitial_ad_dismissed_full_screen_content(a_ad_data: Dictionary) -> void: + var __ad_info: AdInfo = AdInfo.new(a_ad_data) + if remove_interstitial_ads_after_displayed: + remove_interstitial_ad(__ad_info.get_ad_id()) + interstitial_ad_dismissed_full_screen_content.emit(__ad_info) + + +func _on_rewarded_ad_loaded(a_ad_data: Dictionary, a_response_info: Dictionary) -> void: + var __ad_info: AdInfo = AdInfo.new(a_ad_data) + var __response_info: ResponseInfo = ResponseInfo.new(a_response_info) + _active_rewarded_ads.cache(__ad_info.get_ad_id(), __response_info) + rewarded_ad_loaded.emit(__ad_info, __response_info) + + +func _on_rewarded_ad_failed_to_load(a_ad_data: Dictionary, error_data: Dictionary) -> void: + rewarded_ad_failed_to_load.emit(AdInfo.new(a_ad_data), LoadAdError.new(error_data)) + + +func _on_rewarded_ad_impression(a_ad_data: Dictionary) -> void: + rewarded_ad_impression.emit(AdInfo.new(a_ad_data)) + + +func _on_rewarded_ad_clicked(a_ad_data: Dictionary) -> void: + rewarded_ad_clicked.emit(AdInfo.new(a_ad_data)) + + +func _on_rewarded_ad_showed_full_screen_content(a_ad_data: Dictionary) -> void: + rewarded_ad_showed_full_screen_content.emit(AdInfo.new(a_ad_data)) + + +func _on_rewarded_ad_failed_to_show_full_screen_content(a_ad_data: Dictionary, error_data: Dictionary) -> void: + rewarded_ad_failed_to_show_full_screen_content.emit(AdInfo.new(a_ad_data), AdError.new(error_data)) + + +func _on_rewarded_ad_dismissed_full_screen_content(a_ad_data: Dictionary) -> void: + var __ad_info: AdInfo = AdInfo.new(a_ad_data) + if remove_rewarded_ads_after_displayed: + remove_rewarded_ad(__ad_info.get_ad_id()) + rewarded_ad_dismissed_full_screen_content.emit(__ad_info) + + +func _on_rewarded_ad_user_earned_reward(a_ad_data: Dictionary, reward_data: Dictionary) -> void: + rewarded_ad_user_earned_reward.emit(AdInfo.new(a_ad_data), RewardItem.new(reward_data)) + + +func _on_rewarded_interstitial_ad_loaded(a_ad_data: Dictionary, a_response_info: Dictionary) -> void: + var __ad_info: AdInfo = AdInfo.new(a_ad_data) + var __response_info: ResponseInfo = ResponseInfo.new(a_response_info) + _active_rewarded_interstitial_ads.cache(__ad_info.get_ad_id(), __response_info) + rewarded_interstitial_ad_loaded.emit(__ad_info, __response_info) + + +func _on_rewarded_interstitial_ad_failed_to_load(a_ad_data: Dictionary, error_data: Dictionary) -> void: + rewarded_interstitial_ad_failed_to_load.emit(AdInfo.new(a_ad_data), LoadAdError.new(error_data)) + + +func _on_rewarded_interstitial_ad_impression(a_ad_data: Dictionary) -> void: + rewarded_interstitial_ad_impression.emit(AdInfo.new(a_ad_data)) + + +func _on_rewarded_interstitial_ad_clicked(a_ad_data: Dictionary) -> void: + rewarded_interstitial_ad_clicked.emit(AdInfo.new(a_ad_data)) + + +func _on_rewarded_interstitial_ad_showed_full_screen_content(a_ad_data: Dictionary) -> void: + rewarded_interstitial_ad_showed_full_screen_content.emit(AdInfo.new(a_ad_data)) + + +func _on_rewarded_interstitial_ad_failed_to_show_full_screen_content( + a_ad_data: Dictionary, + error_data: Dictionary, +) -> void: + rewarded_interstitial_ad_failed_to_show_full_screen_content.emit(AdInfo.new(a_ad_data), AdError.new(error_data)) + + +func _on_rewarded_interstitial_ad_dismissed_full_screen_content(a_ad_data: Dictionary) -> void: + var __ad_info: AdInfo = AdInfo.new(a_ad_data) + if remove_rewarded_interstitial_ads_after_displayed: + remove_rewarded_interstitial_ad(__ad_info.get_ad_id()) + rewarded_interstitial_ad_dismissed_full_screen_content.emit(__ad_info) + + +func _on_rewarded_interstitial_ad_user_earned_reward(a_ad_data: Dictionary, reward_data: Dictionary) -> void: + rewarded_interstitial_ad_user_earned_reward.emit(AdInfo.new(a_ad_data), RewardItem.new(reward_data)) + + +func _on_app_open_ad_loaded(a_ad_data: Dictionary, a_response_info: Dictionary) -> void: + app_open_ad_loaded.emit(AdInfo.new(a_ad_data), ResponseInfo.new(a_response_info)) + + +func _on_app_open_ad_failed_to_load(a_ad_data: Dictionary, error_data: Dictionary) -> void: + app_open_ad_failed_to_load.emit(AdInfo.new(a_ad_data), LoadAdError.new(error_data)) + + +func _on_app_open_ad_impression(a_ad_data: Dictionary) -> void: + app_open_ad_impression.emit(AdInfo.new(a_ad_data)) + + +func _on_app_open_ad_clicked(a_ad_data: Dictionary) -> void: + app_open_ad_clicked.emit(AdInfo.new(a_ad_data)) + + +func _on_app_open_ad_showed_full_screen_content(a_ad_data: Dictionary) -> void: + app_open_ad_showed_full_screen_content.emit(AdInfo.new(a_ad_data)) + + +func _on_app_open_ad_failed_to_show_full_screen_content(a_ad_data: Dictionary, error_data: Dictionary) -> void: + app_open_ad_failed_to_show_full_screen_content.emit(AdInfo.new(a_ad_data), AdError.new(error_data)) + + +func _on_app_open_ad_dismissed_full_screen_content(a_ad_data: Dictionary) -> void: + app_open_ad_dismissed_full_screen_content.emit(AdInfo.new(a_ad_data)) + + +func _on_native_ad_loaded(a_ad_data: Dictionary, a_response_info: Dictionary) -> void: + var __ad_info: AdInfo = AdInfo.new(a_ad_data) + var __response_info: ResponseInfo = ResponseInfo.new(a_response_info) + _active_native_ads.cache(__ad_info.get_ad_id(), __response_info) + native_ad_loaded.emit(__ad_info, __response_info) + + +func _on_native_ad_failed_to_load(ad_data: Dictionary, error: Dictionary) -> void: + emit_signal("native_ad_failed_to_load", AdInfo.new(ad_data), LoadAdError.new(error)) + + +func _on_native_ad_size_measured(ad_data: Dictionary) -> void: + emit_signal("native_ad_size_measured", AdInfo.new(ad_data)) + + +func _on_native_ad_impression(ad_data: Dictionary) -> void: + emit_signal("native_ad_impression", AdInfo.new(ad_data)) + + +func _on_native_ad_clicked(ad_data: Dictionary) -> void: + emit_signal("native_ad_clicked", AdInfo.new(ad_data)) + + +func _on_native_ad_swipe_gesture_clicked(ad_data: Dictionary) -> void: + emit_signal("native_ad_swipe_gesture_clicked", AdInfo.new(ad_data)) + + +func _on_native_ad_opened(ad_data: Dictionary) -> void: + emit_signal("native_ad_opened", AdInfo.new(ad_data)) + + +func _on_native_ad_closed(ad_data: Dictionary) -> void: + emit_signal("native_ad_closed", AdInfo.new(ad_data)) + + +func _on_consent_form_loaded() -> void: + consent_form_loaded.emit() + + +func _on_consent_form_dismissed(error_data: Dictionary) -> void: + consent_form_dismissed.emit(FormError.new(error_data)) + + +func _on_consent_form_failed_to_load(error_data: Dictionary) -> void: + consent_form_failed_to_load.emit(FormError.new(error_data)) + + +func _on_consent_info_updated() -> void: + consent_info_updated.emit() + + +func _on_consent_info_update_failed(error_data: Dictionary) -> void: + consent_info_update_failed.emit(FormError.new(error_data)) + + +func _on_tracking_authorization_granted() -> void: + tracking_authorization_granted.emit() + + +func _on_tracking_authorization_denied() -> void: + tracking_authorization_denied.emit() diff --git a/addons/AdmobPlugin/Admob.gd.uid b/addons/AdmobPlugin/Admob.gd.uid new file mode 100644 index 0000000..5d3a318 --- /dev/null +++ b/addons/AdmobPlugin/Admob.gd.uid @@ -0,0 +1 @@ +uid://dssu2uimllcg6 diff --git a/addons/AdmobPlugin/AdmobPlugin.gd b/addons/AdmobPlugin/AdmobPlugin.gd new file mode 100644 index 0000000..e0bca65 --- /dev/null +++ b/addons/AdmobPlugin/AdmobPlugin.gd @@ -0,0 +1,374 @@ +# +# © 2024-present https://github.com/cengiz-pz +# + +@tool +extends EditorPlugin + +const PLUGIN_NAME: String = "AdmobPlugin" +const ANDROID_DEPENDENCIES: Array = [ "androidx.appcompat:appcompat:1.7.1", "androidx.lifecycle:lifecycle-process:2.8.3", "com.google.android.gms:play-services-ads:24.9.0" ] +const IOS_PLATFORM_VERSION: String = "14.3" +const IOS_FRAMEWORKS: Array = [ "Foundation.framework", "AppTrackingTransparency.framework" ] +const IOS_EMBEDDED_FRAMEWORKS: Array = [ ] +const IOS_LINKER_FLAGS: Array = [ "-ObjC" ] +const IOS_BUNDLE_FILES: Array = [ ] +const SPM_DEPENDENCIES: Array = [ {&"url": "https://github.com/googleads/swift-package-manager-google-mobile-ads.git", &"version": "12.14.0", &"products": ["GoogleMobileAds"]} ] + +var android_export_plugin: AndroidExportPlugin +var ios_export_plugin: IosExportPlugin + + +func _enter_tree() -> void: + android_export_plugin = AndroidExportPlugin.new() + add_export_plugin(android_export_plugin) + ios_export_plugin = IosExportPlugin.new() + add_export_plugin(ios_export_plugin) + + +func _exit_tree() -> void: + remove_export_plugin(android_export_plugin) + android_export_plugin = null + remove_export_plugin(ios_export_plugin) + ios_export_plugin = null + + +class AndroidExportPlugin extends EditorExportPlugin: + const APP_ID_META_TAG = """ + +""" + + var _plugin_name = PLUGIN_NAME + var _export_config: AdmobAndroidExportConfig + + + func _supports_platform(platform: EditorExportPlatform) -> bool: + return platform is EditorExportPlatformAndroid + + + func _get_android_libraries(platform: EditorExportPlatform, debug: bool) -> PackedStringArray: + if debug: + return PackedStringArray(["%s/bin/debug/%s-debug.aar" % [_plugin_name, _plugin_name]]) + else: + return PackedStringArray(["%s/bin/release/%s-release.aar" % [_plugin_name, _plugin_name]]) + + + func _get_name() -> String: + return _plugin_name + + + func _export_begin(features: PackedStringArray, is_debug: bool, path: String, flags: int) -> void: + if _supports_platform(get_export_platform()): + _export_config = AdmobAndroidExportConfig.new() + if not _export_config.export_config_file_exists() or _export_config.load_export_config_from_file() != OK: + _export_config.load_export_config_from_node() + + + func _get_android_dependencies(platform: EditorExportPlatform, debug: bool) -> PackedStringArray: + var deps: PackedStringArray = PackedStringArray(ANDROID_DEPENDENCIES) + if _export_config and _export_config.enabled_mediation_networks.size() > 0: + for __network in _export_config.enabled_mediation_networks: + for __dependency in __network.android_dependencies: + deps.append(__dependency) + + GmpLogger.log_info("Android dependencies: %s" % str(deps)) + + return deps + + + func _get_android_dependencies_maven_repos(platform: EditorExportPlatform, debug: bool) -> PackedStringArray: + var __custom_repos: PackedStringArray = [] + + if _export_config and _export_config.enabled_mediation_networks.size() > 0: + for network in _export_config.enabled_mediation_networks: + if network.android_custom_maven_repo and not network.android_custom_maven_repo.is_empty(): + __custom_repos.append(network.android_custom_maven_repo) + GmpLogger.log_info( + "Added custom Maven repo for %s mediation: %s" % + [network.tag, network.android_custom_maven_repo], + ) + + return __custom_repos + + + func _get_android_manifest_application_element_contents(platform: EditorExportPlatform, debug: bool) -> String: + var __contents: String + + if _export_config: + __contents = APP_ID_META_TAG % (_export_config.real_application_id if _export_config.is_real \ + else _export_config.debug_application_id ) + else: + GmpLogger.log_warn("Export config not found for %s!" % _plugin_name) + __contents = "" + + return __contents + + +class IosExportPlugin extends EditorExportPlugin: + const NS_APP_TRANSPORT_SECURITY: String = """ +NSAppTransportSecurity + + NSAllowsArbitraryLoads + + NSAllowsArbitraryLoadsInWebContent + + +""" + + var _plugin_name = PLUGIN_NAME + var _spm_dependencies = [] + var _export_config: AdmobIosExportConfig + var _export_path: String + + + func _supports_platform(platform: EditorExportPlatform) -> bool: + return platform is EditorExportPlatformIOS + + + func _get_name() -> String: + return _plugin_name + + + func _export_begin(features: PackedStringArray, is_debug: bool, path: String, flags: int) -> void: + if _supports_platform(get_export_platform()): + _export_path = path.simplify_path() + _export_config = AdmobIosExportConfig.new() + if not _export_config.export_config_file_exists() or _export_config.load_export_config_from_file() != OK: + _export_config.load_export_config_from_node() + + add_apple_embedded_platform_plist_content("GADApplicationIdentifier") + add_apple_embedded_platform_plist_content( + "\t%s" % (_export_config.real_application_id + if _export_config.is_real else _export_config.debug_application_id ), + ) + + if _export_config.att_enabled and _export_config.att_text and not _export_config.att_text.is_empty(): + add_apple_embedded_platform_plist_content("NSUserTrackingUsageDescription") + add_apple_embedded_platform_plist_content("%s" % _export_config.att_text) + + add_apple_embedded_platform_plist_content( + MediationNetwork.generate_sk_ad_network_plist( + _export_config.enabled_mediation_networks, + ), + ) + + add_apple_embedded_platform_plist_content(NS_APP_TRANSPORT_SECURITY) + + for __framework in IOS_FRAMEWORKS: + add_apple_embedded_platform_framework(__framework) + + for __framework in IOS_EMBEDDED_FRAMEWORKS: + add_apple_embedded_platform_embedded_framework(__framework) + + for __flag in IOS_LINKER_FLAGS: + add_apple_embedded_platform_linker_flags(__flag) + + for __bundle_file in IOS_BUNDLE_FILES: + add_apple_embedded_platform_bundle_file(__bundle_file) + + for __spm_dep in SPM_DEPENDENCIES: + _spm_dependencies.append(SpmDependency.new(__spm_dep)) + + + func _end_generate_apple_embedded_project(path: String, will_build_archive: bool) -> void: + GmpLogger.log_info("Apple export project generated at: %s. Will build archive: %s" + % [path, str(will_build_archive)]) + + if _supports_platform(get_export_platform()): + _spm_dependencies.append_array(_get_extra_dependencies()) + + if _spm_dependencies.is_empty(): + GmpLogger.log_info("No SPM dependencies to install. Skipping.") + else: + GmpLogger.log_info("Installing %d SPM dependencies." % _spm_dependencies.size()) + _install_dependencies(path.get_base_dir(), path.get_file().get_basename()) + + + func _get_extra_dependencies() -> Array[SpmDependency]: + var __extra_dependencies:= [] as Array[SpmDependency] + + __extra_dependencies.append_array( + MediationNetwork.get_spm_dependencies(_export_config.enabled_mediation_networks)) + + return __extra_dependencies + + + func _install_dependencies(a_base_dir: String, a_project_name: String) -> void: + var __project_file_name:= "%s.xcodeproj" % a_project_name + var __project_file_path:= a_base_dir.path_join(__project_file_name) + if not DirAccess.dir_exists_absolute(__project_file_path): + GmpLogger.log_error("Xcode project '%s' does not exist! Can't install SPM dependencies." + % __project_file_path) + return + + var __script_name = "add_dependency.rb" + var __add_dependency_script_path = a_base_dir.path_join(__script_name) + var __result = _generate_add_dependency_script(__add_dependency_script_path) + if __result != Error.OK: + GmpLogger.log_error("Failed to generate '%s' script with error %d!" % [__script_name, __result]) + return + + GmpLogger.log_info("Adding SPM dependencies to %s..." % __project_file_path) + + for __spm_dep: SpmDependency in _spm_dependencies: + for __spm_dep_product: String in __spm_dep.get_products(): + var exec_output: Array = [] + var exec_code = OS.execute("ruby", [ + __add_dependency_script_path, + __project_file_path, + __spm_dep.get_url(), + __spm_dep.get_version(), + __spm_dep_product, + ], exec_output, true, false) + + if exec_code == 0: + GmpLogger.log_info("Product %s for SPM dependency %s added successfully!" + % [__spm_dep_product, __spm_dep.format_to_string()]) + for line in exec_output: + GmpLogger.log_info("SPM: %s" % line) + else: + GmpLogger.log_info("Failed to add product %s for SPM dependency %s !" + % [__spm_dep_product, __spm_dep.format_to_string()]) + for line in exec_output: + GmpLogger.log_error("SPM: %s" % line) + + GmpLogger.log_info("Resolving SPM dependencies...") + + __script_name = "resolve_dependencies.sh" + var __resolve_dependencies_script_path = a_base_dir.path_join(__script_name) + __result = _generate_resolve_dependencies_script(__resolve_dependencies_script_path, a_base_dir, a_project_name) + if __result != Error.OK: + GmpLogger.log_error("Failed to generate '%s' script with error %d!" % [__script_name, __result]) + return + + var exec_output: Array = [] + var exec_code = OS.execute(__resolve_dependencies_script_path, [], exec_output, true, false) + + if exec_code == 0: + for line in exec_output: + GmpLogger.log_info("SPM: %s" % line) + GmpLogger.log_info("Resolved dependencies successfully!") + else: + for line in exec_output: + GmpLogger.log_error("SPM: %s" % line) + GmpLogger.log_info("Failed to resolve dependencies! Try manually in Xcode.") + + + const ADD_DEPENDENCY_RUBY_SCRIPT = """ +require 'xcodeproj' + +project_path = ARGV[0] +url = ARGV[1].strip +version = ARGV[2].strip +product_name = ARGV[3].strip + +unless File.exist?(project_path) + puts "Error: Xcode project not found at #{project_path}" + exit 1 +end + +if url.empty? || version.empty? || product_name.empty? + puts "Error: url, version, and product_name must all be non-empty." + exit 1 +end + +begin + project = Xcodeproj::Project.open(project_path) + target = project.targets.first + + if target.nil? + puts "Error: No targets found in the Xcode project." + exit 1 + end + + existing_dep = target.package_product_dependencies.find do |dep| + dep.product_name == product_name + end + + if existing_dep + puts "Warning: Product dependency '#{product_name}' already exists in the project. Skipping add.\n\n" + else + # Reuse an existing package reference for the same URL, or create a new one + pkg = project.root_object.package_references.find do |p| + p.repositoryURL == url + end + + if pkg + puts "Reusing existing package reference for '#{url}'." + else + pkg = project.new(Xcodeproj::Project::Object::XCRemoteSwiftPackageReference) + pkg.repositoryURL = url + pkg.requirement = { + 'kind' => 'upToNextMajorVersion', + 'minimumVersion' => version + } + project.root_object.package_references << pkg + end + + # Create the product dependency and link it to the shared package reference + ref = project.new(Xcodeproj::Project::Object::XCSwiftPackageProductDependency) + ref.product_name = product_name + ref.package = pkg + target.package_product_dependencies << ref + + puts "Successfully added SPM dependency '#{product_name}' " \ + "(#{url} @ #{version}) to #{File.basename(project_path)}\n\n" + end + + project.save + +rescue => e + puts "An error occurred: #{e.message}\n\n" + exit 1 +end +""" + func _generate_add_dependency_script(a_script_path: String) -> Error: + var __result = Error.OK + + var __script_content = ADD_DEPENDENCY_RUBY_SCRIPT + + __result = _create_script(a_script_path, __script_content) + + return __result + + + const RESOLVE_DEPENDENCIES_BASH_SCRIPT = """ +#!/bin/bash +set -e # Exit on error + +xcodebuild -resolvePackageDependencies \ + -project "%s.xcodeproj" \ + -scheme "%s" +""" + func _generate_resolve_dependencies_script(a_script_path: String, a_base_dir: String, + a_project_name: String) -> Error: + var __result: Error = Error.OK + + var __script_content = RESOLVE_DEPENDENCIES_BASH_SCRIPT \ + % [ ProjectSettings.globalize_path(a_base_dir.path_join(a_project_name)), a_project_name ] + + __result = _create_script(a_script_path, __script_content) + + return __result + + + func _create_script(a_script_path: String, a_script_content: String) -> Error: + var __result: Error = Error.OK + + var __script_file = FileAccess.open(a_script_path, FileAccess.WRITE) + if __script_file: + __script_file.store_string(a_script_content) + __script_file.close() + else: + __result = Error.ERR_FILE_CANT_WRITE + + var chmod_output: Array = [] + var chmod_code = OS.execute("chmod", ["+x", a_script_path], chmod_output, true, false) + if chmod_code != 0: + GmpLogger.log_error("Failed to chmod %s script: %s" + % [a_script_path, (chmod_output if chmod_output.size() > 0 else "Unknown error")]) + __result = Error.ERR_FILE_NO_PERMISSION + + return __result diff --git a/addons/AdmobPlugin/AdmobPlugin.gd.uid b/addons/AdmobPlugin/AdmobPlugin.gd.uid new file mode 100644 index 0000000..06fc214 --- /dev/null +++ b/addons/AdmobPlugin/AdmobPlugin.gd.uid @@ -0,0 +1 @@ +uid://ombwj85k4mvh diff --git a/addons/AdmobPlugin/InlineAdaptiveBanner.gd b/addons/AdmobPlugin/InlineAdaptiveBanner.gd new file mode 100644 index 0000000..590b76b --- /dev/null +++ b/addons/AdmobPlugin/InlineAdaptiveBanner.gd @@ -0,0 +1,350 @@ +# +# © 2024-present https://github.com/cengiz-pz +# + +@icon("icon.png") +class_name InlineAdaptiveBanner +extends Control + +const MINIMUM_VALID_WIDTH: float = 50.0 +const MINIMUM_CLIP_THRESHOLD: float = 0.05 +const MAXIMUM_CLIP_THRESHOLD: float = 1.0 +const MINIMUM_RESIZE_THRESHOLD: float = 10.0 +const MAXIMUM_RESIZE_THRESHOLD: float = 500.0 + +## Maximum pixel height of the requested ad. If set to -1, height will be automatically determined. +@export var max_ad_height: int = -1 + +## Minimum portion of the banner’s area (as a percentage) that must be clipped before the banner is considered hidden. +@export_range(MINIMUM_CLIP_THRESHOLD, MAXIMUM_CLIP_THRESHOLD, 0.05) var clip_threshold: float = 0.1: + set(a_value): + clip_threshold = clampf(a_value, MINIMUM_CLIP_THRESHOLD, MAXIMUM_CLIP_THRESHOLD) + +## Width-change threshold (in pixels) that triggers a banner reload when the ad container is resized by this amount +## or more. +@export_range(MINIMUM_RESIZE_THRESHOLD, MAXIMUM_RESIZE_THRESHOLD, 5.0) var resize_threshold: float = 50.0: + set(a_value): + resize_threshold = clampf(a_value, MINIMUM_RESIZE_THRESHOLD, MAXIMUM_RESIZE_THRESHOLD) + +## Path to Admob node. +@export_node_path("Admob") var admob_path: NodePath + +## Whether the inline-adaptive banner ad should be automatically loaded. +@export var auto_load: bool = false + +@export_category("Ad Unit Identifier") +## Use ad unit identifier value defined in the Admob node. +@export var inherit_from_admob_node: bool = false + +@export_group("Android", "ad_unit_id_android") +## Ad unit ID of the inline adaptive banner ad to be loaded on the Android platform. +@export var ad_unit_id_android_debug: String = "ca-app-pub-3940256099942544/2014213617" + +## Ad unit ID of the inline adaptive banner ad to be loaded on the Android platform. +@export var ad_unit_id_android_real: String = "" + +@export_group("iOS", "ad_unit_id_ios") +## Ad unit ID of the inline adaptive banner ad to be loaded on the iOS platform. +@export var ad_unit_id_ios_debug: String = "ca-app-pub-3940256099942544/2934735716" + +## Ad unit ID of the inline adaptive banner ad to be loaded on the iOS platform. +@export var ad_unit_id_ios_real: String = "" + +var admob: Admob +var _ad_unit_id: String +var ad_id: String = "" + +var last_width: float + +var pending_load: bool = false +var is_position_dirty: bool = false +var is_banner_visible: bool = false +var do_ignore_resize: bool = false + + +func _enter_tree() -> void: + var __admob: Admob + if admob_path: + __admob = get_node(admob_path) + + if __admob: + initialize(__admob) + else: + push_warning("Admob node not found") + + +func _exit_tree() -> void: + if ad_id != "": + remove_ad() + + +func _ready() -> void: + if Engine.is_editor_hint(): + return + + size_flags_vertical = Control.SIZE_SHRINK_CENTER + + last_width = size.x + resized.connect(_on_resized) + + +func initialize(a_admob_node: Admob) -> void: + admob = a_admob_node + + visibility_changed.connect(_on_visibility_changed) + admob.banner_ad_loaded.connect(_on_banner_ad_loaded) + admob.banner_ad_failed_to_load.connect(_on_banner_ad_failed_to_load) + admob.banner_ad_refreshed.connect(_on_banner_ad_refreshed) + admob.banner_ad_size_measured.connect(_on_banner_ad_size_measured) + + if admob.is_initialization_completed: + if auto_load: + call_deferred("_deferred_load_ad") + else: + admob.initialization_completed.connect(_on_Admob_initialization_completed) + + if inherit_from_admob_node: + _ad_unit_id = admob._banner_id + else: + if OS.has_feature("ios"): + if admob.is_real: + _ad_unit_id = ad_unit_id_ios_real + else: + _ad_unit_id = ad_unit_id_ios_debug + else: + if admob.is_real: + _ad_unit_id = ad_unit_id_android_real + else: + _ad_unit_id = ad_unit_id_android_debug + + +func _deferred_load_ad() -> void: + load_ad() + _mark_position_dirty() + + +# -------------------------------------------------------------- +# AD MANIPULATION +# -------------------------------------------------------------- + + +func load_ad() -> void: + if _ad_unit_id == "": + return + + var req := LoadAdRequest.new() + req.set_ad_unit_id(_ad_unit_id) + req.set_ad_size(LoadAdRequest.RequestedAdSize.INLINE_ADAPTIVE) + + if custom_minimum_size.x < MINIMUM_VALID_WIDTH: + GmpLogger.log_warn("Can't load ad. Invalid inline adaptive banner width: %.1f" % custom_minimum_size.x) + return + + req.set_adaptive_width(round(custom_minimum_size.x)) + + if max_ad_height != -1: + req.set_adaptive_max_height(max_ad_height) + + req.set_ad_position(LoadAdRequest.AdPosition.CUSTOM) + + pending_load = true + admob.load_banner_ad(req) + + +func show_ad() -> void: + if not is_banner_visible: + admob.show_banner_ad(ad_id) + is_banner_visible = true + + +func hide_ad() -> void: + if is_banner_visible: + admob.hide_banner_ad(ad_id) + is_banner_visible = false + + +func move_ad(a_physical_position: Vector2) -> void: + admob.move_banner_ad(ad_id, a_physical_position.x, a_physical_position.y) + + +func remove_ad() -> void: + admob.remove_banner_ad(ad_id) + is_banner_visible = false + ad_id = "" + + +# -------------------------------------------------------------- +# POSITIONING +# -------------------------------------------------------------- + + +func _process(_delta: float) -> void: + if is_position_dirty: + _deferred_update_position() + is_position_dirty = false + else: + update_banner_position() + + +func _mark_position_dirty() -> void: + is_position_dirty = true + call_deferred("_deferred_update_position") + + +func _deferred_update_position() -> void: + update_banner_position() + + +func update_banner_position() -> void: + if ad_id == "": + return + + # Check basic tree visibility (Tabs, hidden parents) + if not is_visible_in_tree(): + hide_ad() + return + + # Smart clipping (visibility logic) + var is_clipped_out: bool = false + + if size.x > 1.0 and size.y > 1.0: + var virtual_rect := get_global_rect() + var original_area := virtual_rect.get_area() + var visible_region := virtual_rect + var parent = get_parent() + + # Walk up the tree to find ScrollContainers or other clippers + while parent is Control: + if parent.clip_contents: + var parent_rect: Rect2 = parent.get_global_rect() + var intersection := visible_region.intersection(parent_rect) + + # No intersection -> fully clipped (100%) + if intersection.get_area() <= 0: + is_clipped_out = true + break + + visible_region = intersection + parent = parent.get_parent() + + # Final check + if not get_viewport_rect().intersects(virtual_rect): + is_clipped_out = true + else: + # Calculate the percentage clipped + var visible_area := visible_region.get_area() + var clipped_ratio := 1.0 - (visible_area / original_area) + + if clipped_ratio >= clip_threshold: + is_clipped_out = true + + if is_clipped_out: + if is_banner_visible: + hide_ad() + else: + if not is_banner_visible: + show_ad() + + move_ad(_godot_to_physical(global_position)) + + +# -------------------------------------------------------------- +# SIGNAL HANDLERS +# -------------------------------------------------------------- + + +func _on_Admob_initialization_completed(_status: InitializationStatus) -> void: + if auto_load: + call_deferred("_deferred_load_ad") + + +func _on_resized() -> void: + if ad_id == "": + return + + if do_ignore_resize: + do_ignore_resize = false + return + + if abs(size.x - last_width) < resize_threshold: + return + + last_width = size.x + remove_ad() + call_deferred("_deferred_load_ad") + + _mark_position_dirty() + + +func _on_visibility_changed() -> void: + if ad_id != "": + if is_visible_in_tree(): + show_ad() + else: + hide_ad() + + +func _on_banner_ad_loaded(loaded_ad_info: AdInfo, _response: ResponseInfo) -> void: + if !pending_load: + return + + if loaded_ad_info.get_load_ad_request().get_ad_size() != LoadAdRequest.RequestedAdSize.INLINE_ADAPTIVE: + return + + if loaded_ad_info.get_load_ad_request().get_ad_unit_id().casecmp_to(_ad_unit_id) != 0: + return + + pending_load = false + ad_id = loaded_ad_info.get_ad_id() + + _apply_loaded_size(Vector2(loaded_ad_info.get_measured_width(), loaded_ad_info.get_measured_height())) + _mark_position_dirty() + + +func _on_banner_ad_failed_to_load(failed_ad_info: AdInfo, _err: LoadAdError) -> void: + if failed_ad_info.get_load_ad_request().get_ad_size() != LoadAdRequest.RequestedAdSize.INLINE_ADAPTIVE: + return + + if failed_ad_info.get_load_ad_request().get_ad_unit_id().casecmp_to(_ad_unit_id) != 0: + return + + if pending_load: + pending_load = false + + +func _on_banner_ad_size_measured(ad_info: AdInfo) -> void: + if ad_info.get_ad_id().casecmp_to(ad_id) == 0: + _apply_loaded_size(Vector2(ad_info.get_measured_width(), ad_info.get_measured_height())) + + +func _on_banner_ad_refreshed(refreshed_ad_info: AdInfo, _response: ResponseInfo) -> void: + if refreshed_ad_info.get_ad_id().casecmp_to(ad_id) == 0: + hide_ad() # Prevent ad from briefly displaying out of position + _apply_loaded_size(Vector2(refreshed_ad_info.get_measured_width(), refreshed_ad_info.get_measured_height())) + _mark_position_dirty() + + +# -------------------------------------------------------------- +# UTILITY +# -------------------------------------------------------------- + + +func _apply_loaded_size(a_size: Vector2) -> void: + do_ignore_resize = true + + custom_minimum_size = a_size + size = a_size + + +func _calculate_scale_factor() -> Vector2: + var screen_size: Vector2 = DisplayServer.window_get_size() as Vector2 + var virtual_size: Vector2 = get_viewport().get_visible_rect().size + return screen_size / virtual_size + + +func _physical_to_godot(a_physical_size: Vector2) -> Vector2: + return a_physical_size / _calculate_scale_factor() + + +func _godot_to_physical(a_godot_size: Vector2) -> Vector2: + return a_godot_size * _calculate_scale_factor() diff --git a/addons/AdmobPlugin/InlineAdaptiveBanner.gd.uid b/addons/AdmobPlugin/InlineAdaptiveBanner.gd.uid new file mode 100644 index 0000000..d29b134 --- /dev/null +++ b/addons/AdmobPlugin/InlineAdaptiveBanner.gd.uid @@ -0,0 +1 @@ +uid://dq4pb7uer2kjh diff --git a/addons/AdmobPlugin/android_export.cfg b/addons/AdmobPlugin/android_export.cfg new file mode 100644 index 0000000..bf84212 --- /dev/null +++ b/addons/AdmobPlugin/android_export.cfg @@ -0,0 +1,8 @@ +[General] +is_real=false + +[Debug] +app_id="ca-app-pub-3940256099942544~3347511713" + +[Release] +app_id="" diff --git a/addons/AdmobPlugin/bin/debug/AdmobPlugin-debug.aar b/addons/AdmobPlugin/bin/debug/AdmobPlugin-debug.aar new file mode 100644 index 0000000..4966d3d Binary files /dev/null and b/addons/AdmobPlugin/bin/debug/AdmobPlugin-debug.aar differ diff --git a/addons/AdmobPlugin/bin/release/AdmobPlugin-release.aar b/addons/AdmobPlugin/bin/release/AdmobPlugin-release.aar new file mode 100644 index 0000000..4bc1cd4 Binary files /dev/null and b/addons/AdmobPlugin/bin/release/AdmobPlugin-release.aar differ diff --git a/addons/AdmobPlugin/config/AdCache.gd b/addons/AdmobPlugin/config/AdCache.gd new file mode 100644 index 0000000..e2115f8 --- /dev/null +++ b/addons/AdmobPlugin/config/AdCache.gd @@ -0,0 +1,72 @@ +class_name AdCache +extends RefCounted + +var limit: int +var label: String +var remove_ad_callable: Callable + +var _ads: Dictionary[String, Object] +var _ad_queue: Array[String] + + +func _init(a_limit: int, a_label: String, a_callable: Callable) -> void: + self.limit = a_limit + self.label = a_label + self.remove_ad_callable = a_callable + + _ads = {} + _ad_queue = [] + + +func is_empty() -> bool: + return _ads.is_empty() + + +func has_key(a_key: String) -> bool: + return _ads.has(a_key) + + +func last_key() -> String: + return "" if _ad_queue.is_empty() else _ad_queue[0] + + +func all_keys() -> Array[String]: + return _ads.keys() + + +func cache(a_ad_id: String, a_ad_data: Object) -> void: + _ads[a_ad_id] = a_ad_data + _ad_queue.push_front(a_ad_id) + while _ads.size() > limit: + ( + GmpLogger + . log_warn( + ( + "%s: %s cache size (%d) has exceeded maximum (%d)" + % [ + Admob.PLUGIN_SINGLETON_NAME, + label, + _ads.size(), + limit, + ] + ), + ) + ) + remove_ad_callable.call(_erase_oldest()) + + +func erase(a_ad_id: String) -> void: + _ad_queue.erase(a_ad_id) + _ads.erase(a_ad_id) + + +func erase_last() -> String: + var __removed_ad_id: String = _ad_queue.pop_front() + _ads.erase(__removed_ad_id) + return __removed_ad_id + + +func _erase_oldest() -> String: + var __removed_ad_id: String = _ad_queue.pop_back() + _ads.erase(__removed_ad_id) + return __removed_ad_id diff --git a/addons/AdmobPlugin/config/AdCache.gd.uid b/addons/AdmobPlugin/config/AdCache.gd.uid new file mode 100644 index 0000000..73cd971 --- /dev/null +++ b/addons/AdmobPlugin/config/AdCache.gd.uid @@ -0,0 +1 @@ +uid://c8k2i5b2rg7qx diff --git a/addons/AdmobPlugin/config/AdmobAndroidExportConfig.gd b/addons/AdmobPlugin/config/AdmobAndroidExportConfig.gd new file mode 100644 index 0000000..2264ea5 --- /dev/null +++ b/addons/AdmobPlugin/config/AdmobAndroidExportConfig.gd @@ -0,0 +1,24 @@ +# +# © 2024-present https://github.com/cengiz-pz +# +class_name AdmobAndroidExportConfig +extends AdmobExportConfig + +const ANDROID_CONFIG_FILE_PATH: String = "res://addons/" + PLUGIN_NAME + "/android_export.cfg" + + +func get_config_file_path() -> String: + return ANDROID_CONFIG_FILE_PATH + + +func load_platform_specific_export_config_from_node(a_node: Admob) -> Error: + debug_application_id = a_node.android_debug_application_id + real_application_id = a_node.android_real_application_id + + return Error.OK + + +func print_loaded_config() -> void: + super.print_loaded_config() + GmpLogger.log_info("... debug_application_id: %s" % debug_application_id) + GmpLogger.log_info("... real_application_id: %s" % real_application_id) diff --git a/addons/AdmobPlugin/config/AdmobAndroidExportConfig.gd.uid b/addons/AdmobPlugin/config/AdmobAndroidExportConfig.gd.uid new file mode 100644 index 0000000..a8a8c07 --- /dev/null +++ b/addons/AdmobPlugin/config/AdmobAndroidExportConfig.gd.uid @@ -0,0 +1 @@ +uid://bd7jn56dvb8v4 diff --git a/addons/AdmobPlugin/config/AdmobExportConfig.gd b/addons/AdmobPlugin/config/AdmobExportConfig.gd new file mode 100644 index 0000000..75c1f67 --- /dev/null +++ b/addons/AdmobPlugin/config/AdmobExportConfig.gd @@ -0,0 +1,190 @@ +# +# © 2024-present https://github.com/cengiz-pz +# +class_name AdmobExportConfig +extends RefCounted + +const PLUGIN_NODE_TYPE_NAME = "Admob" +const PLUGIN_NAME: String = "AdmobPlugin" + +const CONFIG_FILE_SECTION_GENERAL: String = "General" +const CONFIG_FILE_SECTION_DEBUG: String = "Debug" +const CONFIG_FILE_SECTION_RELEASE: String = "Release" +const CONFIG_FILE_SECTION_MEDIATION: String = "Mediation" + +const CONFIG_FILE_KEY_IS_REAL: String = "is_real" +const CONFIG_FILE_KEY_APP_ID: String = "app_id" +const CONFIG_FILE_KEY_ENABLED_NETWORKS: String = "enabled_networks" + +var is_real: bool +var debug_application_id: String +var real_application_id: String +var enabled_mediation_networks: Array[MediationNetwork] = [] + + +func get_config_file_path() -> String: + return "" + + +func export_config_file_exists() -> bool: + return FileAccess.file_exists(get_config_file_path()) + + +func load_export_config_from_file() -> Error: + GmpLogger.log_info("Loading export config from file!") + + var __result = Error.OK + + var __config_file_path = get_config_file_path() + var __config_file = ConfigFile.new() + + var __load_result = __config_file.load(__config_file_path) + if __load_result == Error.OK: + is_real = __config_file.get_value(CONFIG_FILE_SECTION_GENERAL, CONFIG_FILE_KEY_IS_REAL) + debug_application_id = __config_file.get_value(CONFIG_FILE_SECTION_DEBUG, CONFIG_FILE_KEY_APP_ID) + real_application_id = __config_file.get_value(CONFIG_FILE_SECTION_RELEASE, CONFIG_FILE_KEY_APP_ID) + + if __config_file.has_section(CONFIG_FILE_SECTION_MEDIATION): + if __config_file.has_section_key(CONFIG_FILE_SECTION_MEDIATION, CONFIG_FILE_KEY_ENABLED_NETWORKS): + var __network_array := ( + Array( + __config_file.get_value(CONFIG_FILE_SECTION_MEDIATION, CONFIG_FILE_KEY_ENABLED_NETWORKS, []), + TYPE_STRING, + &"", + null, + ) + as Array[String] + ) + + for __network in __network_array: + if MediationNetwork.is_valid_tag(__network): + enabled_mediation_networks.append(MediationNetwork.get_by_tag(__network)) + else: + GmpLogger.log_error("Invalid network tag '%s' in file %s!" % [__network, __config_file_path]) + else: + ( + GmpLogger + . log_error( + ( + "Missing key %s in section %s of %s!" + % [CONFIG_FILE_KEY_ENABLED_NETWORKS, CONFIG_FILE_SECTION_MEDIATION, __config_file_path] + ), + ) + ) + + if is_real == null or debug_application_id == null or real_application_id == null: + __result = Error.ERR_INVALID_DATA + else: + __result = load_platform_specific_export_config_from_file(__config_file) + + if __result != Error.OK: + GmpLogger.log_error("Invalid export config file %s!" % __config_file_path) + else: + __result = Error.ERR_CANT_OPEN + GmpLogger.log_error("Failed to open export config file %s!" % __config_file_path) + + if __result == Error.OK: + print_loaded_config() + + return __result + + +func load_platform_specific_export_config_from_file(a_config_file: ConfigFile) -> Error: + return Error.OK + + +func load_export_config_from_node() -> Error: + GmpLogger.log_info("Loading export config from node!") + + var __result = Error.OK + + var __admob_node: Admob + + GmpLogger.log_info("Searching edited scene for %s node..." % PLUGIN_NODE_TYPE_NAME) + var __edited_scene_root: Node = EditorInterface.get_edited_scene_root() + if __edited_scene_root: + __admob_node = get_plugin_node(__edited_scene_root) + else: + GmpLogger.log_info("No edited scene found") + + if not __admob_node: + GmpLogger.log_info("Searching main scene for %s node..." % PLUGIN_NODE_TYPE_NAME) + var __main_scene_path: String = ProjectSettings.get_setting("application/run/main_scene") + if __main_scene_path and __main_scene_path != "": + var __packed_main: PackedScene = load(__main_scene_path) + if __packed_main: + var __main_scene = __packed_main.instantiate() + __admob_node = get_plugin_node(__main_scene) + else: + GmpLogger.log_info("Main scene path not defined in the project settings") + + if not __admob_node: + GmpLogger.log_info("Searching all project scenes for %s node..." % PLUGIN_NODE_TYPE_NAME) + + var collect_scene_paths: Callable = func( + a_dir_path: String, + a_collected_paths: Array[String], + a_recursive_func: Callable, + ) -> void: + for __file in DirAccess.get_files_at(a_dir_path): + if __file.ends_with(".tscn") or __file.ends_with(".scn"): + a_collected_paths.append(a_dir_path.path_join(__file)) + + for __dir in DirAccess.get_directories_at(a_dir_path): + a_recursive_func.call(a_dir_path.path_join(__dir), a_collected_paths, a_recursive_func) + + var __scene_paths: Array[String] = [] + collect_scene_paths.call("res://", __scene_paths, collect_scene_paths) + + for __scene_path in __scene_paths: + var packed: PackedScene = load(__scene_path) + if packed: + var __instance = packed.instantiate() + var __found_node: Admob = get_plugin_node(__instance) + if __found_node: + GmpLogger.log_info("Found %s node in scene: %s" % [PLUGIN_NODE_TYPE_NAME, __scene_path]) + __admob_node = __found_node + break + + # Load configuration if node found + if __admob_node: + is_real = __admob_node.is_real + enabled_mediation_networks = MediationNetwork.get_all_enabled(__admob_node.enabled_networks) + + __result = load_platform_specific_export_config_from_node(__admob_node) + if __result == Error.OK: + print_loaded_config() + else: + GmpLogger.log_error("Invalid %s node for %s!" % [PLUGIN_NODE_TYPE_NAME, PLUGIN_NAME]) + else: + GmpLogger.log_error("%s failed to find %s node!" % [PLUGIN_NAME, PLUGIN_NODE_TYPE_NAME]) + __result = Error.ERR_UNCONFIGURED + + return __result + + +func load_platform_specific_export_config_from_node(a_node: Admob) -> Error: + return Error.OK + + +func print_loaded_config() -> void: + GmpLogger.log_info("Loaded export configuration settings:") + GmpLogger.log_info(" is_real: %s" % ("true" if is_real else "false")) + GmpLogger.log_info( + " enabled_mediation_networks: %s" % MediationNetwork.generate_tag_list(enabled_mediation_networks) + ) + + +func get_plugin_node(a_node: Node) -> Admob: + var __result: Admob + + if a_node is Admob: + __result = a_node + elif a_node.get_child_count() > 0: + for __child in a_node.get_children(): + var __child_result = get_plugin_node(__child) + if __child_result is Admob: + __result = __child_result + break + + return __result diff --git a/addons/AdmobPlugin/config/AdmobExportConfig.gd.uid b/addons/AdmobPlugin/config/AdmobExportConfig.gd.uid new file mode 100644 index 0000000..2e9ef91 --- /dev/null +++ b/addons/AdmobPlugin/config/AdmobExportConfig.gd.uid @@ -0,0 +1 @@ +uid://byw0f6m12b2s2 diff --git a/addons/AdmobPlugin/config/AdmobIosExportConfig.gd b/addons/AdmobPlugin/config/AdmobIosExportConfig.gd new file mode 100644 index 0000000..722a637 --- /dev/null +++ b/addons/AdmobPlugin/config/AdmobIosExportConfig.gd @@ -0,0 +1,43 @@ +# +# © 2024-present https://github.com/cengiz-pz +# +class_name AdmobIosExportConfig +extends AdmobExportConfig + +const IOS_CONFIG_FILE_PATH: String = "res://addons/" + PLUGIN_NAME + "/ios_export.cfg" + +const CONFIG_FILE_SECTION_ATT: String = "ATT" + +const CONFIG_FILE_KEY_ATT_ENABLED: String = "att_enabled" +const CONFIG_FILE_KEY_ATT_TEXT: String = "att_text" + +var att_enabled: bool +var att_text: String + + +func get_config_file_path() -> String: + return IOS_CONFIG_FILE_PATH + + +func load_platform_specific_export_config_from_file(a_config_file: ConfigFile) -> Error: + att_enabled = a_config_file.get_value(CONFIG_FILE_SECTION_ATT, CONFIG_FILE_KEY_ATT_ENABLED, false) + att_text = a_config_file.get_value(CONFIG_FILE_SECTION_ATT, CONFIG_FILE_KEY_ATT_TEXT) + + return Error.OK + + +func load_platform_specific_export_config_from_node(a_node: Admob) -> Error: + debug_application_id = a_node.ios_debug_application_id + real_application_id = a_node.ios_real_application_id + att_enabled = a_node.att_enabled + att_text = a_node.att_text + + return Error.OK + + +func print_loaded_config() -> void: + super.print_loaded_config() + GmpLogger.log_info("... debug_application_id: %s" % debug_application_id) + GmpLogger.log_info("... real_application_id: %s" % real_application_id) + GmpLogger.log_info("... att_enabled: %s" % ("true" if att_enabled else "false")) + GmpLogger.log_info("... att_text: %s" % att_text) diff --git a/addons/AdmobPlugin/config/AdmobIosExportConfig.gd.uid b/addons/AdmobPlugin/config/AdmobIosExportConfig.gd.uid new file mode 100644 index 0000000..925db1a --- /dev/null +++ b/addons/AdmobPlugin/config/AdmobIosExportConfig.gd.uid @@ -0,0 +1 @@ +uid://hamb6dlvl4bc diff --git a/addons/AdmobPlugin/icon.png b/addons/AdmobPlugin/icon.png new file mode 100644 index 0000000..33826e8 Binary files /dev/null and b/addons/AdmobPlugin/icon.png differ diff --git a/addons/AdmobPlugin/icon.png.import b/addons/AdmobPlugin/icon.png.import new file mode 100644 index 0000000..23f7e2d --- /dev/null +++ b/addons/AdmobPlugin/icon.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://cmkpgeo3anvuw" +path="res://.godot/imported/icon.png-928f79d906c5a286de384358dfb1ba26.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/AdmobPlugin/icon.png" +dest_files=["res://.godot/imported/icon.png-928f79d906c5a286de384358dfb1ba26.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/addons/AdmobPlugin/ios_export.cfg b/addons/AdmobPlugin/ios_export.cfg new file mode 100644 index 0000000..dc5999e --- /dev/null +++ b/addons/AdmobPlugin/ios_export.cfg @@ -0,0 +1,12 @@ +[General] +is_real=false + +[Debug] +app_id="ca-app-pub-3940256099942544~1458002511" + +[Release] +app_id="" + +[ATT] +att_enabled=false +att_text="Deine Erlaubnis hilft uns, Werbung zu messen und das Spiel zu verbessern." diff --git a/addons/AdmobPlugin/model/AdError.gd b/addons/AdmobPlugin/model/AdError.gd new file mode 100644 index 0000000..81f65e6 --- /dev/null +++ b/addons/AdmobPlugin/model/AdError.gd @@ -0,0 +1,32 @@ +# +# © 2024-present https://github.com/cengiz-pz +# +class_name AdError +extends RefCounted + +const CODE_PROPERTY := &"code" +const DOMAIN_PROPERTY := &"domain" +const MESSAGE_PROPERTY := &"message" +const CAUSE_PROPERTY := &"cause" + +var _data: Dictionary + + +func _init(a_data: Dictionary): + _data = a_data + + +func get_code() -> int: + return _data[CODE_PROPERTY] + + +func get_domain() -> String: + return _data[DOMAIN_PROPERTY] + + +func get_message() -> String: + return _data[MESSAGE_PROPERTY] + + +func get_cause() -> AdError: + return AdError.new(_data[CAUSE_PROPERTY]) if _data.has(CAUSE_PROPERTY) else null diff --git a/addons/AdmobPlugin/model/AdError.gd.uid b/addons/AdmobPlugin/model/AdError.gd.uid new file mode 100644 index 0000000..eb1cbf1 --- /dev/null +++ b/addons/AdmobPlugin/model/AdError.gd.uid @@ -0,0 +1 @@ +uid://0att8tjqjm8t diff --git a/addons/AdmobPlugin/model/AdInfo.gd b/addons/AdmobPlugin/model/AdInfo.gd new file mode 100644 index 0000000..03fe71d --- /dev/null +++ b/addons/AdmobPlugin/model/AdInfo.gd @@ -0,0 +1,37 @@ +# +# © 2024-present https://github.com/cengiz-pz +# +class_name AdInfo +extends RefCounted + +const AD_ID_PROPERTY := &"ad_id" +const MEASURED_WIDTH_PROPERTY := &"measured_width" +const MEASURED_HEIGHT_PROPERTY := &"measured_height" +const IS_COLLAPSIBLE_PROPERTY := &"is_collapsible" +const LOAD_AD_REQUEST_PROPERTY := &"load_ad_request" + +var _data: Dictionary + + +func _init(a_data: Dictionary): + _data = a_data + + +func get_ad_id() -> String: + return _data[AD_ID_PROPERTY] if _data.has(AD_ID_PROPERTY) else "" + + +func get_measured_width() -> int: + return _data[MEASURED_WIDTH_PROPERTY] if _data.has(MEASURED_WIDTH_PROPERTY) else 0 + + +func get_measured_height() -> int: + return _data[MEASURED_HEIGHT_PROPERTY] if _data.has(MEASURED_HEIGHT_PROPERTY) else 0 + + +func get_is_collapsible() -> bool: + return _data[IS_COLLAPSIBLE_PROPERTY] if _data.has(IS_COLLAPSIBLE_PROPERTY) else false + + +func get_load_ad_request() -> LoadAdRequest: + return LoadAdRequest.new(_data[LOAD_AD_REQUEST_PROPERTY]) if _data.has(LOAD_AD_REQUEST_PROPERTY) else null diff --git a/addons/AdmobPlugin/model/AdInfo.gd.uid b/addons/AdmobPlugin/model/AdInfo.gd.uid new file mode 100644 index 0000000..cc8c777 --- /dev/null +++ b/addons/AdmobPlugin/model/AdInfo.gd.uid @@ -0,0 +1 @@ +uid://pm7kx5541j4 diff --git a/addons/AdmobPlugin/model/AdSize.gd b/addons/AdmobPlugin/model/AdSize.gd new file mode 100644 index 0000000..138da6c --- /dev/null +++ b/addons/AdmobPlugin/model/AdSize.gd @@ -0,0 +1,22 @@ +# +# © 2024-present https://github.com/cengiz-pz +# +class_name AdSize +extends RefCounted + +const WIDTH_PROPERTY := &"width" +const HEIGHT_PROPERTY := &"height" + +var _data: Dictionary + + +func _init(a_data: Dictionary): + _data = a_data if a_data else {} + + +func get_width() -> int: + return _data[WIDTH_PROPERTY] if _data.has(WIDTH_PROPERTY) else 0 + + +func get_height() -> int: + return _data[HEIGHT_PROPERTY] if _data.has(HEIGHT_PROPERTY) else 0 diff --git a/addons/AdmobPlugin/model/AdSize.gd.uid b/addons/AdmobPlugin/model/AdSize.gd.uid new file mode 100644 index 0000000..addc06c --- /dev/null +++ b/addons/AdmobPlugin/model/AdSize.gd.uid @@ -0,0 +1 @@ +uid://1y453yingj1w diff --git a/addons/AdmobPlugin/model/AdapterResponseInfo.gd b/addons/AdmobPlugin/model/AdapterResponseInfo.gd new file mode 100644 index 0000000..9db2dcf --- /dev/null +++ b/addons/AdmobPlugin/model/AdapterResponseInfo.gd @@ -0,0 +1,52 @@ +# +# © 2024-present https://github.com/cengiz-pz +# +class_name AdapterResponseInfo +extends RefCounted + +const AD_ERROR_PROPERTY := &"ad_error" +const AD_SOURCE_ID_PROPERTY := &"ad_source_id" +const AD_SOURCE_INSTANCE_ID_PROPERTY := &"ad_source_instance_id" +const AD_SOURCE_INSTANCE_NAME_PROPERTY := &"ad_source_instance_name" +const AD_SOURCE_NAME_PROPERTY := &"ad_source_name" +const ADAPTER_CLASS_NAME_PROPERTY := &"adapter_class_name" +const NETWORK_TAG_PROPERTY := &"network_tag" +const LATENCY_PROPERTY := &"latency" + +var _data: Dictionary + + +func _init(a_data: Dictionary): + _data = a_data + + +func get_ad_error() -> AdError: + return AdError.new(_data[AD_ERROR_PROPERTY]) if _data.has(AD_ERROR_PROPERTY) else null + + +func get_ad_source_id() -> String: + return _data[AD_SOURCE_ID_PROPERTY] + + +func get_ad_source_instance_id() -> String: + return _data[AD_SOURCE_INSTANCE_ID_PROPERTY] + + +func get_ad_source_instance_name() -> String: + return _data[AD_SOURCE_INSTANCE_NAME_PROPERTY] + + +func get_ad_source_name() -> String: + return _data[AD_SOURCE_NAME_PROPERTY] + + +func get_adapter_class_name() -> String: + return _data[ADAPTER_CLASS_NAME_PROPERTY] + + +func get_network_tag() -> String: + return _data[NETWORK_TAG_PROPERTY] + + +func get_latency() -> int: + return _data[LATENCY_PROPERTY] diff --git a/addons/AdmobPlugin/model/AdapterResponseInfo.gd.uid b/addons/AdmobPlugin/model/AdapterResponseInfo.gd.uid new file mode 100644 index 0000000..cd7e8b9 --- /dev/null +++ b/addons/AdmobPlugin/model/AdapterResponseInfo.gd.uid @@ -0,0 +1 @@ +uid://kwsy2dkniyn8 diff --git a/addons/AdmobPlugin/model/AdapterStatus.gd b/addons/AdmobPlugin/model/AdapterStatus.gd new file mode 100644 index 0000000..a78bcc1 --- /dev/null +++ b/addons/AdmobPlugin/model/AdapterStatus.gd @@ -0,0 +1,32 @@ +# +# © 2024-present https://github.com/cengiz-pz +# +class_name AdapterStatus +extends RefCounted + +const ADAPTER_CLASS_PROPERTY := &"adapter_class" +const LATENCY_PROPERTY := &"latency" +const INITIALIZATION_STATE_PROPERTY := &"initialization_state" +const DESCRIPTION_PROPERTY := &"description" + +var _data: Dictionary + + +func _init(a_data: Dictionary): + _data = a_data + + +func get_adapter_class() -> String: + return _data[ADAPTER_CLASS_PROPERTY] + + +func get_latency() -> int: + return _data[LATENCY_PROPERTY] + + +func get_initialization_state() -> String: + return _data[INITIALIZATION_STATE_PROPERTY] + + +func get_description() -> String: + return _data[DESCRIPTION_PROPERTY] diff --git a/addons/AdmobPlugin/model/AdapterStatus.gd.uid b/addons/AdmobPlugin/model/AdapterStatus.gd.uid new file mode 100644 index 0000000..b18e600 --- /dev/null +++ b/addons/AdmobPlugin/model/AdapterStatus.gd.uid @@ -0,0 +1 @@ +uid://scnfdeiyquod diff --git a/addons/AdmobPlugin/model/AdmobConfig.gd b/addons/AdmobPlugin/model/AdmobConfig.gd new file mode 100644 index 0000000..a374ad2 --- /dev/null +++ b/addons/AdmobPlugin/model/AdmobConfig.gd @@ -0,0 +1,105 @@ +# +# © 2024-present https://github.com/cengiz-pz +# +class_name AdmobConfig +extends RefCounted + +enum ContentRating { + G, ## G means "General Audiences" and is suitable for all ages, containing no objectionable material + ## (minimal violence, no nudity or sex scenes, and no drug use). + PG, ## PG stands for "Parental Guidance Suggested," meaning that some material may not be suitable for young + ## children (may include themes, language, or mild violence). + T, ## T means Teen, and indicates content suitable for ages 13 and older (can include violence, suggestive themes, + ## crude humor, simulated gambling, and/or infrequent strong language). + MA, ## MA stands for "Mature Audience", signifying that the material is intended for adults (may include graphic + ## violence, strong language, or explicit sexual activity). +} + +enum TagForChildDirectedTreatment { + UNSPECIFIED = -1, ## Means that you have not provided a specific instruction for your app's content to be treated + ## as child-directed for COPPA purposes. + FALSE = 0, ## Means you are indicating that your app's content is not child-directed for the purposes of the + ## Children's Online Privacy Protection Act (COPPA). + TRUE = 1, ## Tells ad networks to treat ad requests as if the content is directed towards children, which is + ## important for complying with regulations like COPPA. +} + +enum TagForUnderAgeOfConsent { + UNSPECIFIED = -1, ## Means you have not provided any information on whether the user is under the age of consent, + ## so the system does not apply any specific treatment for these users based on this tag alone. + FALSE = 0, ## Indicates that you do not want the specific ad request to receive the special handling suitable for + ## users under the age of consent in the EEA, the UK, and Switzerland. + TRUE = 1, ## Indicates that you want the ad request to be handled in a manner suitable for users under the age of + ## consent within the EEA, the UK, and Switzerland. +} + +enum PersonalizationState { + DEFAULT = 0, ## By default, the SDK will attempt to serve personalized ads based on the user's past behavior and + ## interests. + ENABLED = 1, ## Indicates that the Google Mobile Ads SDK is authorized to serve ads that are tailored to the user + ## based on past activity and collected data. + DISABLED = 2, ## Means that ad requests are set to serve non-personalized ads (NPA) only. +} + +const DATA_KEY_IS_REAL := &"is_real" +const DATA_KEY_MAX_AD_CONTENT_RATING := &"max_ad_content_rating" +const DATA_KEY_CHILD_DIRECTED_TREATMENT := &"tag_for_child_directed_treatment" +const DATA_KEY_UNDER_AGE_OF_CONSENT := &"tag_for_under_age_of_consent" +const DATA_KEY_FIRST_PARTY_ID_ENABLED := &"first_party_id_enabled" +const DATA_KEY_PERSONALIZATION_STATE := &"personalization_state" +const DATA_KEY_TEST_DEVICE_IDS := &"test_device_ids" + +var _data: Dictionary + + +func _init(): + _data = { + DATA_KEY_TEST_DEVICE_IDS: [], + } + + +func set_is_real(a_value: bool) -> AdmobConfig: + _data[DATA_KEY_IS_REAL] = a_value + return self + + +func set_max_ad_content_rating(a_value: ContentRating) -> AdmobConfig: + _data[DATA_KEY_MAX_AD_CONTENT_RATING] = ContentRating.keys()[a_value] + return self + + +func set_child_directed_treatment(a_value: TagForChildDirectedTreatment) -> AdmobConfig: + _data[DATA_KEY_CHILD_DIRECTED_TREATMENT] = a_value + return self + + +func set_under_age_of_consent(a_value: TagForUnderAgeOfConsent) -> AdmobConfig: + _data[DATA_KEY_UNDER_AGE_OF_CONSENT] = a_value + return self + + +func set_first_party_id_enabled(a_value: bool) -> AdmobConfig: + _data[DATA_KEY_FIRST_PARTY_ID_ENABLED] = a_value + return self + + +func set_personalization_state(a_value: PersonalizationState) -> AdmobConfig: + _data[DATA_KEY_PERSONALIZATION_STATE] = a_value + return self + + +func set_test_device_ids(a_value: Array) -> AdmobConfig: + if a_value == null: + _data[DATA_KEY_TEST_DEVICE_IDS] = [] + else: + _data[DATA_KEY_TEST_DEVICE_IDS] = a_value + return self + + +func add_test_device_id(a_value: String) -> AdmobConfig: + _data[DATA_KEY_TEST_DEVICE_IDS].append(a_value) + return self + + +func get_raw_data() -> Dictionary: + return _data diff --git a/addons/AdmobPlugin/model/AdmobConfig.gd.uid b/addons/AdmobPlugin/model/AdmobConfig.gd.uid new file mode 100644 index 0000000..d3d7e74 --- /dev/null +++ b/addons/AdmobPlugin/model/AdmobConfig.gd.uid @@ -0,0 +1 @@ +uid://ihpl6qb5lkw2 diff --git a/addons/AdmobPlugin/model/AdmobSettings.gd b/addons/AdmobPlugin/model/AdmobSettings.gd new file mode 100644 index 0000000..bad8a4f --- /dev/null +++ b/addons/AdmobPlugin/model/AdmobSettings.gd @@ -0,0 +1,46 @@ +# +# © 2026-present https://github.com/cengiz-pz +# +class_name AdmobSettings +extends RefCounted + +const DATA_KEY_AD_VOLUME := &"ad_volume" +const DATA_KEY_ADS_MUTED := &"ads_muted" +const DATA_KEY_APPLY_AT_STARTUP := &"apply_at_startup" + +var _data: Dictionary + + +func _init(a_data: Dictionary = {}) -> void: + _data = a_data + + +func set_ad_volume(a_value: float) -> AdmobSettings: + _data[DATA_KEY_AD_VOLUME] = a_value + return self + + +func get_ad_volume() -> float: + return _data[DATA_KEY_AD_VOLUME] + + +func set_ads_muted(a_value: bool) -> AdmobSettings: + _data[DATA_KEY_ADS_MUTED] = a_value + return self + + +func are_ads_muted() -> bool: + return _data[DATA_KEY_ADS_MUTED] + + +func set_apply_at_startup(a_value: bool) -> AdmobSettings: + _data[DATA_KEY_APPLY_AT_STARTUP] = a_value + return self + + +func get_apply_at_startup() -> bool: + return _data[DATA_KEY_APPLY_AT_STARTUP] + + +func get_raw_data() -> Dictionary: + return _data diff --git a/addons/AdmobPlugin/model/AdmobSettings.gd.uid b/addons/AdmobPlugin/model/AdmobSettings.gd.uid new file mode 100644 index 0000000..225e0f2 --- /dev/null +++ b/addons/AdmobPlugin/model/AdmobSettings.gd.uid @@ -0,0 +1 @@ +uid://u1bg67vtlhd8 diff --git a/addons/AdmobPlugin/model/ConsentRequestParameters.gd b/addons/AdmobPlugin/model/ConsentRequestParameters.gd new file mode 100644 index 0000000..b973928 --- /dev/null +++ b/addons/AdmobPlugin/model/ConsentRequestParameters.gd @@ -0,0 +1,63 @@ +# +# © 2024-present https://github.com/cengiz-pz +# +class_name ConsentRequestParameters +extends RefCounted + +enum DebugGeography { + NOT_SET = -1, ## Don't specify location. (Actual location will be used.) + DISABLED = 0, ## Use actual location. + EEA = 1, ## Use European Economic Area. + NOT_EEA = 2, ## Deprecated. + REGULATED_US_STATE = 3, ## Use a regulated state of USA. (ie. California) + OTHER = 4, ## Use any non-regulated location. +} + +const IS_REAL_PROPERTY := &"is_real" +const TAG_FOR_UNDER_AGE_OF_CONSENT_PROPERTY := &"tag_for_under_age_of_consent" +const DEBUG_GEOGRAPHY_PROPERTY := &"debug_geography" +const TEST_DEVICE_HASHED_IDS_PROPERTY := &"test_device_hashed_ids" + +var _data: Dictionary + + +func _init(): + _data = { + TEST_DEVICE_HASHED_IDS_PROPERTY: [], + } + + +func set_is_real(a_value: bool) -> ConsentRequestParameters: + _data[IS_REAL_PROPERTY] = a_value + + return self + + +func set_tag_for_under_age_of_consent(a_value: AdmobConfig.TagForUnderAgeOfConsent) -> ConsentRequestParameters: + match a_value: + AdmobConfig.TagForChildDirectedTreatment.TRUE: + _data[TAG_FOR_UNDER_AGE_OF_CONSENT_PROPERTY] = true + AdmobConfig.TagForChildDirectedTreatment.FALSE: + _data[TAG_FOR_UNDER_AGE_OF_CONSENT_PROPERTY] = false + + return self + + +func set_debug_geography(a_value: DebugGeography) -> ConsentRequestParameters: + _data[DEBUG_GEOGRAPHY_PROPERTY] = a_value + + return self + + +func add_test_device_hashed_id(a_value: String) -> ConsentRequestParameters: + _data[TEST_DEVICE_HASHED_IDS_PROPERTY].append(a_value) + + return self + + +func get_device_ids() -> Array: + return _data[TEST_DEVICE_HASHED_IDS_PROPERTY] + + +func get_raw_data() -> Dictionary: + return _data diff --git a/addons/AdmobPlugin/model/ConsentRequestParameters.gd.uid b/addons/AdmobPlugin/model/ConsentRequestParameters.gd.uid new file mode 100644 index 0000000..8b5de1b --- /dev/null +++ b/addons/AdmobPlugin/model/ConsentRequestParameters.gd.uid @@ -0,0 +1 @@ +uid://5nuqawjs80oi diff --git a/addons/AdmobPlugin/model/FormError.gd b/addons/AdmobPlugin/model/FormError.gd new file mode 100644 index 0000000..66b7a73 --- /dev/null +++ b/addons/AdmobPlugin/model/FormError.gd @@ -0,0 +1,22 @@ +# +# © 2024-present https://github.com/cengiz-pz +# +class_name FormError +extends RefCounted + +const CODE_PROPERTY := &"code" +const MESSAGE_PROPERTY := &"message" + +var _data: Dictionary + + +func _init(a_data: Dictionary): + _data = a_data if a_data else {} + + +func get_code() -> int: + return _data[CODE_PROPERTY] if _data.has(CODE_PROPERTY) else 0 + + +func get_message() -> String: + return _data[MESSAGE_PROPERTY] if _data.has(MESSAGE_PROPERTY) else "" diff --git a/addons/AdmobPlugin/model/FormError.gd.uid b/addons/AdmobPlugin/model/FormError.gd.uid new file mode 100644 index 0000000..8b684ad --- /dev/null +++ b/addons/AdmobPlugin/model/FormError.gd.uid @@ -0,0 +1 @@ +uid://f4ywykxwjpal diff --git a/addons/AdmobPlugin/model/InitializationStatus.gd b/addons/AdmobPlugin/model/InitializationStatus.gd new file mode 100644 index 0000000..20ac6bc --- /dev/null +++ b/addons/AdmobPlugin/model/InitializationStatus.gd @@ -0,0 +1,22 @@ +# +# © 2024-present https://github.com/cengiz-pz +# +class_name InitializationStatus +extends RefCounted + +var _data: Dictionary + + +func _init(a_data: Dictionary): + if a_data == null: + _data = {} + else: + _data = a_data + + +func get_network_tags() -> Array: + return _data.keys() + + +func get_adapter_status(a_network_tag: String) -> AdapterStatus: + return AdapterStatus.new(_data[a_network_tag]) if _data.has(a_network_tag) else null diff --git a/addons/AdmobPlugin/model/InitializationStatus.gd.uid b/addons/AdmobPlugin/model/InitializationStatus.gd.uid new file mode 100644 index 0000000..31b24f3 --- /dev/null +++ b/addons/AdmobPlugin/model/InitializationStatus.gd.uid @@ -0,0 +1 @@ +uid://b0oie3ngw68ha diff --git a/addons/AdmobPlugin/model/LoadAdError.gd b/addons/AdmobPlugin/model/LoadAdError.gd new file mode 100644 index 0000000..66b5126 --- /dev/null +++ b/addons/AdmobPlugin/model/LoadAdError.gd @@ -0,0 +1,15 @@ +# +# © 2024-present https://github.com/cengiz-pz +# +class_name LoadAdError +extends AdError + +const RESPONSE_INFO_PROPERTY := &"response_info" + + +func _init(a_data: Dictionary): + super(a_data) + + +func get_response_info() -> ResponseInfo: + return ResponseInfo.new(_data[RESPONSE_INFO_PROPERTY]) if _data.has(RESPONSE_INFO_PROPERTY) else null diff --git a/addons/AdmobPlugin/model/LoadAdError.gd.uid b/addons/AdmobPlugin/model/LoadAdError.gd.uid new file mode 100644 index 0000000..649321c --- /dev/null +++ b/addons/AdmobPlugin/model/LoadAdError.gd.uid @@ -0,0 +1 @@ +uid://ddnpjk4ksplb5 diff --git a/addons/AdmobPlugin/model/LoadAdRequest.gd b/addons/AdmobPlugin/model/LoadAdRequest.gd new file mode 100644 index 0000000..6016f00 --- /dev/null +++ b/addons/AdmobPlugin/model/LoadAdRequest.gd @@ -0,0 +1,332 @@ +# +# © 2024-present https://github.com/cengiz-pz +# +class_name LoadAdRequest +extends RefCounted + +enum AdPosition { + TOP, ## Ad will be anchored at the top of the screen. + BOTTOM, ## Ad will be anchored at the bottom of the screen. + LEFT, ## Ad will be anchored to the left of the screen. + RIGHT, ## Ad will be anchored to the right of the screen. + TOP_LEFT, ## Ad will be anchored at the top-left of the screen. + TOP_RIGHT, ## Ad will be anchored at the top-right of the screen. + BOTTOM_LEFT, ## Ad will be anchored at the bottom-left of the screen. + BOTTOM_RIGHT, ## Ad will be anchored at the bottom-right of the screen. + CENTER, ## Ad will be anchored at the center of the screen. + CUSTOM, ## Ad position will be set dynamically. +} + +enum RequestedAdSize { + BANNER, ## A standard ad size that refers to a rectangular ad unit, most commonly 320x50 density-independent pixels + ## (dp). + LARGE_BANNER, ## A fixed-size banner with dimensions of 320x100 density-independent pixels (dp). + MEDIUM_RECTANGLE, ## A standard 300x250 pixel ad, often referred to as the Interactive Advertising Bureau (IAB) + ## medium rectangle. + FULL_BANNER, ## An ad size constant for an Interactive Advertising Bureau (IAB) full-size banner, which has a fixed + ## size of 468x60 density-independent pixels (dp). + LEADERBOARD, ## A banner ad with dimensions of 728 pixels wide by 90 pixels tall. This is a standard display ad + ## size that is designed for tablet screens. + ADAPTIVE, ## An ad that automatically adjusts its width and height to fit the device's available screen space, + ## providing the best-performing banner size for that specific layout. + INLINE_ADAPTIVE, ## An ad that dynamically optimizes its height for the available inline content width, ensuring a + ## smoothly integrated banner that fits naturally within scrollable or in-feed layouts. + SKYSCRAPER, ## Deprecated + FLUID, ## Deprecated +} + +enum CollapsiblePosition { + DISABLED, ## The banner ad will not be collapsible. + TOP, ## The banner ad will be collapsible from bottom to top. + BOTTOM, ## The banner ad will be collapsible from top to bottom. +} + +## Controls which aspect ratios the SDK should prefer when requesting native ad media. +## Maps to NativeAdOptions.NATIVE_MEDIA_ASPECT_RATIO_* constants. +enum NativeMediaAspectRatio { + UNKNOWN = 0, ## No preference for aspect ratio (SDK default). + ANY = 1, ## Any aspect ratio is acceptable. + LANDSCAPE = 2, ## Prefer landscape (wider than tall) media. + PORTRAIT = 3, ## Prefer portrait (taller than wide) media. + SQUARE = 4, ## Prefer square (1:1) media. +} + +## Controls where the AdChoices icon is placed within the native ad view. +## Maps to NativeAdOptions.ADCHOICES_* constants. +enum NativeAdChoicesPlacement { + TOP_LEFT = 0, ## AdChoices icon in the top-left corner. + TOP_RIGHT = 1, ## AdChoices icon in the top-right corner (SDK default). + BOTTOM_RIGHT = 2, ## AdChoices icon in the bottom-right corner. + BOTTOM_LEFT = 3, ## AdChoices icon in the bottom-left corner. +} + +## Controls how icon/image assets are scaled inside their ImageView. +## Maps directly to Android's ImageView.ScaleType enum. +enum NativeImageScaleType { + MATRIX = 0, ## Scale using the image Matrix. + FIT_XY = 1, ## Scale to fill the view bounds, ignoring aspect ratio. + FIT_START = 2, ## Scale to fit within bounds, aligned to top-left. + FIT_CENTER = 3, ## Scale to fit within bounds, centered (Android default). + FIT_END = 4, ## Scale to fit within bounds, aligned to bottom-right. + CENTER = 5, ## Center image without scaling. + CENTER_CROP = 6, ## Scale so shorter dimension fits; crop the longer one. + CENTER_INSIDE = 7, ## Scale to fit within bounds if larger; center otherwise. +} + +const COLLAPSIBLE_POSITION_NAMES: Dictionary = { + CollapsiblePosition.TOP: "top", + CollapsiblePosition.BOTTOM: "bottom", +} + +const DATA_KEY_AD_UNIT_ID := &"ad_unit_id" +const DATA_KEY_REQUEST_AGENT := &"request_agent" +const DATA_KEY_AD_SIZE := &"ad_size" +const DATA_KEY_ADAPTIVE_WIDTH := &"adaptive_width" +const DATA_KEY_ADAPTIVE_MAX_HEIGHT := &"adaptive_max_height" +const DATA_KEY_AD_POSITION := &"ad_position" +const DATA_KEY_COLLAPSIBLE_POSITION := &"collapsible_position" +const DATA_KEY_ANCHOR_TO_SAFE_AREA := &"anchor_to_safe_area" +const DATA_KEY_KEYWORDS := &"keywords" +const DATA_KEY_USER_ID := &"user_id" +const DATA_KEY_CUSTOM_DATA := &"custom_data" +const DATA_KEY_NETWORK_EXTRAS := &"network_extras" + +# Native ad option keys — sent to the Java layer inside the ad-request dictionary. +const DATA_KEY_NATIVE_MEDIA_ASPECT_RATIO := &"native_media_aspect_ratio" +const DATA_KEY_NATIVE_RETURN_URLS_FOR_IMAGE_ASSETS := &"native_return_urls_for_image_assets" +const DATA_KEY_NATIVE_REQUEST_MULTIPLE_IMAGES := &"native_request_multiple_images" +const DATA_KEY_NATIVE_AD_CHOICES_PLACEMENT := &"native_ad_choices_placement" +const DATA_KEY_NATIVE_IMAGE_SCALE_TYPE := &"native_image_scale_type" +const DATA_KEY_NATIVE_DISABLE_VALIDATOR := &"native_disable_validator" + +const DEFAULT_DATA: Dictionary = { + DATA_KEY_KEYWORDS: [], + DATA_KEY_NETWORK_EXTRAS: [], +} + +var _data: Dictionary + + +func _init(a_data: Dictionary = DEFAULT_DATA.duplicate()) -> void: + _data = a_data + + +func set_ad_unit_id(a_value: String) -> LoadAdRequest: + _data[DATA_KEY_AD_UNIT_ID] = a_value + return self + + +func get_ad_unit_id() -> String: + return _data[DATA_KEY_AD_UNIT_ID] + + +func set_request_agent(a_value: String) -> LoadAdRequest: + _data[DATA_KEY_REQUEST_AGENT] = a_value + return self + + +func get_request_agent() -> String: + return _data[DATA_KEY_REQUEST_AGENT] + + +func set_ad_size(a_value: RequestedAdSize) -> LoadAdRequest: + _data[DATA_KEY_AD_SIZE] = RequestedAdSize.keys()[a_value] + return self + + +func get_ad_size() -> RequestedAdSize: + return RequestedAdSize[_data[DATA_KEY_AD_SIZE]] if _data.has(DATA_KEY_AD_SIZE) else RequestedAdSize.BANNER + + +func set_adaptive_width(a_value: int) -> LoadAdRequest: + _data[DATA_KEY_ADAPTIVE_WIDTH] = a_value + return self + + +func get_adaptive_width() -> int: + return _data[DATA_KEY_ADAPTIVE_WIDTH] + + +func set_adaptive_max_height(a_value: int) -> LoadAdRequest: + _data[DATA_KEY_ADAPTIVE_MAX_HEIGHT] = a_value + return self + + +func get_adaptive_max_height() -> int: + return _data[DATA_KEY_ADAPTIVE_MAX_HEIGHT] + + +func set_ad_position(a_value: AdPosition) -> LoadAdRequest: + _data[DATA_KEY_AD_POSITION] = AdPosition.keys()[a_value] + return self + + +func get_ad_position() -> AdPosition: + return AdPosition[_data[DATA_KEY_AD_POSITION]] if _data.has(DATA_KEY_AD_POSITION) else AdPosition.CUSTOM + + +func set_collapsible_position(a_value: CollapsiblePosition) -> LoadAdRequest: + if a_value != CollapsiblePosition.DISABLED: + _data[DATA_KEY_COLLAPSIBLE_POSITION] = COLLAPSIBLE_POSITION_NAMES[a_value] + return self + + +func get_collapsible_position() -> CollapsiblePosition: + return ( + CollapsiblePosition[_data[DATA_KEY_COLLAPSIBLE_POSITION]] + if _data.has(DATA_KEY_COLLAPSIBLE_POSITION) + else CollapsiblePosition.DISABLED + ) + + +func set_anchor_to_safe_area(a_value: bool) -> LoadAdRequest: + _data[DATA_KEY_ANCHOR_TO_SAFE_AREA] = a_value + return self + + +func get_anchor_to_safe_area() -> bool: + return _data[DATA_KEY_ANCHOR_TO_SAFE_AREA] if _data.has(DATA_KEY_ANCHOR_TO_SAFE_AREA) else false + + +func set_keywords(a_value: Array) -> LoadAdRequest: + if a_value == null: + _data[DATA_KEY_KEYWORDS] = [] + else: + _data[DATA_KEY_KEYWORDS] = a_value + return self + + +func add_keyword(a_value: String) -> LoadAdRequest: + _data[DATA_KEY_KEYWORDS].append(a_value) + return self + + +func get_keywords() -> Array: + return _data[DATA_KEY_KEYWORDS] if _data.has(DATA_KEY_KEYWORDS) else [] + + +func set_user_id(a_value: String) -> LoadAdRequest: + _data[DATA_KEY_USER_ID] = a_value + return self + + +func get_user_id() -> String: + return _data[DATA_KEY_USER_ID] if _data.has(DATA_KEY_USER_ID) else "" + + +func set_custom_data(a_value: String) -> LoadAdRequest: + _data[DATA_KEY_CUSTOM_DATA] = a_value + return self + + +func get_custom_data() -> String: + return _data[DATA_KEY_CUSTOM_DATA] if _data.has(DATA_KEY_CUSTOM_DATA) else "" + + +func set_network_extras(a_value: Array) -> LoadAdRequest: + if a_value == null: + _data[DATA_KEY_NETWORK_EXTRAS] = [] + else: + _data[DATA_KEY_NETWORK_EXTRAS] = a_value + return self + + +func get_network_extras() -> Array: + return _data[DATA_KEY_NETWORK_EXTRAS] if _data.has(DATA_KEY_NETWORK_EXTRAS) else [] + + +# --------------------------------------------------------------------------- +# Native Ad Options +# --------------------------------------------------------------------------- + + +## Sets a preferred media aspect ratio for native ads. +## Has no effect when used for non-native ad types. +func set_native_media_aspect_ratio(a_value: NativeMediaAspectRatio) -> LoadAdRequest: + _data[DATA_KEY_NATIVE_MEDIA_ASPECT_RATIO] = NativeMediaAspectRatio.keys()[a_value] + return self + + +func get_native_media_aspect_ratio() -> NativeMediaAspectRatio: + return ( + NativeMediaAspectRatio[_data[DATA_KEY_NATIVE_MEDIA_ASPECT_RATIO]] + if _data.has(DATA_KEY_NATIVE_MEDIA_ASPECT_RATIO) + else NativeMediaAspectRatio.UNKNOWN + ) + + +## When true the SDK returns image asset URLs instead of pre-fetched Drawable +## objects, allowing the app to download and manage images itself. +## Has no effect when used for non-native ad types. +func set_native_return_urls_for_image_assets(a_value: bool) -> LoadAdRequest: + _data[DATA_KEY_NATIVE_RETURN_URLS_FOR_IMAGE_ASSETS] = a_value + return self + + +func get_native_return_urls_for_image_assets() -> bool: + return ( + _data[DATA_KEY_NATIVE_RETURN_URLS_FOR_IMAGE_ASSETS] + if _data.has(DATA_KEY_NATIVE_RETURN_URLS_FOR_IMAGE_ASSETS) + else false + ) + + +## When true the SDK may return ads that contain multiple images for a single +## asset slot. By default only one image per slot is returned. +## Has no effect when used for non-native ad types. +func set_native_request_multiple_images(a_value: bool) -> LoadAdRequest: + _data[DATA_KEY_NATIVE_REQUEST_MULTIPLE_IMAGES] = a_value + return self + + +func get_native_request_multiple_images() -> bool: + return ( + _data[DATA_KEY_NATIVE_REQUEST_MULTIPLE_IMAGES] if _data.has(DATA_KEY_NATIVE_REQUEST_MULTIPLE_IMAGES) else false + ) + + +## Sets where the AdChoices icon is placed inside the native ad view. +## Has no effect when used for non-native ad types. +func set_native_ad_choices_placement(a_value: NativeAdChoicesPlacement) -> LoadAdRequest: + _data[DATA_KEY_NATIVE_AD_CHOICES_PLACEMENT] = NativeAdChoicesPlacement.keys()[a_value] + return self + + +func get_native_ad_choices_placement() -> NativeAdChoicesPlacement: + return ( + NativeAdChoicesPlacement[_data[DATA_KEY_NATIVE_AD_CHOICES_PLACEMENT]] + if _data.has(DATA_KEY_NATIVE_AD_CHOICES_PLACEMENT) + else NativeAdChoicesPlacement.TOP_RIGHT + ) + + +## Sets how icon and image assets are scaled inside their Android ImageView. +## Has no effect when used for non-native ad types. +func set_native_image_scale_type(a_value: NativeImageScaleType) -> LoadAdRequest: + _data[DATA_KEY_NATIVE_IMAGE_SCALE_TYPE] = NativeImageScaleType.keys()[a_value] + return self + + +func get_native_image_scale_type() -> NativeImageScaleType: + return ( + NativeImageScaleType[_data[DATA_KEY_NATIVE_IMAGE_SCALE_TYPE]] + if _data.has(DATA_KEY_NATIVE_IMAGE_SCALE_TYPE) + else NativeImageScaleType.FIT_CENTER + ) + + +## When true the SDK's native ad validator is disabled. The validator normally +## logs warnings when required view bindings are missing; disabling it suppresses +## those warnings in test / custom-layout scenarios. +## Has no effect when used for non-native ad types. +func set_native_disable_validator(a_value: bool) -> LoadAdRequest: + _data[DATA_KEY_NATIVE_DISABLE_VALIDATOR] = a_value + return self + + +func get_native_disable_validator() -> bool: + return _data[DATA_KEY_NATIVE_DISABLE_VALIDATOR] if _data.has(DATA_KEY_NATIVE_DISABLE_VALIDATOR) else false + + +func get_raw_data() -> Dictionary: + return _data diff --git a/addons/AdmobPlugin/model/LoadAdRequest.gd.uid b/addons/AdmobPlugin/model/LoadAdRequest.gd.uid new file mode 100644 index 0000000..def3bd8 --- /dev/null +++ b/addons/AdmobPlugin/model/LoadAdRequest.gd.uid @@ -0,0 +1 @@ +uid://c86msp23213bn diff --git a/addons/AdmobPlugin/model/MediationNetwork.gd b/addons/AdmobPlugin/model/MediationNetwork.gd new file mode 100644 index 0000000..fd01a4e --- /dev/null +++ b/addons/AdmobPlugin/model/MediationNetwork.gd @@ -0,0 +1,322 @@ +# +# © 2024-present https://github.com/cengiz-pz +# +class_name MediationNetwork +extends RefCounted + +enum Flag { + APPLOVIN = 1 << 0, ## AppLovin + CHARTBOOST = 1 << 1, ## Chartboost + DTEXCHANGE = 1 << 2, ## DT Exchange (previously Fyber) + IMOBILE = 1 << 3, ## i-mobile + INMOBI = 1 << 4, ## InMobi + IRONSOURCE = 1 << 5, ## ironSource + LIFTOFF = 1 << 6, ## Liftoff Monetize (previously Vungle) + LINE = 1 << 7, ## LINE Ads Network + MAIO = 1 << 8, ## maio + META = 1 << 9, ## Meta Audience Network (previously Facebook) + MINTEGRAL = 1 << 10, ## Mintegral + MOLOCO = 1 << 11, ## Moloco + MYTARGET = 1 << 12, ## myTarget + PANGLE = 1 << 13, ## Pangle + UNITY = 1 << 14, ## Unity Ads +} + +const FLAG_PROPERTY := &"flag" +const TAG_PROPERTY := &"tag" +const DEPENDENCIES_PROPERTY := &"dependencies" +const MAVEN_REPO_PROPERTY := &"maven_repo" +const PACKAGE_PROPERTY := &"package" +const PACKAGE_URL_PROPERTY := &"package_url" +const PACKAGE_VERSION_PROPERTY := &"package_version" +const SK_AD_NETWORK_IDS_PROPERTY := &"sk_ad_network_ids" +const GOOGLE_SK_AD_NETWORK_IDS = [ "cstr6suwn9", "4fzdc2evr5", "2fnua5tdw4", "ydx93a7ass", "p78axxw29g", "v72qych5uu", "ludvb6z3bs", "cp8zw746q7", "3sh42y64q3", "c6k4g5qg8m", "s39g8k73mm", "3qy4746246", "f38h382jlk", "hs6bdukanm", "mlmmfzh3r3", "v4nxqhlyqp", "wzmmz9fp6w", "su67r6k2v3", "yclnxrl5pm", "t38b2kh725", "7ug5zh24hu", "gta9lk7p23", "vutu7akeur", "y5ghdn5j9k", "v9wttpbfk9", "n38lu8286q", "47vhws6wlr", "kbd757ywx3", "9t245vhmpl", "a2p9lx4jpn", "22mmun2rn5", "44jx6755aq", "k674qkevps", "4468km3ulz", "2u9pt9hc89", "8s468mfl3y", "klf5c3l5u5", "ppxm28t8ap", "kbmxgpxpgc", "uw77j35x4d", "578prtvx9j", "4dzt52r2t5", "tl55sbb4fm", "c3frkrj4fj", "e5fvkxwrpn", "8c4e2ghe7u", "3rd42ekr43", "97r2b46745", "3qcr597p9d" ] + +const SK_AD_NETWORK_ITEM_LIST_FORMAT: String = """ + SKAdNetworkItems + +%s + +""" + +const SK_AD_NETWORK_ITEM_FORMAT: String = """ + + SKAdNetworkIdentifier + %s.skadnetwork + +""" + +const MEDIATION_NETWORKS: Dictionary = { + Flag.APPLOVIN: { + FLAG_PROPERTY: Flag.APPLOVIN, + TAG_PROPERTY: "applovin", + DEPENDENCIES_PROPERTY: ["com.google.ads.mediation:applovin:13.4.0.1"], + MAVEN_REPO_PROPERTY: "", + PACKAGE_PROPERTY: "AppLovinAdapterTarget", + PACKAGE_URL_PROPERTY: "https://github.com/googleads/googleads-mobile-ios-mediation-applovin.git", + PACKAGE_VERSION_PROPERTY: "13.4.0", + SK_AD_NETWORK_IDS_PROPERTY: ["22mmun2rn5", "238da6jt44", "24t9a8vw3c", "24zw6aqk47", "252b5q8x7y", "275upjj5gd", "294l99pt4k", "2fnua5tdw4", "2u9pt9hc89", "32z4fx6l9h", "3l6bd9hu43", "3qcr597p9d", "3qy4746246", "3rd42ekr43", "3sh42y64q3", "424m5254lk", "4468km3ulz", "44jx6755aq", "44n7hlldy6", "47vhws6wlr", "488r3q3dtq", "4dzt52r2t5", "4fzdc2evr5", "4mn522wn87", "4pfyvq9l8r", "4w7y6s5ca2", "523jb4fst2", "52fl2v3hgk", "54nzkqm89y", "578prtvx9j", "5a6flpkh64", "5l3tpt7t6e", "5lm9lj6jb7", "5tjdwbrq8w", "6964rsfnh4", "6g9af3uyq4", "6p4ks3rnbw", "6v7lgmsu45", "6xzpu9s2p8", "737z793b9f", "74b6s63p6l", "79pbpufp6p", "7fmhfwg9en", "7rz58n8ntl", "7ug5zh24hu", "84993kbrcf", "89z7zv988g", "8c4e2ghe7u", "8m87ys6875", "8r8llnkz5a", "8s468mfl3y", "97r2b46745", "9b89h5y424", "9nlqeag3gk", "9rd848q2bz", "9t245vhmpl", "9vvzujtq5s", "9yg77x724h", "a2p9lx4jpn", "a7xqa6mtl2", "a8cz6cu7e5", "av6w8kgt66", "b9bk5wbcq9", "bxvub5ada5", "c3frkrj4fj", "c6k4g5qg8m", "cg4yq2srnc", "cj5566h2ga", "cp8zw746q7", "cs644xg564", "cstr6suwn9", "dbu4b84rxf", "dkc879ngq3", "dzg6xy7pwj", "e5fvkxwrpn", "ecpz2srf59", "eh6m2bh4zr", "ejvt5qm6ak", "f38h382jlk", "f73kdq92p3", "f7s53z58qe", "feyaarzu9v", "g28c52eehv", "g2y4y55b64", "g6gcrrvk4p", "ggvn48r87g", "glqzh8vgby", "gta8lk7p23", "gta9lk7p23", "hb56zgv37p", "hdw39hrw9y", "hs6bdukanm", "k674qkevps", "kbd757ywx3", "kbmxgpxpgc", "klf5c3l5u5", "krvm3zuq6h", "lr83yxwka7", "ludvb6z3bs", "m297p6643m", "m5mvw97r93", "m8dbw4sv7c", "mj797d8u6f", "mlmmfzh3r3", "mls7yz5dvl", "mp6xlyr22a", "mqn7fxpca7", "mtkv5xtk9e", "n38lu8286q", "n66cz3y3bx", "n6fk4nfna4", "n9x2a789qt", "nzq8sh4pbs", "p78axxw29g", "ppxm28t8ap", "prcb7njmu6", "pwa73g5rt2", "pwdxu55a5a", "qqp299437r", "qu637u8glc", "r45fhb6rf7", "rvh3l7un93", "rx5hdcabgc", "s39g8k73mm", "s69wq72ugq", "su67r6k2v3", "t38b2kh725", "tl55sbb4fm", "u679fj5vs4", "uw77j35x4d", "v4nxqhlyqp", "v72qych5uu", "v79kvwwj4g", "v9wttpbfk9", "vcra2ehyfk", "vhf287vqwu", "vutu7akeur", "w9q455wk68", "wg4vff78zm", "wzmmz9fp6w", "x44k69ngh6", "x5l83yy675", "x8jxxk4ff5", "x8uqf25wch", "xga6mpmplv", "xy9t38ct57", "y45688jllp", "y5ghdn5j9k", "yclnxrl5pm", "ydx93a7ass", "zmvfpc5aq8", "zq492l623r"], + }, + Flag.CHARTBOOST: { + FLAG_PROPERTY: Flag.CHARTBOOST, + TAG_PROPERTY: "chartboost", + DEPENDENCIES_PROPERTY: ["com.google.ads.mediation:chartboost:9.10.0.1"], + MAVEN_REPO_PROPERTY: "https://cboost.jfrog.io/artifactory/chartboost-ads/", + PACKAGE_PROPERTY: "ChartboostAdapterTarget", + PACKAGE_URL_PROPERTY: "https://github.com/googleads/googleads-mobile-ios-mediation-chartboost.git", + PACKAGE_VERSION_PROPERTY: "9.11.001", + SK_AD_NETWORK_IDS_PROPERTY: ["f38h382jlk"], + }, + Flag.DTEXCHANGE: { + FLAG_PROPERTY: Flag.DTEXCHANGE, + TAG_PROPERTY: "dtexchange", + DEPENDENCIES_PROPERTY: ["com.google.ads.mediation:fyber:8.4.0.0"], + MAVEN_REPO_PROPERTY: "", + PACKAGE_PROPERTY: "DTExchangeAdapterTarget", + PACKAGE_URL_PROPERTY: "https://github.com/googleads/googleads-mobile-ios-mediation-dtexchange.git", + PACKAGE_VERSION_PROPERTY: "8.4.600", + SK_AD_NETWORK_IDS_PROPERTY: ["ydx93a7ass", "t6d3zquu66", "4fzdc2evr5", "a8cz6cu7e5", "9nlqeag3gk", "a2p9lx4jpn", "k674qkevps", "c3frkrj4fj", "n6fk4nfna4", "mtkv5xtk9e", "p78axxw29g", "n9x2a789qt", "kbmxgpxpgc", "s39g8k73mm", "krvm3zuq6h", "294l99pt4k", "3rd42ekr43", "ggvn48r87g", "4pfyvq9l8r", "cg4yq2srnc", "5a6flpkh64", "v72qych5uu", "x8uqf25wch", "c6k4g5qg8m", "wg4vff78zm", "rx5hdcabgc", "32z4fx6l9h", "3qy4746246", "252b5q8x7y", "f38h382jlk", "24t9a8vw3c", "hs6bdukanm", "9rd848q2bz", "prcb7njmu6", "m8dbw4sv7c", "9g2aggbj52", "wzmmz9fp6w", "yclnxrl5pm", "t38b2kh725", "7ug5zh24hu", "5lm9lj6jb7", "ejvt5qm6ak", "7rz58n8ntl", "kbd757ywx3", "9t245vhmpl", "44jx6755aq", "tl55sbb4fm", "8s468mfl3y", "4468km3ulz", "2u9pt9hc89", "av6w8kgt66", "klf5c3l5u5", "hdw39hrw9y", "y45688jllp", "dzg6xy7pwj", "3sh42y64q3", "ppxm28t8ap", "f73kdq92p3", "5l3tpt7t6e", "uw77j35x4d", "mlmmfzh3r3", "6g9af3uyq4", "su67r6k2v3", "g28c52eehv", "pwa73g5rt2", "zq492l623r", "22mmun2rn5", "8r8llnkz5a", "47vhws6wlr", "5tjdwbrq8w", "h65wbv5k3f", "2fnua5tdw4", "7fmhfwg9en", "tvvz7th9br", "x44k69ngh6", "vhf287vqwu", "4dzt52r2t5", "x8yj322td6", "tskbem2b5g", "cp8zw746q7", "cj5566h2ga", "578prtvx9j", "e5fvkxwrpn", "r45fhb6rf7", "mqn7fxpca7", "g6gcrrvk4p", "xga6mpmplv", "qu637u8glc", "w9q455wk68", "275upjj5gd", "97r2b46745", "cstr6suwn9"], + }, + Flag.IMOBILE: { + FLAG_PROPERTY: Flag.IMOBILE, + TAG_PROPERTY: "imobile", + DEPENDENCIES_PROPERTY: ["com.google.ads.mediation:imobile:2.3.2.1"], + MAVEN_REPO_PROPERTY: "https://imobile.github.io/adnw-sdk-android", + PACKAGE_PROPERTY: "IMobileAdapterTarget", + PACKAGE_URL_PROPERTY: "https://github.com/googleads/googleads-mobile-ios-mediation-imobile.git", + PACKAGE_VERSION_PROPERTY: "2.3.402", + SK_AD_NETWORK_IDS_PROPERTY: ["v4nxqhlyqp"], + }, + Flag.INMOBI: { + FLAG_PROPERTY: Flag.INMOBI, + TAG_PROPERTY: "inmobi", + DEPENDENCIES_PROPERTY: ["com.google.ads.mediation:inmobi:10.8.8.1"], + MAVEN_REPO_PROPERTY: "", + PACKAGE_PROPERTY: "InMobiAdapterTarget", + PACKAGE_URL_PROPERTY: "https://github.com/googleads/googleads-mobile-ios-mediation-inmobi.git", + PACKAGE_VERSION_PROPERTY: "10.8.600", + SK_AD_NETWORK_IDS_PROPERTY: ["uw77j35x4d", "7ug5zh24hu", "hs6bdukanm", "4fzdc2evr5", "ggvn48r87g", "5lm9lj6jb7", "9rd848q2bz", "c6k4g5qg8m", "wzmmz9fp6w", "3sh42y64q3", "yclnxrl5pm", "kbd757ywx3", "f73kdq92p3", "ydx93a7ass", "w9q455wk68", "prcb7njmu6", "wg4vff78zm", "mlmmfzh3r3", "tl55sbb4fm", "4pfyvq9l8r", "t38b2kh725", "5l3tpt7t6e", "7rz58n8ntl", "klf5c3l5u5", "cg4yq2srnc", "av6w8kgt66", "9t245vhmpl", "v72qych5uu", "2u9pt9hc89", "44jx6755aq", "8s468mfl3y", "p78axxw29g", "ppxm28t8ap", "424m5254lk", "5a6flpkh64", "pwa73g5rt2", "e5fvkxwrpn", "9nlqeag3gk", "k674qkevps", "cj5566h2ga", "mtkv5xtk9e", "3rd42ekr43", "g28c52eehv", "2fnua5tdw4", "4468km3ulz", "97r2b46745", "glqzh8vgby", "3qcr597p9d", "578prtvx9j", "n6fk4nfna4", "b9bk5wbcq9", "kbmxgpxpgc", "294l99pt4k", "523jb4fst2", "mls7yz5dvl", "74b6s63p6l", "22mmun2rn5", "52fl2v3hgk", "6g9af3uyq4", "x5l83yy675", "mqn7fxpca7", "g6gcrrvk4p", "r45fhb6rf7", "c3frkrj4fj", "ejvt5qm6ak", "f38h382jlk", "488r3q3dtq", "55644vm79v", "5tjdwbrq8w", "nzq8sh4pbs", "m8dbw4sv7c", "7fmhfwg9en", "6v7lgmsu45", "s39g8k73mm", "6yxyv74ff7", "55y65gfgn7", "mqn7fxpca7", "qqp299437r", "32z4fx6l9h", "feyaarzu9v", "cp8zw746q7", "252b5q8x7y", "qu637u8glc", "vhf287vqwu", "a8cz6cu7e5", "cwn433xbcr", "fq6vru337s", "87u5trcl3r", "mj797d8u6f", "dbu4b84rxf", "238da6jt44", "4dzt52r2t5", "a2p9lx4jpn", "t6d3zquu66", "zq492l623r", "cstr6suwn9", "xga6mpmplv", "tmhh9296z4", "6xzpu9s2p8", "ludvb6z3bs", "v9wttpbfk9", "n38lu8286q", "fz2k2k5tej", "g2y4y55b64", "su67r6k2v3", "v79kvwwj4g", "275upjj5gd", "24zw6aqk47", "44n7hlldy6", "ecpz2srf59", "bvpn9ufa9b", "gta9lk7p23"], + }, + Flag.IRONSOURCE: { + FLAG_PROPERTY: Flag.IRONSOURCE, + TAG_PROPERTY: "ironsource", + DEPENDENCIES_PROPERTY: ["com.google.ads.mediation:ironsource:9.0.0.1"], + MAVEN_REPO_PROPERTY: "", + PACKAGE_PROPERTY: "IronSourceAdapterTarget", + PACKAGE_URL_PROPERTY: "https://github.com/googleads/googleads-mobile-ios-mediation-ironsource.git", + PACKAGE_VERSION_PROPERTY: "8.10.0", + SK_AD_NETWORK_IDS_PROPERTY: ["su67r6k2v3"], + }, + Flag.LIFTOFF: { + FLAG_PROPERTY: Flag.LIFTOFF, + TAG_PROPERTY: "liftoff", + DEPENDENCIES_PROPERTY: ["com.google.ads.mediation:vungle:7.6.0.0"], + MAVEN_REPO_PROPERTY: "", + PACKAGE_PROPERTY: "LiftoffMonetizeAdapterTarget", + PACKAGE_URL_PROPERTY: "https://github.com/googleads/googleads-mobile-ios-mediation-liftoffmonetize.git", + PACKAGE_VERSION_PROPERTY: "7.6.0", + SK_AD_NETWORK_IDS_PROPERTY: ["22mmun2rn5", "238da6jt44", "24t9a8vw3c", "24zw6aqk47", "275upjj5gd", "294l99pt4k", "2fnua5tdw4", "2u9pt9hc89", "32z4fx6l9h", "3l6bd9hu43", "3qcr597p9d", "3qy4746246", "3rd42ekr43", "3sh42y64q3", "424m5254lk", "4468km3ulz", "44jx6755aq", "44n7hlldy6", "488r3q3dtq", "4dzt52r2t5", "4fzdc2evr5", "4pfyvq9l8r", "4w7y6s5ca2", "523jb4fst2", "52fl2v3hgk", "54nzkqm89y", "578prtvx9j", "5a6flpkh64", "5l3tpt7t6e", "5lm9lj6jb7", "5tjdwbrq8w", "6964rsfnh4", "6g9af3uyq4", "6lz2ygh3q6", "6p4ks3rnbw", "6rd35atwn8", "6v7lgmsu45", "6xzpu9s2p8", "6yxyv74ff7", "737z793b9f", "74b6s63p6l", "79pbpufp6p", "7fmhfwg9en", "7rz58n8ntl", "7ug5zh24hu", "84993kbrcf", "87u5trcl3r", "89z7zv988g", "8m87ys6875", "8s468mfl3y", "97r2b46745", "9b89h5y424", "9nlqeag3gk", "9rd848q2bz", "9t245vhmpl", "9vvzujtq5s", "a2p9lx4jpn", "a7xqa6mtl2", "a8cz6cu7e5", "apzhy3va96", "av6w8kgt66", "b9bk5wbcq9", "bvpn9ufa9b", "bxvub5ada5", "c3frkrj4fj", "c6k4g5qg8m", "cg4yq2srnc", "cj5566h2ga", "cp8zw746q7", "cs644xg564", "cstr6suwn9", "cwn433xbcr", "dbu4b84rxf", "dkc879ngq3", "dzg6xy7pwj", "e5fvkxwrpn", "ecpz2srf59", "ejvt5qm6ak", "f2zub97jtl", "f38h382jlk", "f73kdq92p3", "f7s53z58qe", "feyaarzu9v", "fq6vru337s", "fz2k2k5tej", "g28c52eehv", "g2y4y55b64", "g6gcrrvk4p", "ggvn48r87g", "glqzh8vgby", "gta9lk7p23", "hb56zgv37p", "hdw39hrw9y", "hs6bdukanm", "k674qkevps", "k6y4y55b64", "kbd757ywx3", "kbmxgpxpgc", "klf5c3l5u5", "krvm3zuq6h", "ln5gz23vtd", "lr83yxwka7", "ludvb6z3bs", "m297p6643m", "m2jqnlggk3", "m5mvw97r93", "m8dbw4sv7c", "mj797d8u6f", "mlmmfzh3r3", "mls7yz5dvl", "mp6xlyr22a", "mqn7fxpca7", "mtkv5xtk9e", "n38lu8286q", "n6fk4nfna4", "n9x2a789qt", "ns5j362hk7", "p78axxw29g", "pg7ctvrt6f", "ppxm28t8ap", "prcb7njmu6", "pwa73g5rt2", "pwdxu55a5a", "qqp299437r", "qu637u8glc", "qwpu75vrh2", "r45fhb6rf7", "raa6f494kr", "rvh3l7un93", "rx5hdcabgc", "s39g8k73mm", "sczv5946wb", "su67r6k2v3", "t38b2kh725", "t6d3zquu66", "thzdn4h5nc", "tl55sbb4fm", "tmhh9296z4", "u679fj5vs4", "uw77j35x4d", "v72qych5uu", "v79kvwwj4g", "v9wttpbfk9", "vcra2ehyfk", "vhf287vqwu", "w9q455wk68", "wg4vff78zm", "wzmmz9fp6w", "x44k69ngh6", "x5l83yy675", "x8uqf25wch", "xga6mpmplv", "xy9t38ct57", "y45688jllp", "y5ghdn5j9k", "yclnxrl5pm", "ydx93a7ass", "z959bm4gru", "zmvfpc5aq8", "zq492l623r"], + }, + Flag.LINE: { + FLAG_PROPERTY: Flag.LINE, + TAG_PROPERTY: "line", + DEPENDENCIES_PROPERTY: ["com.google.ads.mediation:line:2.9.20250924.1"], + MAVEN_REPO_PROPERTY: "", + PACKAGE_PROPERTY: "LineAdapterTarget", + PACKAGE_URL_PROPERTY: "https://github.com/googleads/googleads-mobile-ios-mediation-line.git", + PACKAGE_VERSION_PROPERTY: "2.9.2025111900", + SK_AD_NETWORK_IDS_PROPERTY: ["vutu7akeur", "eh6m2bh4zr", "cstr6suwn9", "578prtvx9j", "9t245vhmpl", "v72qych5uu", "x8uqf25wch", "7ug5zh24hu", "hs6bdukanm", "dbu4b84rxf", "8c4e2ghe7u"], + }, + Flag.MAIO: { + FLAG_PROPERTY: Flag.MAIO, + TAG_PROPERTY: "maio", + DEPENDENCIES_PROPERTY: ["com.google.ads.mediation:maio:2.0.6.0"], + MAVEN_REPO_PROPERTY: "https://imobile-maio.github.io/maven", + PACKAGE_PROPERTY: "MaioAdapterTarget", + PACKAGE_URL_PROPERTY: "https://github.com/googleads/googleads-mobile-ios-mediation-maio.git", + PACKAGE_VERSION_PROPERTY: "2.2.0", + SK_AD_NETWORK_IDS_PROPERTY: ["v4nxqhlyqp"], + }, + Flag.META: { + FLAG_PROPERTY: Flag.META, + TAG_PROPERTY: "meta", + DEPENDENCIES_PROPERTY: ["com.google.ads.mediation:facebook:6.20.0.2"], + MAVEN_REPO_PROPERTY: "", + PACKAGE_PROPERTY: "MetaAdapterTarget", + PACKAGE_URL_PROPERTY: "https://github.com/googleads/googleads-mobile-ios-mediation-meta.git", + PACKAGE_VERSION_PROPERTY: "6.20.100", + SK_AD_NETWORK_IDS_PROPERTY: ["v9wttpbfk9", "n38lu8286q"], + }, + Flag.MINTEGRAL: { + FLAG_PROPERTY: Flag.MINTEGRAL, + TAG_PROPERTY: "mintegral", + DEPENDENCIES_PROPERTY: ["com.google.ads.mediation:mintegral:16.9.91.2"], + MAVEN_REPO_PROPERTY: "https://dl-maven-android.mintegral.com/repository/mbridge_android_sdk_oversea", + PACKAGE_PROPERTY: "MintegralAdapterTarget", + PACKAGE_URL_PROPERTY: "https://github.com/googleads/googleads-mobile-ios-mediation-mintegral.git", + PACKAGE_VERSION_PROPERTY: "7.7.901", + SK_AD_NETWORK_IDS_PROPERTY: ["kbd757ywx3"], + }, + Flag.MOLOCO: { + FLAG_PROPERTY: Flag.MOLOCO, + TAG_PROPERTY: "moloco", + DEPENDENCIES_PROPERTY: ["com.google.ads.mediation:moloco:4.2.0.0"], + MAVEN_REPO_PROPERTY: "", + PACKAGE_PROPERTY: "MolocoAdapterTarget", + PACKAGE_URL_PROPERTY: "https://github.com/googleads/googleads-mobile-ios-mediation-moloco.git", + PACKAGE_VERSION_PROPERTY: "4.1.0", + SK_AD_NETWORK_IDS_PROPERTY: ["9t245vhmpl"], + }, + Flag.MYTARGET: { + FLAG_PROPERTY: Flag.MYTARGET, + TAG_PROPERTY: "mytarget", + DEPENDENCIES_PROPERTY: ["com.google.ads.mediation:mytarget:5.27.3.0"], + MAVEN_REPO_PROPERTY: "", + PACKAGE_PROPERTY: "MyTargetAdapterTarget", + PACKAGE_URL_PROPERTY: "https://github.com/googleads/googleads-mobile-ios-mediation-mytarget.git", + PACKAGE_VERSION_PROPERTY: "5.36.0", + SK_AD_NETWORK_IDS_PROPERTY: ["n9x2a789qt", "r26jy69rpl"], + }, + Flag.PANGLE: { + FLAG_PROPERTY: Flag.PANGLE, + TAG_PROPERTY: "pangle", + DEPENDENCIES_PROPERTY: ["com.google.ads.mediation:pangle:7.6.0.5.0"], + MAVEN_REPO_PROPERTY: "https://artifact.bytedance.com/repository/pangle/", + PACKAGE_PROPERTY: "PangleAdapterTarget", + PACKAGE_URL_PROPERTY: "https://github.com/googleads/googleads-mobile-ios-mediation-pangle.git", + PACKAGE_VERSION_PROPERTY: "7.6.600", + SK_AD_NETWORK_IDS_PROPERTY: ["22mmun2rn5"], + }, + Flag.UNITY: { + FLAG_PROPERTY: Flag.UNITY, + TAG_PROPERTY: "unity", + DEPENDENCIES_PROPERTY: ["com.unity3d.ads:unity-ads:4.16.2", "com.google.ads.mediation:unity:4.16.3.0"], + MAVEN_REPO_PROPERTY: "", + PACKAGE_PROPERTY: "UnityAdapterTarget", + PACKAGE_URL_PROPERTY: "https://github.com/googleads/googleads-mobile-ios-mediation-unity.git", + PACKAGE_VERSION_PROPERTY: "4.16.4", + SK_AD_NETWORK_IDS_PROPERTY: ["9nlqeag3gk", "hs6bdukanm", "9rd848q2bz", "22mmun2rn5", "f73kdq92p3", "tl55sbb4fm", "prcb7njmu6", "n9x2a789qt", "97r2b46745", "zmvfpc5aq8", "av6w8kgt66", "uw77j35x4d", "pwa73g5rt2", "e5fvkxwrpn", "klf5c3l5u5", "xga6mpmplv", "wg4vff78zm", "lr83yxwka7", "5tjdwbrq8w", "7ug5zh24hu", "glqzh8vgby", "feyaarzu9v", "6yxyv74ff7", "mqn7fxpca7", "vhf287vqwu", "g6gcrrvk4p", "v79kvwwj4g", "32z4fx6l9h", "2fnua5tdw4", "a8cz6cu7e5", "v72qych5uu", "a2p9lx4jpn", "5f5u5tfb26", "4fzdc2evr5", "5lm9lj6jb7", "4w7y6s5ca2", "3sh42y64q3", "4468km3ulz", "mj797d8u6f", "488r3q3dtq", "mlmmfzh3r3", "9t245vhmpl", "x44k69ngh6", "kbd757ywx3", "4dzt52r2t5", "k674qkevps", "294l99pt4k", "m8dbw4sv7c", "2u9pt9hc89", "p78axxw29g", "424m5254lk", "238da6jt44", "ydx93a7ass", "s39g8k73mm", "v9wttpbfk9", "44jx6755aq", "578prtvx9j", "yclnxrl5pm", "k6y4y55b64", "5a6flpkh64", "3rd42ekr43", "f38h382jlk", "cstr6suwn9", "ppxm28t8ap", "f7s53z58qe", "zq492l623r", "t38b2kh725", "mp6xlyr22a", "3qy4746246", "5l3tpt7t6e", "c6k4g5qg8m", "w9q455wk68", "4pfyvq9l8r", "wzmmz9fp6w", "8s468mfl3y"], + }, +} + +const MEDIATION_NETWORK_TAGS: Dictionary = { + "applovin": Flag.APPLOVIN, + "chartboost": Flag.CHARTBOOST, + "dtexchange": Flag.DTEXCHANGE, + "imobile": Flag.IMOBILE, + "inmobi": Flag.INMOBI, + "ironsource": Flag.IRONSOURCE, + "liftoff": Flag.LIFTOFF, + "line": Flag.LINE, + "maio": Flag.MAIO, + "meta": Flag.META, + "mintegral": Flag.MINTEGRAL, + "moloco": Flag.MOLOCO, + "mytarget": Flag.MYTARGET, + "pangle": Flag.PANGLE, + "unity": Flag.UNITY, +} + +var flag: Flag +var tag: String +var android_dependencies: Array +var android_custom_maven_repo: String +var swift_package: String +var swift_package_url: String +var swift_package_version: String +var sk_ad_network_ids: PackedStringArray + + +func _init(a_data: Dictionary) -> void: + flag = a_data[FLAG_PROPERTY] + tag = a_data[TAG_PROPERTY] + android_dependencies = a_data[DEPENDENCIES_PROPERTY] + android_custom_maven_repo = a_data[MAVEN_REPO_PROPERTY] + swift_package = a_data[PACKAGE_PROPERTY] + swift_package_url = a_data[PACKAGE_URL_PROPERTY] + swift_package_version = a_data[PACKAGE_VERSION_PROPERTY] + sk_ad_network_ids = a_data[SK_AD_NETWORK_IDS_PROPERTY] + + +func get_spm_dependency() -> SpmDependency: + return SpmDependency.new({ + SpmDependency.URL_PROPERTY: swift_package_url, + SpmDependency.VERSION_PROPERTY: swift_package_version, + SpmDependency.PRODUCTS_PROPERTY: [swift_package], + }) + +static func is_flag_enabled(a_value: int, a_flag: Flag) -> bool: + return a_value & a_flag + + +static func is_valid_tag(a_tag: String) -> bool: + return MEDIATION_NETWORK_TAGS.has(a_tag) + + +static func get_by_flag(a_flag: Flag) -> MediationNetwork: + return MediationNetwork.new(MEDIATION_NETWORKS[a_flag]) + + +static func get_by_tag(a_tag: String) -> MediationNetwork: + return get_by_flag(MEDIATION_NETWORK_TAGS[a_tag]) + + +static func get_all_enabled(a_value: int) -> Array[MediationNetwork]: + var __enabled_networks: Array[MediationNetwork] = [] + + for __flag in Flag.values(): + if is_flag_enabled(a_value, __flag): + __enabled_networks.append(get_by_flag(__flag)) + + return __enabled_networks + + +static func get_all_enabled_tags(a_value: int) -> Array[String]: + var __enabled_network_tags: Array[String] = [] + + for __flag in Flag.values(): + if is_flag_enabled(a_value, __flag): + var __network: MediationNetwork = get_by_flag(__flag) + __enabled_network_tags.append(__network.tag) + + return __enabled_network_tags + + +static func generate_sk_ad_network_plist(a_networks: Array[MediationNetwork]) -> String: + var __sk_ad_ids_plist_content: String + + var __unique_sk_network_ad_ids: Dictionary = { } + + # Add Google-required SK Ad Network IDs + for __network_id in GOOGLE_SK_AD_NETWORK_IDS: + if not __unique_sk_network_ad_ids.has(__network_id): + __unique_sk_network_ad_ids.set(__network_id, null) + + # Add SK Ad Network IDs required by other networks + for __network in a_networks: + for __network_id in __network.sk_ad_network_ids: + if not __unique_sk_network_ad_ids.has(__network_id): + __unique_sk_network_ad_ids.set(__network_id, null) + + for __network_id in __unique_sk_network_ad_ids.keys(): + __sk_ad_ids_plist_content += SK_AD_NETWORK_ITEM_FORMAT % __network_id + + return SK_AD_NETWORK_ITEM_LIST_FORMAT % __sk_ad_ids_plist_content + + +static func get_spm_dependencies(a_networks: Array[MediationNetwork]) -> Array[SpmDependency]: + var __spm_dependencies: Array[SpmDependency] = [] + + for __network in a_networks: + __spm_dependencies.append(__network.get_spm_dependency()) + + return __spm_dependencies + + +static func generate_tag_list(a_networks: Array[MediationNetwork]) -> String: + var __enabled_network_tags: PackedStringArray = [] + + for __network in a_networks: + __enabled_network_tags.append(__network.tag) + + return ",".join(__enabled_network_tags) diff --git a/addons/AdmobPlugin/model/MediationNetwork.gd.uid b/addons/AdmobPlugin/model/MediationNetwork.gd.uid new file mode 100644 index 0000000..2a69993 --- /dev/null +++ b/addons/AdmobPlugin/model/MediationNetwork.gd.uid @@ -0,0 +1 @@ +uid://devoemkicbhtc diff --git a/addons/AdmobPlugin/model/NetworkExtras.gd b/addons/AdmobPlugin/model/NetworkExtras.gd new file mode 100644 index 0000000..ee9dfe2 --- /dev/null +++ b/addons/AdmobPlugin/model/NetworkExtras.gd @@ -0,0 +1,32 @@ +# +# © 2024-present https://github.com/cengiz-pz +# +class_name NetworkExtras +extends Resource + +const NETWORK_TAG_PROPERTY := &"network_tag" +const EXTRAS_PROPERTY := &"extras" + +## Identifies the target ad network for the extras. +@export var network_flag: MediationNetwork.Flag = MediationNetwork.Flag.APPLOVIN + +## Dictionary holding one or more extras. Only keys of type String are allowed. Values can be String, int, or bool. +@export var extras: Dictionary + + +func get_raw_data() -> Dictionary: + var __network: MediationNetwork = MediationNetwork.get_by_flag(network_flag) + var __raw_data: Dictionary + + __raw_data = {NETWORK_TAG_PROPERTY: __network.tag, EXTRAS_PROPERTY: extras} + + return __raw_data + + +static func build_raw_data_array(a_network_extras: Array[NetworkExtras]) -> Array: + var __raw_data_array: Array[Dictionary] = [] + + for __network_extras in a_network_extras: + __raw_data_array.append(__network_extras.get_raw_data()) + + return __raw_data_array diff --git a/addons/AdmobPlugin/model/NetworkExtras.gd.uid b/addons/AdmobPlugin/model/NetworkExtras.gd.uid new file mode 100644 index 0000000..eccbda0 --- /dev/null +++ b/addons/AdmobPlugin/model/NetworkExtras.gd.uid @@ -0,0 +1 @@ +uid://cxw2idjnk2d72 diff --git a/addons/AdmobPlugin/model/NetworkPrivacySettings.gd b/addons/AdmobPlugin/model/NetworkPrivacySettings.gd new file mode 100644 index 0000000..f4d5202 --- /dev/null +++ b/addons/AdmobPlugin/model/NetworkPrivacySettings.gd @@ -0,0 +1,45 @@ +# +# © 2024-present https://github.com/cengiz-pz +# +class_name NetworkPrivacySettings +extends RefCounted + +const HAS_GDPR_CONSENT_PROPERTY := &"has_gdpr_consent" +const IS_AGE_RESTRICTED_USER_PROPERTY := &"is_age_restricted_user" +const HAS_CCPA_SALE_CONSENT_PROPERTY := &"has_ccpa_sale_consent" +const ENABLED_NETWORKS_PROPERTY := &"enabled_networks" + +var _data: Dictionary + + +func _init() -> void: + _data = { + ENABLED_NETWORKS_PROPERTY: [], + } + + +func set_has_gdpr_consent(a_value: bool) -> NetworkPrivacySettings: + _data[HAS_GDPR_CONSENT_PROPERTY] = a_value + return self + + +func set_is_age_restricted_user(a_value: bool) -> NetworkPrivacySettings: + _data[IS_AGE_RESTRICTED_USER_PROPERTY] = a_value + return self + + +func set_has_ccpa_sale_consent(a_value: bool) -> NetworkPrivacySettings: + _data[HAS_CCPA_SALE_CONSENT_PROPERTY] = a_value + return self + + +func set_enabled_networks(a_value: Array[String]) -> NetworkPrivacySettings: + if a_value == null: + _data[ENABLED_NETWORKS_PROPERTY] = [] + else: + _data[ENABLED_NETWORKS_PROPERTY] = a_value + return self + + +func get_raw_data() -> Dictionary: + return _data diff --git a/addons/AdmobPlugin/model/NetworkPrivacySettings.gd.uid b/addons/AdmobPlugin/model/NetworkPrivacySettings.gd.uid new file mode 100644 index 0000000..9644ce5 --- /dev/null +++ b/addons/AdmobPlugin/model/NetworkPrivacySettings.gd.uid @@ -0,0 +1 @@ +uid://b42fjtxon0ynl diff --git a/addons/AdmobPlugin/model/ResponseInfo.gd b/addons/AdmobPlugin/model/ResponseInfo.gd new file mode 100644 index 0000000..770466c --- /dev/null +++ b/addons/AdmobPlugin/model/ResponseInfo.gd @@ -0,0 +1,46 @@ +# +# © 2024-present https://github.com/cengiz-pz +# +class_name ResponseInfo +extends RefCounted + +const ADAPTER_RESPONSES_PROPERTY := &"adapter_responses" +const LOADED_ADAPTER_RESPONSE_PROPERTY := &"loaded_adapter_response" +const ADAPTER_CLASS_NAME_PROPERTY := &"adapter_class_name" +const NETWORK_TAG_PROPERTY := &"network_tag" +const RESPONSE_ID_PROPERTY := &"response_id" + +var _data: Dictionary + + +func _init(a_data: Dictionary): + _data = a_data + + +func get_adapter_responses() -> Array[AdapterResponseInfo]: + var __responses: Array[AdapterResponseInfo] = [] + + for __response_dict in _data[ADAPTER_RESPONSES_PROPERTY]: + __responses.append(AdapterResponseInfo.new(__response_dict)) + + return __responses + + +func get_loaded_adapter_response() -> AdapterResponseInfo: + return ( + AdapterResponseInfo.new(_data[LOADED_ADAPTER_RESPONSE_PROPERTY]) + if _data.has(LOADED_ADAPTER_RESPONSE_PROPERTY) + else null + ) + + +func get_adapter_class_name() -> String: + return _data[ADAPTER_CLASS_NAME_PROPERTY] if _data.has(ADAPTER_CLASS_NAME_PROPERTY) else "" + + +func get_network_tag() -> String: + return _data[NETWORK_TAG_PROPERTY] if _data.has(NETWORK_TAG_PROPERTY) else "" + + +func get_response_id() -> String: + return _data[RESPONSE_ID_PROPERTY] if _data.has(RESPONSE_ID_PROPERTY) else "" diff --git a/addons/AdmobPlugin/model/ResponseInfo.gd.uid b/addons/AdmobPlugin/model/ResponseInfo.gd.uid new file mode 100644 index 0000000..2b4f71a --- /dev/null +++ b/addons/AdmobPlugin/model/ResponseInfo.gd.uid @@ -0,0 +1 @@ +uid://b3jib8n1rbnyv diff --git a/addons/AdmobPlugin/model/RewardItem.gd b/addons/AdmobPlugin/model/RewardItem.gd new file mode 100644 index 0000000..0d3ab89 --- /dev/null +++ b/addons/AdmobPlugin/model/RewardItem.gd @@ -0,0 +1,22 @@ +# +# © 2024-present https://github.com/cengiz-pz +# +class_name RewardItem +extends RefCounted + +const AMOUNT_PROPERTY := &"amount" +const TYPE_PROPERTY := &"type" + +var _data: Dictionary + + +func _init(a_data: Dictionary): + _data = a_data + + +func get_amount() -> int: + return _data[AMOUNT_PROPERTY] + + +func get_type() -> String: + return _data[TYPE_PROPERTY] diff --git a/addons/AdmobPlugin/model/RewardItem.gd.uid b/addons/AdmobPlugin/model/RewardItem.gd.uid new file mode 100644 index 0000000..3f2c9a0 --- /dev/null +++ b/addons/AdmobPlugin/model/RewardItem.gd.uid @@ -0,0 +1 @@ +uid://cf0fkl05bgokb diff --git a/addons/AdmobPlugin/model/UserConsent.gd b/addons/AdmobPlugin/model/UserConsent.gd new file mode 100644 index 0000000..bb835f4 --- /dev/null +++ b/addons/AdmobPlugin/model/UserConsent.gd @@ -0,0 +1,33 @@ +# +# © 2024-present https://github.com/cengiz-pz +# +class_name UserConsent +extends RefCounted + +enum Status { + UNKNOWN, + NOT_REQUIRED, + REQUIRED, + OBTAINED, +} + +var status: Status + + +func _init(a_status_string: String = "") -> void: + if a_status_string == "": + status = Status.UNKNOWN + else: + status = string_to_status(a_status_string) + + +func to_status_string() -> String: + return Status.keys()[status] + + +static func status_to_string(a_status: Status) -> String: + return Status.keys()[a_status] + + +static func string_to_status(a_string: String) -> Status: + return Status.get(a_string) diff --git a/addons/AdmobPlugin/model/UserConsent.gd.uid b/addons/AdmobPlugin/model/UserConsent.gd.uid new file mode 100644 index 0000000..6d053d2 --- /dev/null +++ b/addons/AdmobPlugin/model/UserConsent.gd.uid @@ -0,0 +1 @@ +uid://dcbs2sidgjetb diff --git a/addons/AdmobPlugin/plugin.cfg b/addons/AdmobPlugin/plugin.cfg new file mode 100644 index 0000000..3fc5d4d --- /dev/null +++ b/addons/AdmobPlugin/plugin.cfg @@ -0,0 +1,10 @@ +; +; © 2024-present https://github.com/cengiz-pz +; +[plugin] + +name="Admob" +description="Allows displaying of AdMob ads." +author="Cengiz" +version="7.0" +script="AdmobPlugin.gd" diff --git a/addons/GMPShared/GmpLogger.gd b/addons/GMPShared/GmpLogger.gd new file mode 100644 index 0000000..f782962 --- /dev/null +++ b/addons/GMPShared/GmpLogger.gd @@ -0,0 +1,29 @@ +# +# © 2026-present https://github.com/<> +# + +class_name GmpLogger extends Object + + +static func log_error(a_description: String) -> void: + push_error("%s" % [a_description]) + + +static func log_warn(a_description: String) -> void: + push_warning("%s" % [a_description]) + + +static func log_info(a_description: String) -> void: + print_rich("[color=lime]INFO: %s[/color]" % [a_description]) + + +static func error(a_plugin_name: String, a_description: String) -> void: + log_error("%s: %s" % [a_plugin_name, a_description]) + + +static func warn(a_plugin_name: String, a_description: String) -> void: + log_warn("%s: %s" % [a_plugin_name, a_description]) + + +static func info(a_plugin_name: String, a_description: String) -> void: + log_info("%s: %s" % [a_plugin_name, a_description]) diff --git a/addons/GMPShared/GmpLogger.gd.uid b/addons/GMPShared/GmpLogger.gd.uid new file mode 100644 index 0000000..07833bd --- /dev/null +++ b/addons/GMPShared/GmpLogger.gd.uid @@ -0,0 +1 @@ +uid://78axiycs2dmy diff --git a/addons/GMPShared/SpmDependency.gd b/addons/GMPShared/SpmDependency.gd new file mode 100644 index 0000000..e5228f5 --- /dev/null +++ b/addons/GMPShared/SpmDependency.gd @@ -0,0 +1,48 @@ +# +# © 2026-present https://github.com/<> +# + +class_name SpmDependency extends RefCounted + +const URL_PROPERTY := &"url" +const VERSION_PROPERTY := &"version" +const PRODUCTS_PROPERTY := &"products" + +var _data: Dictionary + + +func _init(a_data: Dictionary = {}): + _data = a_data + + +func get_url() -> String: + return _data[URL_PROPERTY] + + +func set_url(a_url: String) -> void: + _data[URL_PROPERTY] = a_url + + +func get_version() -> String: + return _data[VERSION_PROPERTY] + + +func set_version(a_version: String) -> void: + _data[VERSION_PROPERTY] = a_version + + +func get_products() -> Array: + return _data[PRODUCTS_PROPERTY] + + +func set_products(a_products: Array) -> void: + _data[PRODUCTS_PROPERTY] = a_products + + +func format_to_string() -> String: + var __result: String = "" + + for __product in get_products(): + __result += "%s@%s (%s) " % [__product, get_version(), get_url()] + + return __result diff --git a/addons/GMPShared/SpmDependency.gd.uid b/addons/GMPShared/SpmDependency.gd.uid new file mode 100644 index 0000000..c09cc1d --- /dev/null +++ b/addons/GMPShared/SpmDependency.gd.uid @@ -0,0 +1 @@ +uid://dxpvam6fnixki diff --git a/addons/agent_tools/LICENSE b/addons/agent_tools/LICENSE new file mode 100644 index 0000000..96f6493 --- /dev/null +++ b/addons/agent_tools/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Blake Bukowsky + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/addons/agent_tools/README.md b/addons/agent_tools/README.md new file mode 100644 index 0000000..15263d1 --- /dev/null +++ b/addons/agent_tools/README.md @@ -0,0 +1,43 @@ +# Agent Tools + +Godot editor plugin that exposes **43 tools across 12 namespaces** (scene edits, signal wiring, resource creation, reference validation, animation, headless test runs, etc.) over MCP so coding agents can operate through the editor's real APIs instead of hand-editing `.tscn` / `.tres` / `project.godot` as text. + +Works with any MCP-capable client — Claude Code, Claude Desktop, Cursor, Cline, Windsurf, Continue, Zed. + +## Quick start + +1. Enable this plugin: *Project → Project Settings → Plugins* → tick **Agent Tools**. +2. Confirm the Output panel shows `[agent_tools] listening on 127.0.0.1:9920`. +3. Configure your MCP client to run the bridge — see the [full setup guide](https://github.com/BlakeBukowsky/GodotTools#configure-your-agent) for per-client config paths. + +Simplest user-scoped config (for Claude Code, Cursor, Windsurf, etc. — different config file per client): + +```json +{ + "mcpServers": { + "godot-agent-tools": { + "command": "npx", + "args": ["-y", "godot-agent-tools-mcp"] + } + } +} +``` + +Full documentation, troubleshooting, and tool catalog: **https://github.com/BlakeBukowsky/GodotTools** + +## Raw TCP protocol (if you want to skip MCP) + +Line-delimited JSON-RPC on `127.0.0.1:9920`: + +```json +{"id": 1, "method": "scene.current", "params": {}} +``` + +Response: +```json +{"id": 1, "result": {"open": false}} +``` + +## License + +MIT — see [LICENSE](LICENSE). diff --git a/addons/agent_tools/headless/driver.gd b/addons/agent_tools/headless/driver.gd new file mode 100644 index 0000000..2a3af94 --- /dev/null +++ b/addons/agent_tools/headless/driver.gd @@ -0,0 +1,187 @@ +# Auto-run driver for run.scene_headless (input simulation, multi-screenshot, +# state dump, deterministic seed). Reads a per-call config from +# res://.godot/agent_tools/headless/config.json, instances the target scene, +# injects scripted input per frame, captures screenshots at specified frames, +# optionally writes a final-state JSON dump, then quits. + +@tool +extends Node + +const CONFIG_PATH := "res://.godot/agent_tools/headless/config.json" +const Coerce := preload("res://addons/agent_tools/tools/_coerce.gd") + +var _input_script: Array = [] +var _screenshots: Array = [] # [{frame, path}, ...] +var _state_dump: bool = false +var _state_dump_path: String = "" +var _quit_frame: int = 120 +var _frame: int = 0 +var _target_inst: Node = null + + +func _ready() -> void: + var f := FileAccess.open(CONFIG_PATH, FileAccess.READ) + if f == null: + push_error("[headless_driver] config not found at %s" % CONFIG_PATH) + get_tree().quit(2) + return + var parsed = JSON.parse_string(f.get_as_text()) + f.close() + if typeof(parsed) != TYPE_DICTIONARY: + push_error("[headless_driver] malformed config") + get_tree().quit(2) + return + + # Seed the global RNG before instancing so the target scene's randomness + # (randi/randf in _ready, etc.) is deterministic per-seed. + if parsed.has("seed"): + seed(int(parsed.seed)) + + _input_script = parsed.get("input_script", []) + _screenshots = parsed.get("screenshots", []) + _state_dump = bool(parsed.get("state_dump", false)) + _state_dump_path = str(parsed.get("state_dump_path", "")) + _quit_frame = int(parsed.get("quit_frame", 120)) + var target_scene_path: String = parsed.get("target_scene", "") + + if target_scene_path == "": + push_error("[headless_driver] config missing 'target_scene'") + get_tree().quit(2) + return + var packed = load(target_scene_path) + if packed == null: + push_error("[headless_driver] failed to load target %s" % target_scene_path) + get_tree().quit(2) + return + _target_inst = packed.instantiate() + add_child(_target_inst) + print("[headless_driver] driving %s for %d frames, %d events, %d screenshots%s" % + [target_scene_path, _quit_frame, _input_script.size(), _screenshots.size(), + ", state_dump=on" if _state_dump else ""]) + + +func _process(_delta: float) -> void: + # 1. Inject input events scheduled for this frame. + for spec in _input_script: + if int(spec.get("frame", -1)) == _frame: + _inject(spec) + + # 2. Capture screenshots scheduled for this frame. + for shot in _screenshots: + if int(shot.get("frame", -1)) == _frame: + _capture(str(shot.get("path", ""))) + + if _frame >= _quit_frame: + if _state_dump and _state_dump_path != "": + _write_state_dump() + print("[headless_driver] done at frame %d" % _frame) + get_tree().quit(0) + return + _frame += 1 + + +func _inject(spec: Dictionary) -> void: + match spec.get("type", ""): + "action_press": + Input.action_press(spec.get("action", ""), float(spec.get("strength", 1.0))) + "action_release": + Input.action_release(spec.get("action", "")) + "action_tap": + Input.action_press(spec.get("action", ""), float(spec.get("strength", 1.0))) + Input.action_release(spec.get("action", "")) + "key": + var ek := InputEventKey.new() + var kc = spec.get("keycode", 0) + if kc is String: + ek.physical_keycode = OS.find_keycode_from_string(kc) + else: + ek.physical_keycode = int(kc) + ek.pressed = bool(spec.get("pressed", true)) + Input.parse_input_event(ek) + "mouse_click": + var pos = spec.get("position", [0, 0]) + var v := Vector2(pos[0], pos[1]) + var button := int(spec.get("button", 1)) + var down := InputEventMouseButton.new() + down.button_index = button + down.pressed = true + down.position = v + down.global_position = v + Input.parse_input_event(down) + var up := InputEventMouseButton.new() + up.button_index = button + up.pressed = false + up.position = v + up.global_position = v + Input.parse_input_event(up) + "mouse_motion": + var pos = spec.get("position", [0, 0]) + var em := InputEventMouseMotion.new() + em.position = Vector2(pos[0], pos[1]) + em.global_position = em.position + Input.parse_input_event(em) + _: + push_warning("[headless_driver] unknown event type: %s" % spec.get("type", "")) + + +func _capture(path: String) -> void: + if path == "": + return + var vp := get_tree().root.get_viewport() + if vp == null: + push_warning("[headless_driver] no root viewport") + return + var tex := vp.get_texture() + if tex == null: + push_warning("[headless_driver] viewport has no texture (running under dummy renderer?)") + return + var img := tex.get_image() + if img == null or img.is_empty(): + push_warning("[headless_driver] failed to capture image at frame %d" % _frame) + return + var dir := path.get_base_dir() + if not DirAccess.dir_exists_absolute(dir): + DirAccess.make_dir_recursive_absolute(dir) + var err := img.save_png(path) + if err != OK: + push_warning("[headless_driver] save_png failed: %d" % err) + else: + print("[headless_driver] frame %d -> %s" % [_frame, path]) + + +func _write_state_dump() -> void: + if _target_inst == null: + return + var snapshot := _node_snapshot(_target_inst, _target_inst) + var f := FileAccess.open(_state_dump_path, FileAccess.WRITE) + if f == null: + push_warning("[headless_driver] failed to open state dump path: %s" % _state_dump_path) + return + f.store_string(JSON.stringify(snapshot, " ")) + f.close() + print("[headless_driver] state dump -> %s" % _state_dump_path) + + +# Mirrors scene.inspect's shape, plus a small selection of commonly-useful +# properties. Kept narrow on purpose — full get_property_list() dumps balloon. +static func _node_snapshot(node: Node, scene_root: Node) -> Dictionary: + var d := { + "name": String(node.name), + "class": node.get_class(), + "node_path": "." if node == scene_root else String(scene_root.get_path_to(node)), + "script": "", + "properties": {}, + "children": [], + } + var scr := node.get_script() as Script + if scr: + d.script = scr.resource_path + # Common props worth verifying, if present on this node type. + for prop_name in ["visible", "position", "global_position", "size", "text", + "value", "modulate", "color", "rotation", "scale", "disabled", "pressed"]: + if prop_name in node: + d.properties[prop_name] = Coerce.to_json(node.get(prop_name)) + for child in node.get_children(): + if child.owner == scene_root or child == scene_root: + d.children.append(_node_snapshot(child, scene_root)) + return d diff --git a/addons/agent_tools/headless/driver.gd.uid b/addons/agent_tools/headless/driver.gd.uid new file mode 100644 index 0000000..5277b9d --- /dev/null +++ b/addons/agent_tools/headless/driver.gd.uid @@ -0,0 +1 @@ +uid://iko41fhoo3mm diff --git a/addons/agent_tools/headless/driver.tscn b/addons/agent_tools/headless/driver.tscn new file mode 100644 index 0000000..9455535 --- /dev/null +++ b/addons/agent_tools/headless/driver.tscn @@ -0,0 +1,6 @@ +[gd_scene load_steps=2 format=3] + +[ext_resource type="Script" path="res://addons/agent_tools/headless/driver.gd" id="1"] + +[node name="HeadlessDriver" type="Node"] +script = ExtResource("1") diff --git a/addons/agent_tools/icon.png b/addons/agent_tools/icon.png new file mode 100644 index 0000000..17f50ca Binary files /dev/null and b/addons/agent_tools/icon.png differ diff --git a/addons/agent_tools/icon.png.import b/addons/agent_tools/icon.png.import new file mode 100644 index 0000000..a0275e5 --- /dev/null +++ b/addons/agent_tools/icon.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://d2wklut1fj5ed" +path="res://.godot/imported/icon.png-381bc5920e04d060db359f1234d8c8b0.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/agent_tools/icon.png" +dest_files=["res://.godot/imported/icon.png-381bc5920e04d060db359f1234d8c8b0.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/addons/agent_tools/plugin.cfg b/addons/agent_tools/plugin.cfg new file mode 100644 index 0000000..f4cd51a --- /dev/null +++ b/addons/agent_tools/plugin.cfg @@ -0,0 +1,9 @@ +[plugin] + +name="Agent Tools" +description="70 tools + 6 MCP Resources for coding agents to edit scenes, wire signals, create resources, and more through Godot's real APIs instead of hand-editing .tscn/.tres files. If you haven't already, configure your MCP client (Claude Code, Cursor, Cline, etc.) per the README: https://github.com/BlakeBukowsky/GodotTools" +author="Blake Bukowsky" +version="0.3.1" +license="MIT" +script="plugin.gd" +icon="icon.png" \ No newline at end of file diff --git a/addons/agent_tools/plugin.gd b/addons/agent_tools/plugin.gd new file mode 100644 index 0000000..4d79dd8 --- /dev/null +++ b/addons/agent_tools/plugin.gd @@ -0,0 +1,132 @@ +@tool +extends EditorPlugin + +const Server := preload("res://addons/agent_tools/server.gd") +const Registry := preload("res://addons/agent_tools/registry.gd") + +const DEFAULT_PORT := 9920 +const PORT_RANGE_END := 9929 # scan 9920..9929 inclusive +const PORT_SETTING := "agent_tools/port" +const BRIDGE_AUTOLOAD_NAME := "_MCPGameBridge" +const BRIDGE_AUTOLOAD_PATH := "res://addons/agent_tools/runtime/game_bridge.gd" + +const DEFAULT_INTERFACE := "127.0.0.1" +const INTERFACE_SETTING := "agent_tools/interface" + +var _server: Server +var _registry: Registry +var _bound_port: int = -1 +var _session_file_path: String = "" + + +func _enter_tree() -> void: + _registry = Registry.new() + _server = Server.new(_registry) + add_child(_server) + + # Interface resolution + # Defaults to 127.0.0.1, override with project.godot agent_tools/interface + var forced_interface := DEFAULT_INTERFACE + if ProjectSettings.has_setting(INTERFACE_SETTING): + forced_interface = ProjectSettings.get_setting(INTERFACE_SETTING) + + # Port resolution: + # 1. If 'agent_tools/port' is set in project.godot, honor it strictly (fail if busy). + # 2. Otherwise try DEFAULT_PORT..PORT_RANGE_END and use the first free one. + # This lets multiple Godot editors coexist: first-to-start takes 9920, second takes 9921, etc. + var forced_port := -1 + if ProjectSettings.has_setting(PORT_SETTING): + forced_port = int(ProjectSettings.get_setting(PORT_SETTING)) + + if forced_port != -1: + if _server.start(forced_port, forced_interface): + _bound_port = forced_port + else: + push_error("[agent_tools] configured port %s:%d is busy. Either close the other process or change 'agent_tools/port' in project.godot." % [forced_interface, forced_port]) + else: + for p in range(DEFAULT_PORT, PORT_RANGE_END + 1): + if _server.start(p, forced_interface): + _bound_port = p + break + if _bound_port == -1: + push_error("[agent_tools] all ports %d-%d are busy on %s. Close other Godot editors running agent_tools, or set 'agent_tools/port' in project.godot." % [DEFAULT_PORT, PORT_RANGE_END, forced_interface]) + + if _bound_port != -1: + print("[agent_tools] listening on %s:%d" % [forced_interface, _bound_port]) + print("[agent_tools] If you haven't already, configure your MCP client (Claude Code, Cursor, Cline, etc.) to use these tools.") + print("[agent_tools] Setup guide: https://github.com/BlakeBukowsky/GodotTools#configure-your-agent") + _write_session_registry() + + # Register the runtime bridge autoload so editor.game_screenshot / logs.read + # can reach the running game. add_autoload_singleton() writes an entry to + # project.godot; remove_autoload_singleton() in _exit_tree cleans it up. + if not ProjectSettings.has_setting("autoload/" + BRIDGE_AUTOLOAD_NAME): + add_autoload_singleton(BRIDGE_AUTOLOAD_NAME, BRIDGE_AUTOLOAD_PATH) + + +func _exit_tree() -> void: + _remove_session_registry() + if _server: + _server.stop() + _server.queue_free() + _server = null + _registry = null + _bound_port = -1 + if ProjectSettings.has_setting("autoload/" + BRIDGE_AUTOLOAD_NAME): + remove_autoload_singleton(BRIDGE_AUTOLOAD_NAME) + + +# Session registry is a per-PID JSON file under a cross-project, user-scoped +# directory. The MCP shim reads these on startup so `session.list` can discover +# every running Godot editor + which port to talk to each. +func _write_session_registry() -> void: + var dir_path := _session_registry_dir() + if dir_path == "": + return + if not DirAccess.dir_exists_absolute(dir_path): + var derr := DirAccess.make_dir_recursive_absolute(dir_path) + if derr != OK: + push_warning("[agent_tools] could not create session registry dir: %s" % dir_path) + return + var pid := OS.get_process_id() + _session_file_path = dir_path.path_join("%d.json" % pid) + + var project_name := "" + if ProjectSettings.has_setting("application/config/name"): + project_name = str(ProjectSettings.get_setting("application/config/name")) + var version := Engine.get_version_info() + + var entry := { + "pid": pid, + "port": _bound_port, + "project_path": ProjectSettings.globalize_path("res://"), + "project_name": project_name, + "godot_version": "%d.%d.%d-%s" % [version.major, version.minor, version.patch, version.status], + "started_at_unix": int(Time.get_unix_time_from_system()), + } + var f := FileAccess.open(_session_file_path, FileAccess.WRITE) + if f == null: + push_warning("[agent_tools] could not write session file: %s" % _session_file_path) + return + f.store_string(JSON.stringify(entry, " ")) + f.close() + + +func _remove_session_registry() -> void: + if _session_file_path == "": + return + if FileAccess.file_exists(_session_file_path): + @warning_ignore("return_value_discarded") + DirAccess.remove_absolute(_session_file_path) + _session_file_path = "" + + +# Cross-project location: /.godot-agent-tools/sessions/. Chosen to +# sit outside any single project's res:// or user:// tree. +func _session_registry_dir() -> String: + var home := OS.get_environment("USERPROFILE") + if home == "": + home = OS.get_environment("HOME") + if home == "": + return "" + return home.path_join(".godot-agent-tools").path_join("sessions") diff --git a/addons/agent_tools/plugin.gd.uid b/addons/agent_tools/plugin.gd.uid new file mode 100644 index 0000000..1bd0422 --- /dev/null +++ b/addons/agent_tools/plugin.gd.uid @@ -0,0 +1 @@ +uid://bkvb64lq51xyg diff --git a/addons/agent_tools/registry.gd b/addons/agent_tools/registry.gd new file mode 100644 index 0000000..1d95a5f --- /dev/null +++ b/addons/agent_tools/registry.gd @@ -0,0 +1,129 @@ +@tool +extends RefCounted + +const SceneTools := preload("res://addons/agent_tools/tools/scene_tools.gd") +const SignalTools := preload("res://addons/agent_tools/tools/signal_tools.gd") +const ScriptTools := preload("res://addons/agent_tools/tools/script_tools.gd") +const ResourceTools := preload("res://addons/agent_tools/tools/resource_tools.gd") +const RefsTools := preload("res://addons/agent_tools/tools/refs_tools.gd") +const ProjectTools := preload("res://addons/agent_tools/tools/project_tools.gd") +const EditorTools := preload("res://addons/agent_tools/tools/editor_tools.gd") +const DocsTools := preload("res://addons/agent_tools/tools/docs_tools.gd") +const InputTools := preload("res://addons/agent_tools/tools/input_tools.gd") +const RunTools := preload("res://addons/agent_tools/tools/run_tools.gd") +const FsTools := preload("res://addons/agent_tools/tools/fs_tools.gd") +const AnimationTools := preload("res://addons/agent_tools/tools/animation_tools.gd") +const UserFsTools := preload("res://addons/agent_tools/tools/user_fs_tools.gd") +const PerformanceTools := preload("res://addons/agent_tools/tools/performance_tools.gd") +const TestTools := preload("res://addons/agent_tools/tools/test_tools.gd") +const ClientTools := preload("res://addons/agent_tools/tools/client_tools.gd") +const PhysicsTools := preload("res://addons/agent_tools/tools/physics_tools.gd") +const ThemeTools := preload("res://addons/agent_tools/tools/theme_tools.gd") + + +# Returns {"data": } on success, {"error": {"code": int, "message": str}} on failure. +func dispatch(method: String, params: Dictionary) -> Dictionary: + match method: + "scene.new": return SceneTools.new_scene(params) + "scene.add_node": return SceneTools.add_node(params) + "scene.instance_packed": return SceneTools.instance_packed(params) + "scene.duplicate_node": return SceneTools.duplicate_node(params) + "scene.remove_node": return SceneTools.remove_node(params) + "scene.reparent": return SceneTools.reparent(params) + "scene.set_property": return SceneTools.set_property(params) + "scene.get_property": return SceneTools.get_property(params) + "scene.call_method": return SceneTools.call_method(params) + "scene.build_tree": return SceneTools.build_tree(params) + "scene.open": return SceneTools.open_scene(params) + "scene.save": return SceneTools.save_scene(params) + "scene.current": return SceneTools.current(params) + "scene.inspect": return SceneTools.inspect(params) + "scene.capture_screenshot": return SceneTools.capture_screenshot(params) + "signal.connect": return SignalTools.connect_signal(params) + "signal.disconnect": return SignalTools.disconnect_signal(params) + "signal.list": return SignalTools.list_signals(params) + "script.create": return ScriptTools.create(params) + "script.attach": return ScriptTools.attach(params) + "script.patch": return ScriptTools.patch(params) + "resource.create": return ResourceTools.create(params) + "resource.set_property": return ResourceTools.set_property(params) + "resource.call_method": return ResourceTools.call_method(params) + "refs.validate_project": return RefsTools.validate_project(params) + "refs.find_usages": return RefsTools.find_usages(params) + "refs.rename": return RefsTools.rename(params) + "refs.rename_class": return RefsTools.rename_class(params) + "project.get_setting": return ProjectTools.get_setting(params) + "project.set_setting": return ProjectTools.set_setting(params) + "autoload.add": return ProjectTools.autoload_add(params) + "autoload.remove": return ProjectTools.autoload_remove(params) + "autoload.list": return ProjectTools.autoload_list(params) + "editor.reload_filesystem": return EditorTools.reload_filesystem(params) + "editor.save_all_scenes": return EditorTools.save_all_scenes(params) + "editor.state": return EditorTools.state(params) + "editor.selection_get": return EditorTools.selection_get(params) + "editor.selection_set": return EditorTools.selection_set(params) + "editor.game_screenshot": return EditorTools.game_screenshot(params) + "logs.read": return EditorTools.logs_read(params) + "logs.clear": return EditorTools.logs_clear(params) + "docs.class_ref": return DocsTools.class_ref(params) + "input_map.add_action": return InputTools.add_action(params) + "input_map.add_event": return InputTools.add_event(params) + "input_map.list": return InputTools.list_actions(params) + "input_map.remove_action": return InputTools.remove_action(params) + "input_map.remove_event": return InputTools.remove_event(params) + "run.scene_headless": return RunTools.scene_headless(params) + "fs.list": return FsTools.list(params) + "fs.read_text": return FsTools.read_text(params) + "fs.write_text": return FsTools.write_text(params) + "user_fs.read": return UserFsTools.read(params) + "user_fs.list": return UserFsTools.list(params) + "animation.list": return AnimationTools.list_animations(params) + "animation.add_animation": return AnimationTools.add_animation(params) + "animation.remove_animation": return AnimationTools.remove_animation(params) + "animation.add_value_track": return AnimationTools.add_value_track(params) + "performance.monitors": return PerformanceTools.monitors(params) + "test.run": return TestTools.run(params) + "client.list": return ClientTools.list_clients(params) + "client.configure": return ClientTools.configure(params) + "client.remove": return ClientTools.remove(params) + "physics.autofit_collision_shape_2d": return PhysicsTools.autofit_collision_shape_2d(params) + "theme.set_color": return ThemeTools.set_color(params) + "theme.set_constant": return ThemeTools.set_constant(params) + "theme.set_font_size": return ThemeTools.set_font_size(params) + "theme.set_stylebox_flat": return ThemeTools.set_stylebox_flat(params) + "batch.execute": return batch_execute(params) + _: + return {"error": {"code": -32601, "message": "method not found: %s" % method}} + + +# batch.execute — run multiple tool calls in one round trip. +# Params: {calls: [{method, params?}, ...], stop_on_error?: false} +# Returns: {results: [{method, ok, data|error, index}, ...]} +func batch_execute(params: Dictionary) -> Dictionary: + var calls: Array = params.get("calls", []) + var stop_on_error: bool = params.get("stop_on_error", false) + if calls.is_empty(): + return {"error": {"code": -32602, "message": "missing 'calls' (array of {method, params})"}} + + var results: Array = [] + for i in calls.size(): + var entry = calls[i] + if typeof(entry) != TYPE_DICTIONARY or not entry.has("method"): + results.append({"index": i, "ok": false, "error": "each call needs {method, params?}"}) + if stop_on_error: + break + continue + var sub_method: String = entry.method + var sub_params: Dictionary = entry.get("params", {}) + var sub_result: Dictionary = dispatch(sub_method, sub_params) + var row := {"index": i, "method": sub_method} + if sub_result.has("error"): + row["ok"] = false + row["error"] = sub_result.error + else: + row["ok"] = true + row["data"] = sub_result.get("data") + results.append(row) + if stop_on_error and not row.ok: + break + return {"data": {"results": results, "count": results.size(), "requested": calls.size()}} diff --git a/addons/agent_tools/registry.gd.uid b/addons/agent_tools/registry.gd.uid new file mode 100644 index 0000000..dd7f4d7 --- /dev/null +++ b/addons/agent_tools/registry.gd.uid @@ -0,0 +1 @@ +uid://b5pe4omwjkjgo diff --git a/addons/agent_tools/runtime/bridge_logger.gd b/addons/agent_tools/runtime/bridge_logger.gd new file mode 100644 index 0000000..94b3517 --- /dev/null +++ b/addons/agent_tools/runtime/bridge_logger.gd @@ -0,0 +1,31 @@ +# Custom Logger that forwards every captured message to the game bridge's +# log buffer. Has to live in its own top-level file — nested classes don't +# reliably register Logger's virtual overrides (_log_error / _log_message). + +extends Logger + +var _bridge: Node + + +func _init(bridge: Node) -> void: + _bridge = bridge + + +func _log_error(function: String, file: String, line: int, code: String, rationale: String, editor_notify: bool, error_type: int, script_backtraces: Array) -> void: + # error_type is Logger.ErrorType: 0=ERROR, 1=WARNING, 2=SCRIPT, 3=SHADER. + var level: String + match error_type: + 1: level = "WARNING" + 2: level = "SCRIPT" + 3: level = "SHADER" + _: level = "ERROR" + var msg: String = rationale if rationale != "" else code + if file != "" and line > 0: + msg += " (at %s:%d in %s)" % [file, line, function] + if _bridge and _bridge.has_method("log_entry"): + _bridge.log_entry(level, msg) + + +func _log_message(message: String, error: bool) -> void: + if _bridge and _bridge.has_method("log_entry"): + _bridge.log_entry("ERROR" if error else "INFO", message.strip_edges()) diff --git a/addons/agent_tools/runtime/bridge_logger.gd.uid b/addons/agent_tools/runtime/bridge_logger.gd.uid new file mode 100644 index 0000000..ea559a9 --- /dev/null +++ b/addons/agent_tools/runtime/bridge_logger.gd.uid @@ -0,0 +1 @@ +uid://wosqs8dlh1vx diff --git a/addons/agent_tools/runtime/game_bridge.gd b/addons/agent_tools/runtime/game_bridge.gd new file mode 100644 index 0000000..0065bf4 --- /dev/null +++ b/addons/agent_tools/runtime/game_bridge.gd @@ -0,0 +1,134 @@ +# MCPGameBridge — runtime autoload registered by the Agent Tools plugin. +# +# Bridges the editor-side agent tools to the *running* game via a file-based +# request/response channel under res://.godot/agent_tools/bridge/. Currently: +# - screenshot: capture the running viewport, save to a path +# - logs: append print/push_error/push_warning output to a buffer +# +# This script ONLY runs in the game process (checks Engine.is_editor_hint() +# on startup and disables itself in the editor). The editor plugin registers +# it via add_autoload_singleton() on _enter_tree and removes it on _exit_tree. + +extends Node + +const BridgeLogger := preload("res://addons/agent_tools/runtime/bridge_logger.gd") +const BRIDGE_DIR := "res://.godot/agent_tools/bridge" +const REQUEST_PATH := BRIDGE_DIR + "/request.json" +const RESPONSE_PATH := BRIDGE_DIR + "/response.json" +const LOGS_PATH := BRIDGE_DIR + "/logs.json" +const POLL_INTERVAL_SEC := 0.05 # 20 Hz — cheap enough, responsive enough +const LOG_BUFFER_MAX := 500 + +var _timer: Timer +var _log_buffer: Array = [] +var _initialized := false + + +func _ready() -> void: + if Engine.is_editor_hint(): + # Tool mode — don't do anything inside the editor. The bridge only + # makes sense in the running game. + return + _initialized = true + _ensure_bridge_dir() + # Poll for editor requests on a timer so we don't eat a _process callback. + _timer = Timer.new() + _timer.wait_time = POLL_INTERVAL_SEC + _timer.one_shot = false + _timer.autostart = true + _timer.timeout.connect(_poll_request) + add_child(_timer) + # Attach our custom logger to capture print / push_error / push_warning + # from the game and mirror them to the log buffer file so logs.read can + # pick them up from the editor side. + var logger := BridgeLogger.new(self) + OS.add_logger(logger) + + +func _ensure_bridge_dir() -> void: + if not DirAccess.dir_exists_absolute(BRIDGE_DIR): + @warning_ignore("return_value_discarded") + DirAccess.make_dir_recursive_absolute(BRIDGE_DIR) + + +# Called by MCPBridgeLogger whenever the game writes to stdout/stderr. +func log_entry(level: String, message: String) -> void: + _log_buffer.append({ + "level": level, + "message": message, + "time_ms": Time.get_ticks_msec(), + }) + if _log_buffer.size() > LOG_BUFFER_MAX: + _log_buffer = _log_buffer.slice(_log_buffer.size() - LOG_BUFFER_MAX) + # Flush to disk so editor-side logs.read sees it. Debounced by poll interval + # in practice because each log call flushes but writes are cheap. + _flush_logs() + + +func _flush_logs() -> void: + var f := FileAccess.open(LOGS_PATH, FileAccess.WRITE) + if f == null: + return + f.store_string(JSON.stringify(_log_buffer)) + f.close() + + +func _poll_request() -> void: + if not FileAccess.file_exists(REQUEST_PATH): + return + var f := FileAccess.open(REQUEST_PATH, FileAccess.READ) + if f == null: + return + var body := f.get_as_text() + f.close() + @warning_ignore("return_value_discarded") + DirAccess.remove_absolute(REQUEST_PATH) + + var parsed = JSON.parse_string(body) + if typeof(parsed) != TYPE_DICTIONARY: + _write_response({"id": "", "error": "malformed request"}) + return + var req_id: String = parsed.get("id", "") + match parsed.get("type", ""): + "screenshot": + _handle_screenshot(req_id, parsed) + _: + _write_response({"id": req_id, "error": "unknown request type: %s" % parsed.get("type", "")}) + + +func _handle_screenshot(req_id: String, req: Dictionary) -> void: + var output_path: String = req.get("output", "") + if output_path == "": + _write_response({"id": req_id, "error": "missing 'output'"}) + return + var vp := get_viewport() + if vp == null: + _write_response({"id": req_id, "error": "no viewport available"}) + return + var tex := vp.get_texture() + if tex == null: + _write_response({"id": req_id, "error": "viewport has no texture"}) + return + var img := tex.get_image() + if img == null or img.is_empty(): + _write_response({"id": req_id, "error": "failed to capture image"}) + return + var dir := output_path.get_base_dir() + if not DirAccess.dir_exists_absolute(dir): + @warning_ignore("return_value_discarded") + DirAccess.make_dir_recursive_absolute(dir) + var save_err := img.save_png(output_path) + if save_err != OK: + _write_response({"id": req_id, "error": "save_png failed: %d" % save_err}) + return + _write_response({"id": req_id, "path": output_path}) + + +func _write_response(payload: Dictionary) -> void: + var f := FileAccess.open(RESPONSE_PATH, FileAccess.WRITE) + if f == null: + return + f.store_string(JSON.stringify(payload)) + f.close() + + diff --git a/addons/agent_tools/runtime/game_bridge.gd.uid b/addons/agent_tools/runtime/game_bridge.gd.uid new file mode 100644 index 0000000..4c4ff74 --- /dev/null +++ b/addons/agent_tools/runtime/game_bridge.gd.uid @@ -0,0 +1 @@ +uid://cds186n8uv4f0 diff --git a/addons/agent_tools/server.gd b/addons/agent_tools/server.gd new file mode 100644 index 0000000..f21ab4c --- /dev/null +++ b/addons/agent_tools/server.gd @@ -0,0 +1,125 @@ +@tool +extends Node + +# Line-delimited JSON-RPC over TCP on localhost. +# One request per line: {"id": , "method": "", "params": {...}} +# One response per line: {"id": , "result": } OR {"id": ..., "error": {"code": int, "message": str, "data"?: any}} + +const MAX_MESSAGE_BYTES := 1_048_576 # 1 MiB per line — safety cap + +var _tcp: TCPServer +var _clients: Array = [] # Array of {peer: StreamPeerTCP, buf: PackedByteArray} +var _registry + + +func _init(registry) -> void: + _registry = registry + + +func start(port: int, interface: String = "127.0.0.1") -> bool: + _tcp = TCPServer.new() + var err := _tcp.listen(port, interface) + if err != OK: + _tcp = null + return false + set_process(true) + return true + + +func stop() -> void: + set_process(false) + for c in _clients: + (c.peer as StreamPeerTCP).disconnect_from_host() + _clients.clear() + if _tcp: + _tcp.stop() + _tcp = null + + +func _process(_delta: float) -> void: + if _tcp == null: + return + + while _tcp.is_connection_available(): + var peer := _tcp.take_connection() + _clients.append({"peer": peer, "buf": PackedByteArray()}) + + var still_alive: Array = [] + for c in _clients: + var peer: StreamPeerTCP = c.peer + peer.poll() + if peer.get_status() != StreamPeerTCP.STATUS_CONNECTED: + continue + var available := peer.get_available_bytes() + if available > 0: + var chunk := peer.get_data(available) + if chunk[0] == OK: + c.buf.append_array(chunk[1]) + _drain_messages(c) + if (c.buf as PackedByteArray).size() > MAX_MESSAGE_BYTES: + _send_error(peer, null, -32700, "message exceeds %d bytes" % MAX_MESSAGE_BYTES) + peer.disconnect_from_host() + continue + still_alive.append(c) + _clients = still_alive + + +func _drain_messages(c: Dictionary) -> void: + while true: + var buf: PackedByteArray = c.buf + var nl := buf.find(0x0A) # \n + if nl < 0: + return + var line_bytes := buf.slice(0, nl) + c.buf = buf.slice(nl + 1) + var line := line_bytes.get_string_from_utf8().strip_edges() + if line == "": + continue + _handle_line(c.peer, line) + + +func _handle_line(peer: StreamPeerTCP, line: String) -> void: + var parsed = JSON.parse_string(line) + if typeof(parsed) != TYPE_DICTIONARY: + _send_error(peer, null, -32700, "parse error — expected JSON object") + return + var id = parsed.get("id") + var method: String = parsed.get("method", "") + var params: Dictionary = parsed.get("params", {}) + if method == "": + _send_error(peer, id, -32600, "missing 'method'") + return + + var result: Dictionary = _registry.dispatch(method, params) + # Guard against silent tool-module failures: when a tool's .gd file has a parse + # error, preload() returns null, calls on it produce no output, and dispatch + # returns its default {}. Surface that instead of sending {"result": null}. + if result.is_empty(): + _send_error(peer, id, -32000, + "tool returned empty response — likely a parse error in the tool module. Check Godot's Output panel for the actual error.", + {"method": method}) + elif result.has("error"): + var e: Dictionary = result.error + _send_error(peer, id, e.get("code", -32000), e.get("message", ""), e.get("data")) + elif result.has("data"): + _send_result(peer, id, result.data) + else: + _send_error(peer, id, -32000, + "tool returned malformed response (missing both 'data' and 'error')", + {"method": method}) + + +func _send_result(peer: StreamPeerTCP, id, data) -> void: + _write(peer, {"id": id, "result": data}) + + +func _send_error(peer: StreamPeerTCP, id, code: int, msg: String, data = null) -> void: + var err := {"code": code, "message": msg} + if data != null: + err["data"] = data + _write(peer, {"id": id, "error": err}) + + +func _write(peer: StreamPeerTCP, obj: Dictionary) -> void: + var line := JSON.stringify(obj) + "\n" + peer.put_data(line.to_utf8_buffer()) diff --git a/addons/agent_tools/server.gd.uid b/addons/agent_tools/server.gd.uid new file mode 100644 index 0000000..92ebb45 --- /dev/null +++ b/addons/agent_tools/server.gd.uid @@ -0,0 +1 @@ +uid://pcfsf0ijlm8x diff --git a/addons/agent_tools/tools/_coerce.gd b/addons/agent_tools/tools/_coerce.gd new file mode 100644 index 0000000..93d9c9d --- /dev/null +++ b/addons/agent_tools/tools/_coerce.gd @@ -0,0 +1,306 @@ +@tool +extends RefCounted + +# Shared JSON-to-Godot type coercion for scene.set_property, resource.set_property, +# and any other tool that assigns a user-provided value into a typed property slot. +# +# Contract: +# coerce(value, target_type: int) -> Variant +# +# Returns the coerced value on success. On mismatch, returns a Dictionary with +# a single "_error" key describing the failure — callers check `is Dictionary` +# and forward the message as a tool-level error. +# +# New types go here. Don't re-add `_coerce` to individual tool files; route +# through Coerce.coerce() instead. + +static func coerce(value, target_type: int): + match target_type: + TYPE_BOOL: + # Godot 4 has no `bool()` constructor (it errors at runtime). Convert via + # truthiness, but parse strings explicitly so "false"/"0" don't read as + # truthy the way any non-empty string otherwise would. + if value is String: + return value.strip_edges().to_lower() in ["true", "1", "yes", "on"] + return true if value else false + TYPE_INT: + return int(value) + TYPE_FLOAT: + return float(value) + TYPE_STRING, TYPE_STRING_NAME: + return String(value) + TYPE_NODE_PATH: + return NodePath(String(value)) + TYPE_VECTOR2: + if value is Array and value.size() == 2: + return Vector2(value[0], value[1]) + return {"_error": "Vector2 expects [x, y]"} + TYPE_VECTOR2I: + if value is Array and value.size() == 2: + return Vector2i(int(value[0]), int(value[1])) + return {"_error": "Vector2i expects [x, y]"} + TYPE_VECTOR3: + if value is Array and value.size() == 3: + return Vector3(value[0], value[1], value[2]) + return {"_error": "Vector3 expects [x, y, z]"} + TYPE_VECTOR3I: + if value is Array and value.size() == 3: + return Vector3i(int(value[0]), int(value[1]), int(value[2])) + return {"_error": "Vector3i expects [x, y, z]"} + TYPE_VECTOR4: + if value is Array and value.size() == 4: + return Vector4(value[0], value[1], value[2], value[3]) + return {"_error": "Vector4 expects [x, y, z, w]"} + TYPE_VECTOR4I: + if value is Array and value.size() == 4: + return Vector4i(int(value[0]), int(value[1]), int(value[2]), int(value[3])) + return {"_error": "Vector4i expects [x, y, z, w]"} + TYPE_RECT2: + if value is Array and value.size() == 4: + return Rect2(value[0], value[1], value[2], value[3]) + return {"_error": "Rect2 expects [x, y, width, height]"} + TYPE_RECT2I: + if value is Array and value.size() == 4: + return Rect2i(int(value[0]), int(value[1]), int(value[2]), int(value[3])) + return {"_error": "Rect2i expects [x, y, width, height]"} + TYPE_QUATERNION: + if value is Array and value.size() == 4: + return Quaternion(value[0], value[1], value[2], value[3]) + return {"_error": "Quaternion expects [x, y, z, w]"} + TYPE_TRANSFORM2D: + # TRS form: {origin: [x,y], rotation: radians, scale: [x,y]} — all optional. + if value is Dictionary: + var origin = value.get("origin", [0, 0]) + var rotation: float = float(value.get("rotation", 0.0)) + var scale_v = value.get("scale", [1, 1]) + var skew: float = float(value.get("skew", 0.0)) + if not (origin is Array and origin.size() == 2 and scale_v is Array and scale_v.size() == 2): + return {"_error": "Transform2D origin/scale expect [x, y]"} + return Transform2D( + rotation, + Vector2(scale_v[0], scale_v[1]), + skew, + Vector2(origin[0], origin[1]) + ) + return {"_error": "Transform2D expects {origin: [x,y], rotation: radians, scale: [x,y], skew?}"} + TYPE_TRANSFORM3D: + # TRS form: {origin: [x,y,z], rotation: [x,y,z] euler radians, scale: [x,y,z]}. + if value is Dictionary: + var origin = value.get("origin", [0, 0, 0]) + var rotation = value.get("rotation", [0, 0, 0]) + var scale_v = value.get("scale", [1, 1, 1]) + if not (origin is Array and origin.size() == 3 + and rotation is Array and rotation.size() == 3 + and scale_v is Array and scale_v.size() == 3): + return {"_error": "Transform3D origin/rotation/scale expect [x, y, z]"} + var basis := Basis.from_euler(Vector3(rotation[0], rotation[1], rotation[2])) + basis = basis.scaled(Vector3(scale_v[0], scale_v[1], scale_v[2])) + return Transform3D(basis, Vector3(origin[0], origin[1], origin[2])) + return {"_error": "Transform3D expects {origin: [x,y,z], rotation: [x,y,z] (euler rad), scale: [x,y,z]}"} + TYPE_BASIS: + if value is Dictionary: + var rotation = value.get("rotation", [0, 0, 0]) + var scale_v = value.get("scale", [1, 1, 1]) + if not (rotation is Array and rotation.size() == 3 + and scale_v is Array and scale_v.size() == 3): + return {"_error": "Basis rotation/scale expect [x, y, z]"} + return Basis.from_euler(Vector3(rotation[0], rotation[1], rotation[2])).scaled( + Vector3(scale_v[0], scale_v[1], scale_v[2])) + return {"_error": "Basis expects {rotation: [x,y,z] (euler rad), scale: [x,y,z]}"} + TYPE_AABB: + if value is Dictionary: + var pos = value.get("position", [0, 0, 0]) + var size = value.get("size", [0, 0, 0]) + if not (pos is Array and pos.size() == 3 and size is Array and size.size() == 3): + return {"_error": "AABB position/size expect [x, y, z]"} + return AABB(Vector3(pos[0], pos[1], pos[2]), Vector3(size[0], size[1], size[2])) + return {"_error": "AABB expects {position: [x,y,z], size: [x,y,z]}"} + TYPE_PLANE: + if value is Dictionary: + var normal = value.get("normal", [0, 1, 0]) + var d: float = float(value.get("d", 0.0)) + if not (normal is Array and normal.size() == 3): + return {"_error": "Plane normal expects [x, y, z]"} + return Plane(Vector3(normal[0], normal[1], normal[2]), d) + return {"_error": "Plane expects {normal: [x,y,z], d: float}"} + TYPE_COLOR: + if value is Array and (value.size() == 3 or value.size() == 4): + var a := float(value[3]) if value.size() == 4 else 1.0 + return Color(value[0], value[1], value[2], a) + if value is String: + return Color(value) + return {"_error": "Color expects [r,g,b(,a)] or '#rrggbb(aa)'"} + TYPE_PACKED_STRING_ARRAY: + if value is Array: + var out := PackedStringArray() + for s in value: + out.append(String(s)) + return out + return {"_error": "PackedStringArray expects an array of strings"} + TYPE_PACKED_INT32_ARRAY: + if value is Array: + var out := PackedInt32Array() + for i in value: + out.append(int(i)) + return out + return {"_error": "PackedInt32Array expects an array of numbers"} + TYPE_PACKED_INT64_ARRAY: + if value is Array: + var out := PackedInt64Array() + for i in value: + out.append(int(i)) + return out + return {"_error": "PackedInt64Array expects an array of numbers"} + TYPE_PACKED_FLOAT32_ARRAY: + if value is Array: + var out := PackedFloat32Array() + for f in value: + out.append(float(f)) + return out + return {"_error": "PackedFloat32Array expects an array of numbers"} + TYPE_PACKED_FLOAT64_ARRAY: + if value is Array: + var out := PackedFloat64Array() + for f in value: + out.append(float(f)) + return out + return {"_error": "PackedFloat64Array expects an array of numbers"} + TYPE_PACKED_VECTOR2_ARRAY: + if value is Array: + var out := PackedVector2Array() + for v in value: + if v is Array and v.size() == 2: + out.append(Vector2(v[0], v[1])) + else: + return {"_error": "PackedVector2Array expects [[x, y], ...]"} + return out + return {"_error": "PackedVector2Array expects an array of [x, y] pairs"} + TYPE_PACKED_VECTOR3_ARRAY: + if value is Array: + var out := PackedVector3Array() + for v in value: + if v is Array and v.size() == 3: + out.append(Vector3(v[0], v[1], v[2])) + else: + return {"_error": "PackedVector3Array expects [[x, y, z], ...]"} + return out + return {"_error": "PackedVector3Array expects an array of [x, y, z] triples"} + TYPE_PACKED_COLOR_ARRAY: + if value is Array: + var out := PackedColorArray() + for c in value: + if c is Array and (c.size() == 3 or c.size() == 4): + var a := float(c[3]) if c.size() == 4 else 1.0 + out.append(Color(c[0], c[1], c[2], a)) + elif c is String: + out.append(Color(c)) + else: + return {"_error": "PackedColorArray expects [[r,g,b(,a)], ...] or ['#hex', ...]"} + return out + return {"_error": "PackedColorArray expects an array of colors"} + TYPE_OBJECT: + # Auto-load a Resource when the caller passes a "res://..." or "uid://..." path string. + # Without this, Godot silently drops the assignment and leaves the slot null. + if value is String and (value.begins_with("res://") or value.begins_with("uid://")): + var loaded := ResourceLoader.load(value) + if loaded == null: + return {"_error": "failed to load resource: %s" % value} + return loaded + return value + _: + return value + + +# Inverse of coerce(): convert a Godot value back to a JSON-native form so the +# response echo is unambiguous data rather than Godot's str() stringification +# (which turns Vector2 into "(x, y)", Color into "(r, g, b, a)", null Object +# into "", etc., all of which look like success strings). +static func to_json(value): + match typeof(value): + TYPE_VECTOR2: + return [value.x, value.y] + TYPE_VECTOR2I: + return [int(value.x), int(value.y)] + TYPE_VECTOR3: + return [value.x, value.y, value.z] + TYPE_VECTOR3I: + return [int(value.x), int(value.y), int(value.z)] + TYPE_VECTOR4: + return [value.x, value.y, value.z, value.w] + TYPE_VECTOR4I: + return [int(value.x), int(value.y), int(value.z), int(value.w)] + TYPE_RECT2: + return [value.position.x, value.position.y, value.size.x, value.size.y] + TYPE_RECT2I: + return [int(value.position.x), int(value.position.y), int(value.size.x), int(value.size.y)] + TYPE_COLOR: + return [value.r, value.g, value.b, value.a] + TYPE_QUATERNION: + return [value.x, value.y, value.z, value.w] + TYPE_NODE_PATH: + return String(value) + TYPE_STRING_NAME: + return String(value) + TYPE_TRANSFORM2D: + return { + "origin": [value.origin.x, value.origin.y], + "x": [value.x.x, value.x.y], + "y": [value.y.x, value.y.y], + } + TYPE_TRANSFORM3D: + return { + "origin": [value.origin.x, value.origin.y, value.origin.z], + "basis": to_json(value.basis), + } + TYPE_BASIS: + return [ + [value.x.x, value.x.y, value.x.z], + [value.y.x, value.y.y, value.y.z], + [value.z.x, value.z.y, value.z.z], + ] + TYPE_AABB: + return { + "position": [value.position.x, value.position.y, value.position.z], + "size": [value.size.x, value.size.y, value.size.z], + } + TYPE_PLANE: + return { + "normal": [value.normal.x, value.normal.y, value.normal.z], + "d": value.d, + } + TYPE_OBJECT: + if value == null: + return null + if value is Resource and value.resource_path != "": + return {"class": value.get_class(), "resource_path": value.resource_path} + return {"class": (value as Object).get_class()} + TYPE_PACKED_BYTE_ARRAY, TYPE_PACKED_INT32_ARRAY, TYPE_PACKED_INT64_ARRAY, \ + TYPE_PACKED_FLOAT32_ARRAY, TYPE_PACKED_FLOAT64_ARRAY, TYPE_PACKED_STRING_ARRAY: + return Array(value) + TYPE_PACKED_VECTOR2_ARRAY: + var out_v2: Array = [] + for v in value: + out_v2.append([v.x, v.y]) + return out_v2 + TYPE_PACKED_VECTOR3_ARRAY: + var out_v3: Array = [] + for v in value: + out_v3.append([v.x, v.y, v.z]) + return out_v3 + TYPE_PACKED_COLOR_ARRAY: + var out_c: Array = [] + for c in value: + out_c.append([c.r, c.g, c.b, c.a]) + return out_c + TYPE_ARRAY: + var out_a: Array = [] + for item in value: + out_a.append(to_json(item)) + return out_a + TYPE_DICTIONARY: + var out_d: Dictionary = {} + for k in value: + out_d[str(k)] = to_json(value[k]) + return out_d + _: + return value diff --git a/addons/agent_tools/tools/_coerce.gd.uid b/addons/agent_tools/tools/_coerce.gd.uid new file mode 100644 index 0000000..f3abf9d --- /dev/null +++ b/addons/agent_tools/tools/_coerce.gd.uid @@ -0,0 +1 @@ +uid://cncjf8g7jufrc diff --git a/addons/agent_tools/tools/animation_tools.gd b/addons/agent_tools/tools/animation_tools.gd new file mode 100644 index 0000000..26c7050 --- /dev/null +++ b/addons/agent_tools/tools/animation_tools.gd @@ -0,0 +1,210 @@ +@tool +extends RefCounted + +# Tools for working with AnimationPlayer nodes in the currently-edited scene. +# Animations live in AnimationLibrary resources attached to the player; the "" library +# is the default, named libraries prefix animations as "lib/anim". + +# animation.list — list animations on an AnimationPlayer with their tracks. +# Params: {node_path} +static func list_animations(params: Dictionary) -> Dictionary: + var node_path: String = params.get("node_path", "") + if node_path == "": + return _err(-32602, "missing 'node_path'") + var root := EditorInterface.get_edited_scene_root() + if root == null: + return _err(-32001, "no scene open") + var node: Node = root if node_path == "." else root.get_node_or_null(node_path) + if node == null: + return _err(-32001, "node not found: %s" % node_path) + if not (node is AnimationPlayer): + return _err(-32602, "node is not an AnimationPlayer: %s (%s)" % [node_path, node.get_class()]) + var ap: AnimationPlayer = node + + var out: Array = [] + for lib_name in ap.get_animation_library_list(): + var lib: AnimationLibrary = ap.get_animation_library(lib_name) + for anim_name in lib.get_animation_list(): + var anim: Animation = lib.get_animation(anim_name) + var full_name: String + if String(lib_name) == "": + full_name = String(anim_name) + else: + full_name = "%s/%s" % [lib_name, anim_name] + var tracks: Array = [] + for i in anim.get_track_count(): + tracks.append({ + "index": i, + "type": _track_type_name(anim.track_get_type(i)), + "path": String(anim.track_get_path(i)), + "key_count": anim.track_get_key_count(i), + }) + out.append({ + "name": full_name, + "length": anim.length, + "loop_mode": anim.loop_mode, + "step": anim.step, + "tracks": tracks, + }) + return _ok({"node_path": node_path, "animations": out}) + + +# animation.add_animation — create an empty animation in the player's library. +# Params: {node_path, name, length?: 1.0, library?: ""} +static func add_animation(params: Dictionary) -> Dictionary: + var node_path: String = params.get("node_path", "") + var anim_name: String = params.get("name", "") + var length: float = float(params.get("length", 1.0)) + var library_name: String = params.get("library", "") + if node_path == "" or anim_name == "": + return _err(-32602, "missing 'node_path' or 'name'") + + var root := EditorInterface.get_edited_scene_root() + if root == null: + return _err(-32001, "no scene open") + var node: Node = root if node_path == "." else root.get_node_or_null(node_path) + if node == null: + return _err(-32001, "node not found: %s" % node_path) + if not (node is AnimationPlayer): + return _err(-32602, "node is not an AnimationPlayer: %s (%s)" % [node_path, node.get_class()]) + var ap: AnimationPlayer = node + + var lib: AnimationLibrary + if ap.has_animation_library(library_name): + lib = ap.get_animation_library(library_name) + else: + lib = AnimationLibrary.new() + var add_err: int = ap.add_animation_library(library_name, lib) + if add_err != OK: + return _err(-32001, "add_animation_library failed: %d" % add_err) + + if lib.has_animation(anim_name): + return _err(-32602, "animation already exists: %s" % anim_name) + + var anim := Animation.new() + anim.length = length + @warning_ignore("return_value_discarded") + lib.add_animation(anim_name, anim) + EditorInterface.mark_scene_as_unsaved() + return _ok({"name": anim_name, "library": library_name, "length": length}) + + +# animation.remove_animation — delete an animation from the player. +# Params: {node_path, name, library?: ""} +static func remove_animation(params: Dictionary) -> Dictionary: + var node_path: String = params.get("node_path", "") + var anim_name: String = params.get("name", "") + var library_name: String = params.get("library", "") + if node_path == "" or anim_name == "": + return _err(-32602, "missing 'node_path' or 'name'") + + var root := EditorInterface.get_edited_scene_root() + if root == null: + return _err(-32001, "no scene open") + var node: Node = root if node_path == "." else root.get_node_or_null(node_path) + if node == null: + return _err(-32001, "node not found: %s" % node_path) + if not (node is AnimationPlayer): + return _err(-32602, "node is not an AnimationPlayer") + var ap: AnimationPlayer = node + + if not ap.has_animation_library(library_name): + return _err(-32001, "library not found: '%s'" % library_name) + var lib: AnimationLibrary = ap.get_animation_library(library_name) + if not lib.has_animation(anim_name): + return _err(-32001, "animation not found: %s" % anim_name) + lib.remove_animation(anim_name) + EditorInterface.mark_scene_as_unsaved() + return _ok({"removed": anim_name, "library": library_name}) + + +# animation.add_value_track — add a value track that animates a property over time. +# target_node is resolved relative to the scene root (simpler than the +# AnimationPlayer.root_node-relative resolution, which is fiddly). +# Params: {node_path, animation, target_node, property, keyframes: [{time, value, easing?}]} +static func add_value_track(params: Dictionary) -> Dictionary: + var node_path: String = params.get("node_path", "") + var anim_name: String = params.get("animation", "") + var target_node: String = params.get("target_node", "") + var property_name: String = params.get("property", "") + var keyframes: Array = params.get("keyframes", []) + + if node_path == "" or anim_name == "" or target_node == "" or property_name == "": + return _err(-32602, "missing 'node_path', 'animation', 'target_node', or 'property'") + if keyframes.is_empty(): + return _err(-32602, "'keyframes' must contain at least one entry") + + var root := EditorInterface.get_edited_scene_root() + if root == null: + return _err(-32001, "no scene open") + var node: Node = root if node_path == "." else root.get_node_or_null(node_path) + if node == null: + return _err(-32001, "node not found: %s" % node_path) + if not (node is AnimationPlayer): + return _err(-32602, "node is not an AnimationPlayer") + var ap: AnimationPlayer = node + + if not ap.has_animation(anim_name): + return _err(-32001, "animation not found on this player: %s" % anim_name) + var anim: Animation = ap.get_animation(anim_name) + + # Validate that the target resolves from the scene root (a simpler contract + # than AP.root_node-relative — callers think in scene-tree terms already). + if root.get_node_or_null(target_node) == null: + return _err(-32001, "target_node does not resolve from scene root: %s" % target_node) + + var track_idx: int = anim.add_track(Animation.TYPE_VALUE) + anim.track_set_path(track_idx, NodePath("%s:%s" % [target_node, property_name])) + + var max_time: float = 0.0 + for kf in keyframes: + if typeof(kf) != TYPE_DICTIONARY or not kf.has("time") or not kf.has("value"): + return _err(-32602, "each keyframe needs {time, value}") + var t: float = float(kf.time) + var transition: float = float(kf.get("easing", 1.0)) + anim.track_insert_key(track_idx, t, kf.value, transition) + if t > max_time: + max_time = t + + if max_time > anim.length: + anim.length = max_time + + EditorInterface.mark_scene_as_unsaved() + return _ok({ + "animation": anim_name, + "track_index": track_idx, + "target": "%s:%s" % [target_node, property_name], + "keyframes_added": keyframes.size(), + }) + + +static func _track_type_name(t: int) -> String: + match t: + Animation.TYPE_VALUE: + return "value" + Animation.TYPE_POSITION_3D: + return "position_3d" + Animation.TYPE_ROTATION_3D: + return "rotation_3d" + Animation.TYPE_SCALE_3D: + return "scale_3d" + Animation.TYPE_BLEND_SHAPE: + return "blend_shape" + Animation.TYPE_METHOD: + return "method" + Animation.TYPE_BEZIER: + return "bezier" + Animation.TYPE_AUDIO: + return "audio" + Animation.TYPE_ANIMATION: + return "animation" + _: + return "unknown(%d)" % t + + +static func _ok(data) -> Dictionary: + return {"data": data} + + +static func _err(code: int, msg: String) -> Dictionary: + return {"error": {"code": code, "message": msg}} diff --git a/addons/agent_tools/tools/animation_tools.gd.uid b/addons/agent_tools/tools/animation_tools.gd.uid new file mode 100644 index 0000000..1933acc --- /dev/null +++ b/addons/agent_tools/tools/animation_tools.gd.uid @@ -0,0 +1 @@ +uid://kurfdxg67ggk diff --git a/addons/agent_tools/tools/client_tools.gd b/addons/agent_tools/tools/client_tools.gd new file mode 100644 index 0000000..4dc7cc0 --- /dev/null +++ b/addons/agent_tools/tools/client_tools.gd @@ -0,0 +1,296 @@ +@tool +extends RefCounted + +# client.* — manage MCP client config files so users don't have to hand-edit them. +# Each client has its own config schema; we handle three shapes: +# "mcp_servers" → {"mcpServers": {"": {command, args}}} +# Used by Claude Code, Claude Desktop, Cursor, Windsurf. +# "vscode_servers" → {"servers": {"": {command, args}}} +# VS Code's native MCP config. +# "continue_experimental" → {"experimental": {"modelContextProtocolServers": +# [{"transport": {"type": "stdio", command, args}}]}} +# Continue.dev's MCP config shape (array, not dict). +# +# Clients requiring JSONC (with comments) or non-file storage — Zed, VS Code +# user-scope settings.json, Cline — aren't supported yet. Use manual config +# for those; see the README per-client section. + +const SERVER_NAME := "godot-agent-tools" +const SERVER_COMMAND := "npx" +const SERVER_ARGS := ["-y", "godot-agent-tools-mcp"] + + +static func list_clients(_params: Dictionary) -> Dictionary: + var out: Array = [] + for client_id in _client_ids(): + var info := _client_info(client_id) + if info.has("_error"): + out.append({"client": client_id, "error": info._error}) + continue + var res := _read_config(info.path) + var cfg: Dictionary = res.cfg + var installed := _has_server_entry(cfg, info.format) + out.append({ + "client": client_id, + "config_path": info.path, + "exists": _path_exists(info.path), + "configured": installed, + "unparseable": res.unparseable, + }) + return _ok({"clients": out}) + + +static func configure(params: Dictionary) -> Dictionary: + var client_id: String = params.get("client", "") + var overwrite: bool = params.get("overwrite", false) + if client_id == "": + return _err(-32602, "missing 'client' (supported: %s)" % ", ".join(_client_ids())) + var info := _client_info(client_id) + if info.has("_error"): + return _err(-32602, info._error) + + var res := _read_config(info.path) + if res.unparseable: + return _err(-32001, "existing config is not valid JSON — refusing to overwrite (fix or remove it first): %s" % info.path) + var cfg: Dictionary = res.cfg + if _has_server_entry(cfg, info.format) and not overwrite: + return _ok({"client": client_id, "path": info.path, "status": "already_configured"}) + + _set_server_entry(cfg, info.format) + var write_err := _write_config(info.path, cfg) + if write_err != "": + return _err(-32001, write_err) + return _ok({"client": client_id, "path": info.path, "status": "configured"}) + + +static func remove(params: Dictionary) -> Dictionary: + var client_id: String = params.get("client", "") + if client_id == "": + return _err(-32602, "missing 'client'") + var info := _client_info(client_id) + if info.has("_error"): + return _err(-32602, info._error) + + var res := _read_config(info.path) + if res.unparseable: + return _err(-32001, "existing config is not valid JSON — refusing to modify (fix or remove it first): %s" % info.path) + var cfg: Dictionary = res.cfg + if not _has_server_entry(cfg, info.format): + return _ok({"client": client_id, "path": info.path, "status": "not_present"}) + + _clear_server_entry(cfg, info.format) + var write_err := _write_config(info.path, cfg) + if write_err != "": + return _err(-32001, write_err) + return _ok({"client": client_id, "path": info.path, "status": "removed"}) + + +static func _client_ids() -> Array: + return [ + "claude_code_project", + "claude_code_user", + "claude_desktop", + "cursor_project", + "cursor_user", + "windsurf_user", + "continue_user", + "vscode_project", + ] + + +static func _client_info(client_id: String) -> Dictionary: + var home := OS.get_environment("USERPROFILE") + if home == "": + home = OS.get_environment("HOME") + if home == "": + return {"_error": "cannot resolve home directory (neither USERPROFILE nor HOME set)"} + + match client_id: + "claude_code_project": + return {"path": ProjectSettings.globalize_path("res://.mcp.json"), "format": "mcp_servers"} + "claude_code_user": + return {"path": home.path_join(".claude.json"), "format": "mcp_servers"} + "cursor_project": + return {"path": ProjectSettings.globalize_path("res://.cursor/mcp.json"), "format": "mcp_servers"} + "cursor_user": + return {"path": home.path_join(".cursor").path_join("mcp.json"), "format": "mcp_servers"} + "windsurf_user": + return {"path": home.path_join(".codeium").path_join("windsurf").path_join("mcp_config.json"), "format": "mcp_servers"} + "continue_user": + return {"path": home.path_join(".continue").path_join("config.json"), "format": "continue_experimental"} + "vscode_project": + return {"path": ProjectSettings.globalize_path("res://.vscode/mcp.json"), "format": "vscode_servers"} + "claude_desktop": + var os_name := OS.get_name() + var path: String + if os_name == "macOS": + path = home.path_join("Library").path_join("Application Support").path_join("Claude").path_join("claude_desktop_config.json") + elif os_name == "Windows": + var appdata := OS.get_environment("APPDATA") + if appdata == "": + return {"_error": "APPDATA env var not set on Windows"} + path = appdata.path_join("Claude").path_join("claude_desktop_config.json") + else: + path = home.path_join(".config").path_join("Claude").path_join("claude_desktop_config.json") + return {"path": path, "format": "mcp_servers"} + _: + return {"_error": "unknown client: %s (supported: %s)" % [client_id, ", ".join(_client_ids())]} + + +static func _path_exists(path: String) -> bool: + return FileAccess.file_exists(path) + + +# Returns {"cfg": Dictionary, "unparseable": bool}. unparseable=true means the +# file exists with non-empty content that is NOT a valid JSON object — callers +# that go on to WRITE must abort, since merging into {} would wipe the user's +# data (e.g. a large ~/.claude.json with a transient syntax error, or a JSONC +# file with comments that JSON.parse_string can't handle). +static func _read_config(path: String) -> Dictionary: + var content := "" + if _path_exists(path): + var f := FileAccess.open(path, FileAccess.READ) + if f: + content = f.get_as_text() + f.close() + if content.strip_edges() == "": + return {"cfg": {}, "unparseable": false} + var parsed = JSON.parse_string(content) + if typeof(parsed) != TYPE_DICTIONARY: + return {"cfg": {}, "unparseable": true} + return {"cfg": parsed, "unparseable": false} + + +static func _write_config(path: String, cfg: Dictionary) -> String: + var dir_path := path.get_base_dir() + if not DirAccess.dir_exists_absolute(dir_path): + var derr := DirAccess.make_dir_recursive_absolute(dir_path) + if derr != OK: + return "mkdir failed for %s (%d)" % [dir_path, derr] + # Back up any existing non-empty file before overwriting, so a mistaken merge + # or an interrupted write is recoverable. + if FileAccess.file_exists(path): + var prev := FileAccess.open(path, FileAccess.READ) + if prev: + var prev_content := prev.get_as_text() + prev.close() + if prev_content.strip_edges() != "": + var bak := FileAccess.open(path + ".bak", FileAccess.WRITE) + if bak: + bak.store_string(prev_content) + bak.close() + var f := FileAccess.open(path, FileAccess.WRITE) + if f == null: + return "could not open for write: %s (error %d)" % [path, FileAccess.get_open_error()] + f.store_string(JSON.stringify(cfg, " ")) + f.close() + return "" + + +# --- Format-specific readers/writers ----------------------------------------- + +static func _has_server_entry(cfg: Dictionary, format: String) -> bool: + match format: + "mcp_servers": + var servers = cfg.get("mcpServers") + return typeof(servers) == TYPE_DICTIONARY and (servers as Dictionary).has(SERVER_NAME) + "vscode_servers": + var servers = cfg.get("servers") + return typeof(servers) == TYPE_DICTIONARY and (servers as Dictionary).has(SERVER_NAME) + "continue_experimental": + var exp = cfg.get("experimental") + if typeof(exp) != TYPE_DICTIONARY: + return false + var arr = exp.get("modelContextProtocolServers") + if typeof(arr) != TYPE_ARRAY: + return false + for entry in arr: + if typeof(entry) == TYPE_DICTIONARY and _entry_matches_continue(entry): + return true + return false + _: + return false + + +static func _set_server_entry(cfg: Dictionary, format: String) -> void: + match format: + "mcp_servers": + if typeof(cfg.get("mcpServers")) != TYPE_DICTIONARY: + cfg["mcpServers"] = {} + (cfg["mcpServers"] as Dictionary)[SERVER_NAME] = { + "command": SERVER_COMMAND, + "args": SERVER_ARGS.duplicate(), + } + "vscode_servers": + if typeof(cfg.get("servers")) != TYPE_DICTIONARY: + cfg["servers"] = {} + (cfg["servers"] as Dictionary)[SERVER_NAME] = { + "command": SERVER_COMMAND, + "args": SERVER_ARGS.duplicate(), + } + "continue_experimental": + if typeof(cfg.get("experimental")) != TYPE_DICTIONARY: + cfg["experimental"] = {} + var exp: Dictionary = cfg["experimental"] + if typeof(exp.get("modelContextProtocolServers")) != TYPE_ARRAY: + exp["modelContextProtocolServers"] = [] + var arr: Array = exp["modelContextProtocolServers"] + # Remove any existing match so we don't duplicate. + var kept: Array = [] + for entry in arr: + if typeof(entry) != TYPE_DICTIONARY or not _entry_matches_continue(entry): + kept.append(entry) + kept.append({ + "transport": { + "type": "stdio", + "command": SERVER_COMMAND, + "args": SERVER_ARGS.duplicate(), + }, + }) + exp["modelContextProtocolServers"] = kept + + +static func _clear_server_entry(cfg: Dictionary, format: String) -> void: + match format: + "mcp_servers": + var servers = cfg.get("mcpServers") + if typeof(servers) == TYPE_DICTIONARY: + (servers as Dictionary).erase(SERVER_NAME) + "vscode_servers": + var servers = cfg.get("servers") + if typeof(servers) == TYPE_DICTIONARY: + (servers as Dictionary).erase(SERVER_NAME) + "continue_experimental": + var exp = cfg.get("experimental") + if typeof(exp) != TYPE_DICTIONARY: + return + var arr = exp.get("modelContextProtocolServers") + if typeof(arr) != TYPE_ARRAY: + return + var kept: Array = [] + for entry in arr: + if typeof(entry) != TYPE_DICTIONARY or not _entry_matches_continue(entry): + kept.append(entry) + exp["modelContextProtocolServers"] = kept + + +# Continue's config uses a flat array with no `name` field — identify our entry +# by the transport command+args combo. +static func _entry_matches_continue(entry: Dictionary) -> bool: + var transport = entry.get("transport") + if typeof(transport) != TYPE_DICTIONARY: + return false + if transport.get("command") != SERVER_COMMAND: + return false + var a = transport.get("args") + if typeof(a) != TYPE_ARRAY or (a as Array).size() < 2: + return false + return a[1] == SERVER_ARGS[1] + + +static func _ok(data) -> Dictionary: + return {"data": data} + + +static func _err(code: int, msg: String) -> Dictionary: + return {"error": {"code": code, "message": msg}} diff --git a/addons/agent_tools/tools/client_tools.gd.uid b/addons/agent_tools/tools/client_tools.gd.uid new file mode 100644 index 0000000..3cdf9b0 --- /dev/null +++ b/addons/agent_tools/tools/client_tools.gd.uid @@ -0,0 +1 @@ +uid://b3u1el0113l81 diff --git a/addons/agent_tools/tools/docs_tools.gd b/addons/agent_tools/tools/docs_tools.gd new file mode 100644 index 0000000..fbdd50b --- /dev/null +++ b/addons/agent_tools/tools/docs_tools.gd @@ -0,0 +1,72 @@ +@tool +extends RefCounted + +# docs.class_ref — return the public API of a Godot class so the agent can plan +# work without guessing method/property/signal names. +# Params: {class_name, include_inherited?: false} +# Returns: {class, parent, methods: [{name, args, return}], properties, signals, constants} +static func class_ref(params: Dictionary) -> Dictionary: + var cname: String = params.get("class_name", "") + var include_inherited: bool = params.get("include_inherited", false) + if cname == "": + return _err(-32602, "missing 'class_name'") + if not ClassDB.class_exists(cname): + return _err(-32602, "unknown class: %s" % cname) + + var no_inheritance := not include_inherited + + var methods: Array = [] + for m in ClassDB.class_get_method_list(cname, no_inheritance): + # Skip internal / editor-only methods — they're noise for an agent. + if (m.flags as int) & METHOD_FLAG_OBJECT_CORE: + continue + var args: Array = [] + for a in m.args: + args.append({"name": a.name, "type": type_string(a.type)}) + methods.append({ + "name": m.name, + "args": args, + "return": type_string(m.return.type) if m.has("return") else "void", + "virtual": bool((m.flags as int) & METHOD_FLAG_VIRTUAL), + "static": bool((m.flags as int) & METHOD_FLAG_STATIC), + }) + + var properties: Array = [] + for p in ClassDB.class_get_property_list(cname, no_inheritance): + # Only user-facing properties (storage + editor). Internal group markers have usage=0. + if not ((p.usage as int) & PROPERTY_USAGE_EDITOR): + continue + properties.append({ + "name": p.name, + "type": type_string(p.type), + "hint": p.get("hint_string", ""), + }) + + var signals: Array = [] + for s in ClassDB.class_get_signal_list(cname, no_inheritance): + var args: Array = [] + for a in s.args: + args.append({"name": a.name, "type": type_string(a.type)}) + signals.append({"name": s.name, "args": args}) + + var constants: Array = [] + for c in ClassDB.class_get_integer_constant_list(cname, no_inheritance): + constants.append({"name": c, "value": ClassDB.class_get_integer_constant(cname, c)}) + + return _ok({ + "class": cname, + "parent": ClassDB.get_parent_class(cname), + "inherited_included": include_inherited, + "methods": methods, + "properties": properties, + "signals": signals, + "constants": constants, + }) + + +static func _ok(data) -> Dictionary: + return {"data": data} + + +static func _err(code: int, msg: String) -> Dictionary: + return {"error": {"code": code, "message": msg}} diff --git a/addons/agent_tools/tools/docs_tools.gd.uid b/addons/agent_tools/tools/docs_tools.gd.uid new file mode 100644 index 0000000..3ba1e88 --- /dev/null +++ b/addons/agent_tools/tools/docs_tools.gd.uid @@ -0,0 +1 @@ +uid://dwarklg56vsim diff --git a/addons/agent_tools/tools/editor_tools.gd b/addons/agent_tools/tools/editor_tools.gd new file mode 100644 index 0000000..6c79757 --- /dev/null +++ b/addons/agent_tools/tools/editor_tools.gd @@ -0,0 +1,201 @@ +@tool +extends RefCounted + +# editor.reload_filesystem — trigger an editor filesystem rescan. +# Run this after external file changes (created/moved/deleted by tools outside the editor) +# so load() and the FileSystem dock reflect reality. +static func reload_filesystem(_params: Dictionary) -> Dictionary: + EditorInterface.get_resource_filesystem().scan() + return {"data": {"scanned": true}} + + +# editor.save_all_scenes — save every open edited scene. +static func save_all_scenes(_params: Dictionary) -> Dictionary: + EditorInterface.save_all_scenes() + return {"data": {"saved": true}} + + +# editor.state — consolidated editor + project status in one call. +# Replaces piecewise scene.current + project.get_setting + etc. +static func state(_params: Dictionary) -> Dictionary: + var version := Engine.get_version_info() + var version_str: String = "%d.%d.%d-%s" % [version.major, version.minor, version.patch, version.status] + + var current_scene_path := "" + var current_scene_class := "" + var current_scene_name := "" + var root := EditorInterface.get_edited_scene_root() + if root != null: + current_scene_path = root.scene_file_path + current_scene_class = root.get_class() + current_scene_name = String(root.name) + + var open_scenes: Array = [] + for s in EditorInterface.get_open_scenes(): + open_scenes.append(s) + + var project_name := "" + if ProjectSettings.has_setting("application/config/name"): + project_name = str(ProjectSettings.get_setting("application/config/name")) + + return {"data": { + "godot_version": version_str, + "project_name": project_name, + "current_scene": { + "path": current_scene_path, + "root_name": current_scene_name, + "root_class": current_scene_class, + "open": root != null, + }, + "open_scenes": open_scenes, + "playing_scene": EditorInterface.is_playing_scene(), + "playing_scene_path": EditorInterface.get_playing_scene() if EditorInterface.is_playing_scene() else "", + }} + + +# editor.selection_get — return the currently-selected nodes in the editor tree dock. +# Useful for "operate on whatever I clicked" workflows. +static func selection_get(_params: Dictionary) -> Dictionary: + var selection := EditorInterface.get_selection() + var root := EditorInterface.get_edited_scene_root() + var out: Array = [] + for node in selection.get_selected_nodes(): + var path_str := "" + if root != null and (node == root or root.is_ancestor_of(node)): + path_str = String(root.get_path_to(node)) + out.append({ + "name": String(node.name), + "class": node.get_class(), + "node_path": path_str, + }) + return {"data": {"selected": out, "count": out.size()}} + + +# editor.selection_set — select a specific set of nodes in the editor tree dock. +# Params: {node_paths: ["Player", "Enemy"]} +static func selection_set(params: Dictionary) -> Dictionary: + var paths: Array = params.get("node_paths", []) + var root := EditorInterface.get_edited_scene_root() + if root == null: + return {"error": {"code": -32001, "message": "no scene open"}} + + var selection := EditorInterface.get_selection() + selection.clear() + + var resolved: Array = [] + var missing: Array = [] + for p in paths: + var node: Node = root if p == "." else root.get_node_or_null(p) + if node == null: + missing.append(p) + continue + selection.add_node(node) + resolved.append(p) + + if not missing.is_empty(): + return {"error": {"code": -32001, "message": "nodes not found: %s" % str(missing)}} + return {"data": {"selected": resolved}} + + +# editor.game_screenshot — capture the viewport of the currently-running game. +# Works when the user has pressed F5 (or equivalent) and a game process is running +# with our runtime bridge autoload registered. Writes a request file, polls for +# the bridge to produce the PNG, returns the path. +# +# Params: {output?: "res://.godot/agent_tools/game.png", timeout_ms?: 5000} +static func game_screenshot(params: Dictionary) -> Dictionary: + var output_path: String = params.get("output", "res://.godot/agent_tools/game_screenshot.png") + var timeout_ms: int = int(params.get("timeout_ms", 5000)) + + if not EditorInterface.is_playing_scene(): + return {"error": {"code": -32001, + "message": "no game running — press F5 (or equivalent) to start the game, then call again. Alternative for arbitrary scenes: run.scene_headless with 'screenshot'."}} + + var bridge_dir := "res://.godot/agent_tools/bridge" + if not DirAccess.dir_exists_absolute(bridge_dir): + var derr := DirAccess.make_dir_recursive_absolute(bridge_dir) + if derr != OK: + return {"error": {"code": -32001, "message": "mkdir failed: %d" % derr}} + + # Wipe stale output so we can detect when the bridge has written fresh data. + if FileAccess.file_exists(output_path): + @warning_ignore("return_value_discarded") + DirAccess.remove_absolute(output_path) + + var request_path := bridge_dir + "/request.json" + var request_id := str(Time.get_ticks_msec()) + var req := { + "id": request_id, + "type": "screenshot", + "output": output_path, + } + var f := FileAccess.open(request_path, FileAccess.WRITE) + if f == null: + return {"error": {"code": -32001, "message": "could not write bridge request"}} + f.store_string(JSON.stringify(req)) + f.close() + + # Poll for response — bridge writes response.json with matching id when done. + var response_path := bridge_dir + "/response.json" + var deadline_usec: int = Time.get_ticks_usec() + timeout_ms * 1000 + while Time.get_ticks_usec() < deadline_usec: + if FileAccess.file_exists(response_path): + var rf := FileAccess.open(response_path, FileAccess.READ) + if rf: + var body := rf.get_as_text() + rf.close() + var parsed = JSON.parse_string(body) + if typeof(parsed) == TYPE_DICTIONARY and parsed.get("id", "") == request_id: + @warning_ignore("return_value_discarded") + DirAccess.remove_absolute(response_path) + if parsed.has("error"): + return {"error": {"code": -32001, "message": "bridge: %s" % parsed.error}} + return {"data": { + "path": output_path, + "captured": FileAccess.file_exists(output_path), + }} + OS.delay_msec(50) + + # Clean up stale request if bridge never picked it up. + if FileAccess.file_exists(request_path): + @warning_ignore("return_value_discarded") + DirAccess.remove_absolute(request_path) + return {"error": {"code": -32001, + "message": "bridge timed out after %dms — is the running game using the _MCPGameBridge autoload? Re-enable the plugin to re-register it, then press F5 again." % timeout_ms}} + + +# logs.read — pull recent log lines captured by the game-bridge autoload. +# Params: {clear?: false, max_lines?: 200} +static func logs_read(params: Dictionary) -> Dictionary: + var clear: bool = params.get("clear", false) + var max_lines: int = int(params.get("max_lines", 200)) + var logs_path := "res://.godot/agent_tools/bridge/logs.json" + if not FileAccess.file_exists(logs_path): + return {"data": { + "entries": [], + "count": 0, + "note": "no log buffer found — the game may not be running or the autoload isn't initialized. Press F5 and try again.", + }} + var f := FileAccess.open(logs_path, FileAccess.READ) + if f == null: + return {"error": {"code": -32001, "message": "could not read logs"}} + var parsed = JSON.parse_string(f.get_as_text()) + f.close() + if typeof(parsed) != TYPE_ARRAY: + parsed = [] + var entries: Array = parsed + if entries.size() > max_lines: + entries = entries.slice(entries.size() - max_lines) + if clear: + @warning_ignore("return_value_discarded") + DirAccess.remove_absolute(logs_path) + return {"data": {"entries": entries, "count": entries.size()}} + + +# logs.clear — drop the log buffer regardless of read state. +static func logs_clear(_params: Dictionary) -> Dictionary: + var logs_path := "res://.godot/agent_tools/bridge/logs.json" + if FileAccess.file_exists(logs_path): + @warning_ignore("return_value_discarded") + DirAccess.remove_absolute(logs_path) + return {"data": {"cleared": true}} diff --git a/addons/agent_tools/tools/editor_tools.gd.uid b/addons/agent_tools/tools/editor_tools.gd.uid new file mode 100644 index 0000000..04e77c4 --- /dev/null +++ b/addons/agent_tools/tools/editor_tools.gd.uid @@ -0,0 +1 @@ +uid://bgoufu3l40rvx diff --git a/addons/agent_tools/tools/fs_tools.gd b/addons/agent_tools/tools/fs_tools.gd new file mode 100644 index 0000000..7301165 --- /dev/null +++ b/addons/agent_tools/tools/fs_tools.gd @@ -0,0 +1,144 @@ +@tool +extends RefCounted + +# fs.list — enumerate project files by type, with optional glob filter. +# Params: {type?: "all"|"scene"|"script"|"resource"|"shader"|"image"|"audio", +# glob?: "res://scenes/**/*.tscn", +# include_addons?: false} +# Returns: {type, count, files: [paths]} +static func list(params: Dictionary) -> Dictionary: + var type_filter: String = params.get("type", "all") + var glob: String = params.get("glob", "") + var include_addons: bool = params.get("include_addons", false) + + var exts: Array + match type_filter: + "", "all": + exts = ["gd", "cs", "tscn", "tres", "res", "gdshader", "gdshaderinc", + "png", "jpg", "jpeg", "svg", "webp", + "ogg", "wav", "mp3", + "json", "cfg"] + "scene": + exts = ["tscn"] + "script": + exts = ["gd", "cs"] + "resource": + exts = ["tres", "res"] + "shader": + exts = ["gdshader", "gdshaderinc"] + "image": + exts = ["png", "jpg", "jpeg", "svg", "webp"] + "audio": + exts = ["ogg", "wav", "mp3"] + _: + return _err(-32602, "unknown type: %s (use all|scene|script|resource|shader|image|audio)" % type_filter) + + var files: Array = [] + _walk("res://", exts, files, include_addons) + + if glob != "": + var filtered: Array = [] + for f in files: + if f.matchn(glob): # case-insensitive glob match + filtered.append(f) + files = filtered + + files.sort() + return _ok({ + "type": type_filter, + "count": files.size(), + "files": files, + }) + + +# fs.read_text — read a text file under res://. Complement to user_fs.read. +# Params: {path} +static func read_text(params: Dictionary) -> Dictionary: + var path: String = params.get("path", "") + if path == "": + return _err(-32602, "missing 'path'") + if not path.begins_with("res://"): + return _err(-32602, "path must begin with 'res://' (use user_fs.read for user:// files)") + if not _within_sandbox(path, "res://"): + return _err(-32602, "path escapes the res:// project sandbox ('..' traversal not allowed): %s" % path) + var f := FileAccess.open(path, FileAccess.READ) + if f == null: + return _err(-32001, "failed to open %s (error %d)" % [path, FileAccess.get_open_error()]) + var size_bytes := f.get_length() + var content := f.get_as_text() + f.close() + return _ok({"path": path, "content": content, "size_bytes": size_bytes}) + + +# fs.write_text — write a text file under res://. Creates parent directories +# if needed. Triggers the editor's filesystem rescan so the new file shows up +# in the FileSystem dock. +# Params: {path, content, overwrite?: false} +static func write_text(params: Dictionary) -> Dictionary: + var path: String = params.get("path", "") + var content: String = str(params.get("content", "")) + var overwrite: bool = params.get("overwrite", false) + if path == "": + return _err(-32602, "missing 'path'") + if not path.begins_with("res://"): + return _err(-32602, "path must begin with 'res://'") + if not _within_sandbox(path, "res://"): + return _err(-32602, "path escapes the res:// project sandbox ('..' traversal not allowed): %s" % path) + if FileAccess.file_exists(path) and not overwrite: + return _err(-32602, "file exists (pass overwrite:true to replace): %s" % path) + + var dir_path := path.get_base_dir() + if not DirAccess.dir_exists_absolute(dir_path): + var derr := DirAccess.make_dir_recursive_absolute(dir_path) + if derr != OK: + return _err(-32001, "mkdir failed (%d): %s" % [derr, dir_path]) + + var f := FileAccess.open(path, FileAccess.WRITE) + if f == null: + return _err(-32001, "could not open for write: %s" % path) + f.store_string(content) + f.close() + EditorInterface.get_resource_filesystem().update_file(path) + return _ok({"path": path, "bytes_written": content.length()}) + + +static func _walk(dir_path: String, exts: Array, out: Array, include_addons: bool) -> void: + var d := DirAccess.open(dir_path) + if d == null: + return + d.list_dir_begin() + while true: + var name := d.get_next() + if name == "": + break + if name.begins_with("."): + continue + var full := dir_path.path_join(name) + if d.current_is_dir(): + if not include_addons and full == "res://addons/agent_tools": + continue # always skip our own addon + _walk(full, exts, out, include_addons) + else: + for e in exts: + if name.ends_with("." + e): + out.append(full) + break + d.list_dir_end() + + +# Path-traversal guard. `begins_with("res://")` is NOT a sandbox — Godot resolves +# `..` segments against the real filesystem, so "res://../../etc/x" escapes the +# project. Globalize to an absolute path, collapse `..`, and confirm the result +# stays inside the (globalized, collapsed) prefix root. +static func _within_sandbox(path: String, prefix: String) -> bool: + var root := ProjectSettings.globalize_path(prefix).simplify_path().trim_suffix("/") + var abs := ProjectSettings.globalize_path(path).simplify_path() + return abs == root or abs.begins_with(root + "/") + + +static func _ok(data) -> Dictionary: + return {"data": data} + + +static func _err(code: int, msg: String) -> Dictionary: + return {"error": {"code": code, "message": msg}} diff --git a/addons/agent_tools/tools/fs_tools.gd.uid b/addons/agent_tools/tools/fs_tools.gd.uid new file mode 100644 index 0000000..f64858c --- /dev/null +++ b/addons/agent_tools/tools/fs_tools.gd.uid @@ -0,0 +1 @@ +uid://dxq0mo7kx65wd diff --git a/addons/agent_tools/tools/input_tools.gd b/addons/agent_tools/tools/input_tools.gd new file mode 100644 index 0000000..4621907 --- /dev/null +++ b/addons/agent_tools/tools/input_tools.gd @@ -0,0 +1,243 @@ +@tool +extends RefCounted + +# input_map.add_action — register a new input action in project.godot. +# Params: {name, deadzone?: 0.5} +static func add_action(params: Dictionary) -> Dictionary: + var action_name: String = params.get("name", "") + if action_name == "": + return _err(-32602, "missing 'name'") + var deadzone: float = float(params.get("deadzone", 0.5)) + var key := "input/" + action_name + if ProjectSettings.has_setting(key): + return _err(-32602, "action already exists: %s" % action_name) + ProjectSettings.set_setting(key, { + "deadzone": deadzone, + "events": [], + }) + var err := ProjectSettings.save() + if err != OK: + return _err(-32001, "save failed: %d" % err) + # Keep the runtime InputMap in sync with the on-disk project settings — + # ProjectSettings.save() only writes the file; it doesn't register actions + # with InputMap until editor restart. + if not InputMap.has_action(action_name): + InputMap.add_action(action_name, deadzone) + return _ok({"added": action_name}) + + +# input_map.add_event — attach an input event to an existing action. +# Supported event shapes (pick one via 'type'): +# {type: "key", keycode: "A" | "Space" | "F1" ..., physical?: true} +# {type: "mouse_button", button_index: 1 (left) | 2 (right) | 3 (middle)} +# {type: "joy_button", button_index: 0..} +# Params: {action, event: } +static func add_event(params: Dictionary) -> Dictionary: + var action_name: String = params.get("action", "") + var event_spec: Dictionary = params.get("event", {}) + if action_name == "": + return _err(-32602, "missing 'action'") + if event_spec.is_empty(): + return _err(-32602, "missing 'event'") + var key := "input/" + action_name + if not ProjectSettings.has_setting(key): + return _err(-32001, "action not found: %s" % action_name) + + var event := _build_event(event_spec) + if event == null: + return _err(-32602, "invalid event spec — see tool description for supported shapes") + + var current: Dictionary = ProjectSettings.get_setting(key) + var events_arr: Array = current.get("events", []) + events_arr.append(event) + current["events"] = events_arr + ProjectSettings.set_setting(key, current) + var err := ProjectSettings.save() + if err != OK: + return _err(-32001, "save failed: %d" % err) + # Sync InputMap so Input.is_action_pressed() etc. see the event without restart. + if InputMap.has_action(action_name): + InputMap.action_add_event(action_name, event) + return _ok({"action": action_name, "event": event_spec}) + + +# input_map.list — return registered input actions with their events. +# Params: {include_builtins?: false} +# Defaults to user-defined only (parses the [input] section of project.godot, +# which contains only user overrides — Godot's ~90 ui_* builtins live in-memory). +# When include_builtins, falls back to InputMap which has the full union. +static func list_actions(params: Dictionary) -> Dictionary: + var include_builtins: bool = params.get("include_builtins", false) + var items: Array = [] + + if include_builtins: + for a in InputMap.get_actions(): + var described: Array = [] + for e in InputMap.action_get_events(a): + described.append(_describe_event(e)) + items.append({ + "name": String(a), + "deadzone": InputMap.action_get_deadzone(a), + "events": described, + }) + else: + # Read straight from project.godot so newly-added actions show up even + # if they haven't been registered with the runtime InputMap yet. + var cf := ConfigFile.new() + var err := cf.load("res://project.godot") + if err == OK and cf.has_section("input"): + for action_name in cf.get_section_keys("input"): + var value: Dictionary = cf.get_value("input", action_name) + var described: Array = [] + for e in value.get("events", []): + described.append(_describe_event(e)) + items.append({ + "name": action_name, + "deadzone": value.get("deadzone", 0.5), + "events": described, + }) + + return _ok({"actions": items}) + + +# input_map.remove_event — remove an event from an action by index. +# Call input_map.list first to see indices; events are listed in the order they +# were added. +# Params: {action, event_index} +static func remove_event(params: Dictionary) -> Dictionary: + var action_name: String = params.get("action", "") + if action_name == "": + return _err(-32602, "missing 'action'") + if not params.has("event_index"): + return _err(-32602, "missing 'event_index'") + var idx: int = int(params.event_index) + var key := "input/" + action_name + if not ProjectSettings.has_setting(key): + return _err(-32001, "action not found: %s" % action_name) + var current: Dictionary = ProjectSettings.get_setting(key) + var events_arr: Array = current.get("events", []) + if idx < 0 or idx >= events_arr.size(): + return _err(-32602, "event_index %d out of range (0..%d)" % [idx, events_arr.size() - 1]) + var removed_event = events_arr[idx] + events_arr.remove_at(idx) + current["events"] = events_arr + ProjectSettings.set_setting(key, current) + var err := ProjectSettings.save() + if err != OK: + return _err(-32001, "save failed: %d" % err) + if InputMap.has_action(action_name) and removed_event != null: + InputMap.action_erase_event(action_name, removed_event) + return _ok({"action": action_name, "removed_index": idx}) + + +# input_map.remove_action — delete a user-registered action. +static func remove_action(params: Dictionary) -> Dictionary: + var action_name: String = params.get("name", "") + if action_name == "": + return _err(-32602, "missing 'name'") + var key := "input/" + action_name + if not ProjectSettings.has_setting(key): + return _err(-32001, "action not found: %s" % action_name) + ProjectSettings.clear(key) + var err := ProjectSettings.save() + if err != OK: + return _err(-32001, "save failed: %d" % err) + if InputMap.has_action(action_name): + InputMap.erase_action(action_name) + return _ok({"removed": action_name}) + + +static func _build_event(spec: Dictionary) -> InputEvent: + # device defaults to -1 (ALL_DEVICES) matching the editor's "All Devices" default. + # For local multiplayer bind device=0, device=1, etc. to distinguish controllers. + var device: int = int(spec.get("device", -1)) + match spec.get("type", ""): + "key": + var e := InputEventKey.new() + e.device = device + var kc_raw = spec.get("keycode", "") + var keycode := 0 + if kc_raw is int: + keycode = kc_raw + elif kc_raw is String: + keycode = OS.find_keycode_from_string(kc_raw) + if keycode == 0: + return null + if spec.get("physical", true): + e.physical_keycode = keycode + else: + e.keycode = keycode + return e + "mouse_button": + var e := InputEventMouseButton.new() + e.device = device + e.button_index = int(spec.get("button_index", 1)) + return e + "joy_button": + var e := InputEventJoypadButton.new() + e.device = device + e.button_index = int(spec.get("button_index", 0)) + return e + "joy_motion": + # axis accepts int 0..5 or one of: + # "left_x" / "left_y" / "right_x" / "right_y" / "trigger_left" / "trigger_right" + # axis_value: -1.0 (full negative) to 1.0 (full positive); sign picks the direction + # that triggers the action. + var e := InputEventJoypadMotion.new() + e.device = device + e.axis = _coerce_axis(spec.get("axis", 0)) + e.axis_value = float(spec.get("axis_value", 1.0)) + return e + _: + return null + + +static func _coerce_axis(v) -> int: + if v is int: + return v + if v is String: + match v: + "left_x": + return JOY_AXIS_LEFT_X + "left_y": + return JOY_AXIS_LEFT_Y + "right_x": + return JOY_AXIS_RIGHT_X + "right_y": + return JOY_AXIS_RIGHT_Y + "trigger_left": + return JOY_AXIS_TRIGGER_LEFT + "trigger_right": + return JOY_AXIS_TRIGGER_RIGHT + return 0 + + +static func _describe_event(e) -> Dictionary: + if e is InputEventKey: + var kc: int = e.physical_keycode if e.physical_keycode != 0 else e.keycode + return { + "type": "key", + "keycode": OS.get_keycode_string(kc), + "physical": e.physical_keycode != 0, + "device": e.device, + } + if e is InputEventMouseButton: + return {"type": "mouse_button", "button_index": e.button_index, "device": e.device} + if e is InputEventJoypadButton: + return {"type": "joy_button", "button_index": e.button_index, "device": e.device} + if e is InputEventJoypadMotion: + return { + "type": "joy_motion", + "axis": e.axis, + "axis_value": e.axis_value, + "device": e.device, + } + return {"type": "unknown", "class": (e as Object).get_class() if e else "null"} + + +static func _ok(data) -> Dictionary: + return {"data": data} + + +static func _err(code: int, msg: String) -> Dictionary: + return {"error": {"code": code, "message": msg}} diff --git a/addons/agent_tools/tools/input_tools.gd.uid b/addons/agent_tools/tools/input_tools.gd.uid new file mode 100644 index 0000000..6da1139 --- /dev/null +++ b/addons/agent_tools/tools/input_tools.gd.uid @@ -0,0 +1 @@ +uid://dp1m17i4hu7t1 diff --git a/addons/agent_tools/tools/performance_tools.gd b/addons/agent_tools/tools/performance_tools.gd new file mode 100644 index 0000000..d40c7a4 --- /dev/null +++ b/addons/agent_tools/tools/performance_tools.gd @@ -0,0 +1,56 @@ +@tool +extends RefCounted + +# performance.monitors — read Godot's built-in performance monitors. +# Params: {monitors?: [string]} — specific monitor names, default = common set +# Returns: {fps, frame_time_ms, process_time_ms, physics_time_ms, mem_static_mb, +# mem_dynamic_mb, objects, resources, nodes, draw_calls, ...} +static func monitors(params: Dictionary) -> Dictionary: + var requested: Array = params.get("monitors", []) + var out: Dictionary = {} + + # Default set — the monitors agents actually want for quick perf snapshots. + if requested.is_empty(): + out["fps"] = Performance.get_monitor(Performance.TIME_FPS) + out["frame_time_ms"] = Performance.get_monitor(Performance.TIME_PROCESS) * 1000.0 + out["physics_time_ms"] = Performance.get_monitor(Performance.TIME_PHYSICS_PROCESS) * 1000.0 + out["mem_static_mb"] = Performance.get_monitor(Performance.MEMORY_STATIC) / 1048576.0 + out["mem_static_max_mb"] = Performance.get_monitor(Performance.MEMORY_STATIC_MAX) / 1048576.0 + out["object_count"] = int(Performance.get_monitor(Performance.OBJECT_COUNT)) + out["resource_count"] = int(Performance.get_monitor(Performance.OBJECT_RESOURCE_COUNT)) + out["node_count"] = int(Performance.get_monitor(Performance.OBJECT_NODE_COUNT)) + out["orphan_node_count"] = int(Performance.get_monitor(Performance.OBJECT_ORPHAN_NODE_COUNT)) + out["draw_calls"] = int(Performance.get_monitor(Performance.RENDER_TOTAL_DRAW_CALLS_IN_FRAME)) + else: + for name in requested: + var enum_value := _resolve_monitor_name(String(name)) + if enum_value == -1: + out[name] = null + else: + out[name] = Performance.get_monitor(enum_value) + + return _ok({"monitors": out}) + + +# Map friendly strings to Performance.* enum values so callers don't have to +# remember the integer IDs. +static func _resolve_monitor_name(name: String) -> int: + match name: + "fps": return Performance.TIME_FPS + "frame_time": return Performance.TIME_PROCESS + "physics_time": return Performance.TIME_PHYSICS_PROCESS + "mem_static": return Performance.MEMORY_STATIC + "mem_static_max": return Performance.MEMORY_STATIC_MAX + "objects": return Performance.OBJECT_COUNT + "resources": return Performance.OBJECT_RESOURCE_COUNT + "nodes": return Performance.OBJECT_NODE_COUNT + "orphan_nodes": return Performance.OBJECT_ORPHAN_NODE_COUNT + "draw_calls": return Performance.RENDER_TOTAL_DRAW_CALLS_IN_FRAME + "audio_latency": return Performance.AUDIO_OUTPUT_LATENCY + "physics_2d_active_objects": return Performance.PHYSICS_2D_ACTIVE_OBJECTS + "physics_3d_active_objects": return Performance.PHYSICS_3D_ACTIVE_OBJECTS + _: return -1 + + +static func _ok(data) -> Dictionary: + return {"data": data} diff --git a/addons/agent_tools/tools/performance_tools.gd.uid b/addons/agent_tools/tools/performance_tools.gd.uid new file mode 100644 index 0000000..27885bf --- /dev/null +++ b/addons/agent_tools/tools/performance_tools.gd.uid @@ -0,0 +1 @@ +uid://bijue5qgitvnk diff --git a/addons/agent_tools/tools/physics_tools.gd b/addons/agent_tools/tools/physics_tools.gd new file mode 100644 index 0000000..b5aa478 --- /dev/null +++ b/addons/agent_tools/tools/physics_tools.gd @@ -0,0 +1,139 @@ +@tool +extends RefCounted + +# physics.autofit_collision_shape_2d — compute a CollisionShape2D sized to a +# sibling/child Sprite2D or AnimatedSprite2D's visual bounds. Common pattern +# that's tedious to set up by hand: you have a sprite, you want a rectangle or +# circle collider that roughly matches it. +# +# Params: { +# node_path: CollisionShape2D to fit (created if missing when create:true) +# source: NodePath to a Sprite2D / AnimatedSprite2D (auto-detected +# among siblings if omitted) +# shape: "rectangle" | "circle" | "capsule" (default "rectangle") +# margin: float pixels to shrink by (default 0) +# create: bool — if true and node_path doesn't exist, create it as +# a child of the source's parent. Default false. +# } +static func autofit_collision_shape_2d(params: Dictionary) -> Dictionary: + var node_path: String = params.get("node_path", "") + var source_path: String = params.get("source", "") + var shape_type: String = params.get("shape", "rectangle") + var margin: float = float(params.get("margin", 0.0)) + var create: bool = params.get("create", false) + + if node_path == "": + return _err(-32602, "missing 'node_path'") + + var root := EditorInterface.get_edited_scene_root() + if root == null: + return _err(-32001, "no scene open") + + var target: Node = root if node_path == "." else root.get_node_or_null(node_path) + if target == null and not create: + return _err(-32001, "node not found: %s (pass create:true to auto-create)" % node_path) + + # Resolve the visual source. + var source: Node = null + if source_path != "": + source = root.get_node_or_null(source_path) + if source == null: + return _err(-32001, "source not found: %s" % source_path) + elif target != null: + # Look among siblings for a Sprite2D / AnimatedSprite2D. + var parent := target.get_parent() + if parent: + for sib in parent.get_children(): + if sib is Sprite2D or sib is AnimatedSprite2D: + source = sib + break + if source == null: + return _err(-32001, "no Sprite2D/AnimatedSprite2D source found — pass 'source'") + + # Compute visual bounds (untransformed; CollisionShape2D inherits parent's transform). + var bounds := _sprite_bounds(source) + if bounds.size.x <= 0 or bounds.size.y <= 0: + return _err(-32001, "source has no visible extents (%s)" % source.get_class()) + + # Create the CollisionShape2D if needed. + if target == null and create: + var inst := CollisionShape2D.new() + inst.name = node_path.get_file() if "/" in node_path else node_path + var parent := source.get_parent() + parent.add_child(inst) + inst.owner = root + target = inst + + if not target is CollisionShape2D: + return _err(-32602, "target is not a CollisionShape2D: %s (%s)" % [node_path, target.get_class()]) + var cs: CollisionShape2D = target + + # Build the requested shape sized to bounds. + var shape_res: Shape2D + match shape_type: + "rectangle": + var r := RectangleShape2D.new() + r.size = Vector2( + max(1.0, bounds.size.x - margin * 2.0), + max(1.0, bounds.size.y - margin * 2.0) + ) + shape_res = r + "circle": + var c := CircleShape2D.new() + c.radius = max(1.0, min(bounds.size.x, bounds.size.y) / 2.0 - margin) + shape_res = c + "capsule": + var cap := CapsuleShape2D.new() + cap.radius = max(1.0, min(bounds.size.x, bounds.size.y) / 2.0 - margin) + cap.height = max(cap.radius * 2.0 + 1.0, bounds.size.y - margin * 2.0) + shape_res = cap + _: + return _err(-32602, "unknown shape '%s' (supported: rectangle, circle, capsule)" % shape_type) + + cs.shape = shape_res + # Center the CollisionShape2D on the sprite's center. + cs.position = bounds.position + bounds.size / 2.0 + EditorInterface.mark_scene_as_unsaved() + + return _ok({ + "node_path": String(root.get_path_to(cs)), + "shape": shape_type, + "size": [bounds.size.x - margin * 2.0, bounds.size.y - margin * 2.0], + "center": [cs.position.x, cs.position.y], + "source": String(root.get_path_to(source)), + }) + + +static func _sprite_bounds(source: Node) -> Rect2: + if source is Sprite2D: + var s: Sprite2D = source + if s.texture == null: + return Rect2(Vector2.ZERO, Vector2.ZERO) + var tex_size: Vector2 = s.texture.get_size() + if s.region_enabled: + tex_size = s.region_rect.size + var size: Vector2 = tex_size * Vector2(abs(s.scale.x), abs(s.scale.y)) + # For centered sprites, bounds are at (-size/2, -size/2). + var offset: Vector2 = -size / 2.0 if s.centered else Vector2.ZERO + offset += s.offset + return Rect2(offset, size) + if source is AnimatedSprite2D: + var a: AnimatedSprite2D = source + if a.sprite_frames == null or a.animation == "": + return Rect2(Vector2.ZERO, Vector2.ZERO) + var tex := a.sprite_frames.get_frame_texture(a.animation, a.frame) + if tex == null: + return Rect2(Vector2.ZERO, Vector2.ZERO) + var size: Vector2 = tex.get_size() * Vector2(abs(a.scale.x), abs(a.scale.y)) + var offset: Vector2 = -size / 2.0 if a.centered else Vector2.ZERO + offset += a.offset + return Rect2(offset, size) + return Rect2(Vector2.ZERO, Vector2.ZERO) + + +static func _ok(data) -> Dictionary: + return {"data": data} + + +static func _err(code: int, msg: String) -> Dictionary: + return {"error": {"code": code, "message": msg}} diff --git a/addons/agent_tools/tools/physics_tools.gd.uid b/addons/agent_tools/tools/physics_tools.gd.uid new file mode 100644 index 0000000..855a8c4 --- /dev/null +++ b/addons/agent_tools/tools/physics_tools.gd.uid @@ -0,0 +1 @@ +uid://c26p52t0pcj6q diff --git a/addons/agent_tools/tools/project_tools.gd b/addons/agent_tools/tools/project_tools.gd new file mode 100644 index 0000000..0265584 --- /dev/null +++ b/addons/agent_tools/tools/project_tools.gd @@ -0,0 +1,89 @@ +@tool +extends RefCounted + +# project.get_setting — read a value from project.godot. Returns {exists: false} if unset. +# Params: {key: "application/config/name"} +static func get_setting(params: Dictionary) -> Dictionary: + var key: String = params.get("key", "") + if key == "": + return _err(-32602, "missing 'key'") + if not ProjectSettings.has_setting(key): + return _ok({"key": key, "exists": false}) + return _ok({"key": key, "exists": true, "value": ProjectSettings.get_setting(key)}) + + +# project.set_setting — write a value to project.godot and save. +# DESTRUCTIVE: mutates project.godot. Prefer targeted tools (autoload.add, etc.) when available. +# Params: {key, value} +static func set_setting(params: Dictionary) -> Dictionary: + var key: String = params.get("key", "") + if key == "": + return _err(-32602, "missing 'key'") + if not params.has("value"): + return _err(-32602, "missing 'value'") + ProjectSettings.set_setting(key, params["value"]) + var err := ProjectSettings.save() + if err != OK: + return _err(-32001, "ProjectSettings.save() failed: %d" % err) + return _ok({"key": key, "value": params["value"]}) + + +# autoload.add — register an autoload (singleton by default). +# Params: {name, path, singleton?: true} +static func autoload_add(params: Dictionary) -> Dictionary: + var autoload_name: String = params.get("name", "") + var path: String = params.get("path", "") + var singleton: bool = params.get("singleton", true) + if autoload_name == "" or path == "": + return _err(-32602, "missing 'name' or 'path'") + if not ResourceLoader.exists(path): + return _err(-32001, "path does not exist: %s" % path) + + var value := ("*" + path) if singleton else path + ProjectSettings.set_setting("autoload/" + autoload_name, value) + var err := ProjectSettings.save() + if err != OK: + return _err(-32001, "save failed: %d" % err) + return _ok({"name": autoload_name, "path": path, "singleton": singleton}) + + +# autoload.remove — unregister an autoload. +# Params: {name} +static func autoload_remove(params: Dictionary) -> Dictionary: + var autoload_name: String = params.get("name", "") + if autoload_name == "": + return _err(-32602, "missing 'name'") + var key := "autoload/" + autoload_name + if not ProjectSettings.has_setting(key): + return _err(-32001, "autoload not found: %s" % autoload_name) + ProjectSettings.clear(key) + var err := ProjectSettings.save() + if err != OK: + return _err(-32001, "save failed: %d" % err) + return _ok({"removed": autoload_name}) + + +# autoload.list — enumerate all registered autoloads. +static func autoload_list(_params: Dictionary) -> Dictionary: + var items: Array = [] + # ProjectSettings.get_property_list() contains our autoload entries under "autoload/*". + for p in ProjectSettings.get_property_list(): + var name: String = p.name + if not name.begins_with("autoload/"): + continue + var value: String = str(ProjectSettings.get_setting(name)) + var singleton := value.begins_with("*") + items.append({ + "name": name.trim_prefix("autoload/"), + "path": value.trim_prefix("*") if singleton else value, + "singleton": singleton, + }) + return _ok({"autoloads": items}) + + +static func _ok(data) -> Dictionary: + return {"data": data} + + +static func _err(code: int, msg: String) -> Dictionary: + return {"error": {"code": code, "message": msg}} diff --git a/addons/agent_tools/tools/project_tools.gd.uid b/addons/agent_tools/tools/project_tools.gd.uid new file mode 100644 index 0000000..61be430 --- /dev/null +++ b/addons/agent_tools/tools/project_tools.gd.uid @@ -0,0 +1 @@ +uid://bsfbwkk5nas36 diff --git a/addons/agent_tools/tools/refs_tools.gd b/addons/agent_tools/tools/refs_tools.gd new file mode 100644 index 0000000..cef255d --- /dev/null +++ b/addons/agent_tools/tools/refs_tools.gd @@ -0,0 +1,371 @@ +@tool +extends RefCounted + +# refs.validate_project — project-wide sweep. No params. +# Returns {checked: int, issues: [{path, kind, ...}]}. +# Kinds: script_parse_error, scene_load_failed, scene_instantiate_failed, +# resource_load_failed, missing_ext_resource, missing_uid, +# signal_method_missing, signal_target_missing. +static func validate_project(_params: Dictionary) -> Dictionary: + var issues: Array = [] + var files: Array = [] + _walk("res://", ["gd", "tscn", "tres"], files) + + for path in files: + if path.begins_with("res://addons/agent_tools/"): + continue # don't validate ourselves + if path.ends_with(".gd"): + _check_script(path, issues) + elif path.ends_with(".tscn"): + _check_scene(path, issues) + elif path.ends_with(".tres"): + _check_resource(path, issues) + + return {"data": {"checked": files.size(), "issues": issues}} + + +static func _walk(dir_path: String, exts: Array, out: Array) -> void: + var d := DirAccess.open(dir_path) + if d == null: + return + d.list_dir_begin() + while true: + var name := d.get_next() + if name == "": + break + if name.begins_with("."): + continue + var full := dir_path.path_join(name) + if d.current_is_dir(): + _walk(full, exts, out) + else: + for e in exts: + if name.ends_with("." + e): + out.append(full) + break + d.list_dir_end() + + +static func _check_script(path: String, issues: Array) -> void: + # Go through the normal loader so class_name / preload chains resolve against + # the real global registry. A detached GDScript.new() + reload() would false-positive + # on any script with class_name (duplicate global class registration). + var script := ResourceLoader.load(path, "Script") + if script == null: + issues.append({"path": path, "kind": "script_load_failed"}) + + +static func _check_scene(path: String, issues: Array) -> void: + _check_ext_resources_text(path, issues) + + var packed := ResourceLoader.load(path, "PackedScene") as PackedScene + if packed == null: + issues.append({"path": path, "kind": "scene_load_failed"}) + return + var root := packed.instantiate(PackedScene.GEN_EDIT_STATE_DISABLED) + if root == null: + issues.append({"path": path, "kind": "scene_instantiate_failed"}) + return + _walk_scene_signals(root, root, path, issues) + root.free() + + +static func _walk_scene_signals(node: Node, scene_root: Node, scene_path: String, issues: Array) -> void: + for sig in node.get_signal_list(): + var sig_name: String = sig.name + for c in node.get_signal_connection_list(sig_name): + var flags: int = c.flags + if not (flags & CONNECT_PERSIST): + continue # ignore runtime-only connections + var cb: Callable = c.callable + var target: Object = cb.get_object() + var method: String = cb.get_method() + if target == null: + issues.append({ + "path": scene_path, + "kind": "signal_target_missing", + "node": String(scene_root.get_path_to(node)), + "signal": sig_name, + }) + elif not target.has_method(method): + issues.append({ + "path": scene_path, + "kind": "signal_method_missing", + "node": String(scene_root.get_path_to(node)), + "signal": sig_name, + "method": method, + }) + for child in node.get_children(): + if child.owner == scene_root or child == scene_root: + _walk_scene_signals(child, scene_root, scene_path, issues) + + +static func _check_resource(path: String, issues: Array) -> void: + var res := ResourceLoader.load(path) + if res == null: + issues.append({"path": path, "kind": "resource_load_failed"}) + + +# refs.find_usages — find every file that references a given resource. +# Target may be either a path ("res://player.gd") or a uid ("uid://abc123"). +# Searches for both forms so you catch uid-indirected references too. +# Params: {target: "res://..." | "uid://..."} +static func find_usages(params: Dictionary) -> Dictionary: + var target: String = params.get("target", "") + if target == "": + return _err(-32602, "missing 'target'") + + var terms: Array = [target] + if target.begins_with("uid://"): + var id := ResourceUID.text_to_id(target) + if id != -1 and ResourceUID.has_id(id): + var p := ResourceUID.get_id_path(id) + if p != "" and p != target: + terms.append(p) + else: + var id := ResourceLoader.get_resource_uid(target) + if id != -1: + terms.append(ResourceUID.id_to_text(id)) + + var files: Array = [] + _walk("res://", ["gd", "tscn", "tres", "cfg", "gdshader", "gdshaderinc", "cs", "godot"], files) + + var matches: Array = [] + for path in files: + if path.begins_with("res://addons/agent_tools/"): + continue + if path == target: + continue # skip the file itself + var f := FileAccess.open(path, FileAccess.READ) + if f == null: + continue + var line_num := 0 + while not f.eof_reached(): + line_num += 1 + var line := f.get_line() + for term in terms: + if term in line: + matches.append({ + "path": path, + "line": line_num, + "matched": term, + "text": line.strip_edges(), + }) + break + + return _ok({"target": target, "terms_searched": terms, "matches": matches}) + + +# refs.rename — move a file and rewrite all references to it. +# Path-form references are text-replaced; uid-form references keep working because +# the .uid sidecar moves with the file (preserving the UID → new path mapping). +# Params: {from, to, overwrite?: false, dry_run?: false} +static func rename(params: Dictionary) -> Dictionary: + var from_path: String = params.get("from", "") + var to_path: String = params.get("to", "") + var overwrite: bool = params.get("overwrite", false) + var dry_run: bool = params.get("dry_run", false) + + if from_path == "" or to_path == "": + return _err(-32602, "missing 'from' or 'to'") + if not (from_path.begins_with("res://") and to_path.begins_with("res://")): + return _err(-32602, "paths must be 'res://...'") + if from_path == to_path: + return _err(-32602, "'from' and 'to' are the same") + if not FileAccess.file_exists(from_path): + return _err(-32001, "source not found: %s" % from_path) + if FileAccess.file_exists(to_path) and not overwrite: + return _err(-32602, "destination exists (pass overwrite:true): %s" % to_path) + + # Collect path-form usages. UID-form references are preserved automatically when + # the .uid sidecar moves with the file, so we don't need to rewrite them. + var usage := find_usages({"target": from_path}) + if usage.has("error"): + return usage + var edits: Dictionary = {} + for m in usage.data.matches: + if m.matched != from_path: + continue # uid-form match — no rewrite needed + if not edits.has(m.path): + edits[m.path] = true + + if dry_run: + return _ok({ + "would_move": {"from": from_path, "to": to_path}, + "would_update": edits.keys(), + }) + + var dir_path := to_path.get_base_dir() + if not DirAccess.dir_exists_absolute(dir_path): + var derr := DirAccess.make_dir_recursive_absolute(dir_path) + if derr != OK: + return _err(-32001, "mkdir failed (%d): %s" % [derr, dir_path]) + + var move_err := DirAccess.rename_absolute(from_path, to_path) + if move_err != OK: + return _err(-32001, "move failed (%d)" % move_err) + + # Move .uid sidecar so UIDs continue resolving to the new path. Best-effort — + # sidecar failures don't block the main rename that already succeeded. + var from_uid := from_path + ".uid" + if FileAccess.file_exists(from_uid): + @warning_ignore("return_value_discarded") + DirAccess.rename_absolute(from_uid, to_path + ".uid") + # Move .import sidecar for imported assets (textures, audio, etc.). + var from_import := from_path + ".import" + if FileAccess.file_exists(from_import): + @warning_ignore("return_value_discarded") + DirAccess.rename_absolute(from_import, to_path + ".import") + + var files_updated: Array = [] + for ref_path in edits.keys(): + var rf := FileAccess.open(ref_path, FileAccess.READ) + if rf == null: + continue + var content := rf.get_as_text() + rf.close() + var new_content := content.replace(from_path, to_path) + if new_content == content: + continue + var wf := FileAccess.open(ref_path, FileAccess.WRITE) + if wf == null: + continue + wf.store_string(new_content) + wf.close() + files_updated.append(ref_path) + + EditorInterface.get_resource_filesystem().scan() + return _ok({ + "moved": {"from": from_path, "to": to_path}, + "files_updated": files_updated, + }) + + +# refs.rename_class — rename `class_name X` to `class_name Y` and rewrite every +# word-boundary reference to X across .gd / .tscn / .tres files. Best-effort: +# won't rewrite an X that happens to be a local variable name (word boundary +# alone isn't semantic analysis). Use dry_run first on anything non-trivial. +# Params: {from, to, dry_run?: false} +static func rename_class(params: Dictionary) -> Dictionary: + var from_name: String = params.get("from", "") + var to_name: String = params.get("to", "") + var dry_run: bool = params.get("dry_run", false) + + if from_name == "" or to_name == "": + return _err(-32602, "missing 'from' or 'to'") + if not _is_valid_identifier(from_name): + return _err(-32602, "'from' is not a valid identifier: %s" % from_name) + if not _is_valid_identifier(to_name): + return _err(-32602, "'to' is not a valid identifier: %s" % to_name) + if from_name == to_name: + return _err(-32602, "'from' and 'to' are identical") + + var defining_script := _find_class_definition(from_name) + if defining_script == "": + return _err(-32001, "no script found declaring class_name %s" % from_name) + if _find_class_definition(to_name) != "": + return _err(-32602, "class_name %s already exists" % to_name) + + var files: Array = [] + _walk("res://", ["gd", "tscn", "tres"], files) + + var regex := RegEx.new() + regex.compile("\\b%s\\b" % from_name) + + var edits: Array = [] + for path in files: + if path.begins_with("res://addons/agent_tools/"): + continue + var f := FileAccess.open(path, FileAccess.READ) + if f == null: + continue + var text := f.get_as_text() + f.close() + if regex.search(text) != null: + edits.append(path) + + if dry_run: + return _ok({ + "from": from_name, + "to": to_name, + "defining_script": defining_script, + "would_update": edits, + }) + + var updated: Array = [] + for path in edits: + var f := FileAccess.open(path, FileAccess.READ) + if f == null: + continue + var text := f.get_as_text() + f.close() + var new_text := regex.sub(text, to_name, true) + if new_text == text: + continue + var wf := FileAccess.open(path, FileAccess.WRITE) + if wf == null: + continue + wf.store_string(new_text) + wf.close() + updated.append(path) + + EditorInterface.get_resource_filesystem().scan() + return _ok({ + "from": from_name, + "to": to_name, + "defining_script": defining_script, + "updated": updated, + }) + + +static func _is_valid_identifier(s: String) -> bool: + var re := RegEx.new() + re.compile("^[A-Za-z_][A-Za-z0-9_]*$") + return re.search(s) != null + + +static func _find_class_definition(class_name_val: String) -> String: + var files: Array = [] + _walk("res://", ["gd"], files) + var re := RegEx.new() + re.compile("^\\s*class_name\\s+%s\\b" % class_name_val) + for path in files: + if path.begins_with("res://addons/agent_tools/"): + continue + var f := FileAccess.open(path, FileAccess.READ) + if f == null: + continue + var text := f.get_as_text() + f.close() + for line in text.split("\n"): + if re.search(line) != null: + return path + return "" + + +static func _ok(data) -> Dictionary: + return {"data": data} + + +static func _err(code: int, msg: String) -> Dictionary: + return {"error": {"code": code, "message": msg}} + + +# Parse the raw .tscn/.tres text for [ext_resource ... path="..."] and verify each target resolves. +# Catches references to files that were deleted or moved without updating the scene. +static func _check_ext_resources_text(path: String, issues: Array) -> void: + var f := FileAccess.open(path, FileAccess.READ) + if f == null: + return + var text := f.get_as_text() + var regex := RegEx.new() + regex.compile("\\[ext_resource[^\\]]*path=\"([^\"]+)\"") + for m in regex.search_all(text): + var ref: String = m.get_string(1) + if ref.begins_with("uid://"): + var id := ResourceUID.text_to_id(ref) + if id == -1 or not ResourceUID.has_id(id): + issues.append({"path": path, "kind": "missing_uid", "uid": ref}) + continue + if not ResourceLoader.exists(ref): + issues.append({"path": path, "kind": "missing_ext_resource", "target": ref}) diff --git a/addons/agent_tools/tools/refs_tools.gd.uid b/addons/agent_tools/tools/refs_tools.gd.uid new file mode 100644 index 0000000..6d777da --- /dev/null +++ b/addons/agent_tools/tools/refs_tools.gd.uid @@ -0,0 +1 @@ +uid://vlnvjolougnv diff --git a/addons/agent_tools/tools/resource_tools.gd b/addons/agent_tools/tools/resource_tools.gd new file mode 100644 index 0000000..363cbed --- /dev/null +++ b/addons/agent_tools/tools/resource_tools.gd @@ -0,0 +1,175 @@ +@tool +extends RefCounted + +const Coerce := preload("res://addons/agent_tools/tools/_coerce.gd") + +# resource.create — create a new .tres file. +# Params: {path, type, script?, properties?, overwrite?: false} +# 'type' must be a built-in Resource subclass (StyleBoxFlat, Theme, Curve, etc.). +# For custom Resource subclasses written in GDScript, pass 'script' pointing at the +# class's .gd file instead of 'type'. +static func create(params: Dictionary) -> Dictionary: + var path: String = params.get("path", "") + var type_name: String = params.get("type", "") + var script_path: String = params.get("script", "") + var properties: Dictionary = params.get("properties", {}) + var overwrite: bool = params.get("overwrite", false) + + if path == "" or not path.begins_with("res://") or not path.ends_with(".tres"): + return _err(-32602, "'path' must be 'res://...' ending in .tres") + if FileAccess.file_exists(path) and not overwrite: + return _err(-32602, "file exists (pass overwrite:true to replace): %s" % path) + + var res: Resource + if script_path != "": + var s := ResourceLoader.load(script_path, "Script") as Script + if s == null: + return _err(-32001, "failed to load script: %s" % script_path) + var inst = s.new() + if not (inst is Resource): + return _err(-32602, "script does not produce a Resource") + res = inst + elif type_name != "": + if not ClassDB.class_exists(type_name): + return _err(-32602, "unknown class: %s (use 'script' for custom Resource types)" % type_name) + if not ClassDB.is_parent_class(type_name, "Resource"): + return _err(-32602, "not a Resource subclass: %s" % type_name) + if not ClassDB.can_instantiate(type_name): + return _err(-32602, "class is not instantiable: %s" % type_name) + res = ClassDB.instantiate(type_name) as Resource + else: + return _err(-32602, "provide either 'type' (built-in Resource class) or 'script' (path to custom Resource .gd)") + + var apply_res := _apply_properties(res, properties) + if apply_res.has("error"): + return apply_res + + var dir_path := path.get_base_dir() + if not DirAccess.dir_exists_absolute(dir_path): + var derr := DirAccess.make_dir_recursive_absolute(dir_path) + if derr != OK: + return _err(-32001, "mkdir failed (%d): %s" % [derr, dir_path]) + + var save_err := ResourceSaver.save(res, path) + if save_err != OK: + return _err(-32001, "ResourceSaver.save failed: %d" % save_err) + + EditorInterface.get_resource_filesystem().update_file(path) + return _ok({"path": path, "class": res.get_class()}) + + +# resource.set_property — load a .tres, set one property, save it back. +# Params: {path, property, value} — same coercion as scene.set_property. +static func set_property(params: Dictionary) -> Dictionary: + var path: String = params.get("path", "") + var property_name: String = params.get("property", "") + if path == "" or property_name == "": + return _err(-32602, "missing 'path' or 'property'") + + var res := ResourceLoader.load(path) + if res == null: + return _err(-32001, "failed to load: %s" % path) + + var prop_info: Dictionary = {} + for p in res.get_property_list(): + if p.name == property_name: + prop_info = p + break + if prop_info.is_empty(): + return _err(-32602, "property not found on %s: %s" % [res.get_class(), property_name]) + if int(prop_info.get("usage", 0)) & PROPERTY_USAGE_READ_ONLY: + return _err(-32602, "property '%s' on %s is read-only" % [property_name, res.get_class()]) + + var coerced = Coerce.coerce(params.get("value"), prop_info.type) + if coerced is Dictionary and coerced.has("_error"): + return _err(-32602, coerced._error) + + res.set(property_name, coerced) + var stored = res.get(property_name) + if coerced != null and stored == null and prop_info.type == TYPE_OBJECT: + return _err(-32001, + "property '%s' on %s was not accepted (assignment silently dropped — target expects a %s; passed value type didn't match)" % + [property_name, res.get_class(), prop_info.get("hint_string", "Resource")]) + var save_err := ResourceSaver.save(res, path) + if save_err != OK: + return _err(-32001, "save failed: %d" % save_err) + EditorInterface.get_resource_filesystem().update_file(path) + return _ok({"path": path, "property": property_name, "value": Coerce.to_json(stored)}) + + +# resource.call_method — load a .tres, invoke a method, save, return the method's result. +# Useful for helper methods that aren't properties — e.g. StyleBoxFlat.set_border_width_all(4) +# or set_corner_radius_all(14). Args are coerced against the method's declared types. +# Params: {path, method, args?: [], save?: true} +static func call_method(params: Dictionary) -> Dictionary: + var path: String = params.get("path", "") + var method_name: String = params.get("method", "") + var args: Array = params.get("args", []) + var should_save: bool = params.get("save", true) + if path == "" or method_name == "": + return _err(-32602, "missing 'path' or 'method'") + + var res := ResourceLoader.load(path) + if res == null: + return _err(-32001, "failed to load: %s" % path) + if not res.has_method(method_name): + return _err(-32602, "method not found: %s.%s" % [res.get_class(), method_name]) + + var coerced_args = _coerce_args(res, method_name, args) + if coerced_args is Dictionary: + return coerced_args # error passthrough (same reason as scene.call_method: no type annotation) + var result = res.callv(method_name, coerced_args) + + if should_save: + var save_err := ResourceSaver.save(res, path) + if save_err != OK: + return _err(-32001, "save failed: %d" % save_err) + EditorInterface.get_resource_filesystem().update_file(path) + + return _ok({ + "path": path, + "method": method_name, + "return": Coerce.to_json(result), + "saved": should_save, + }) + + +static func _coerce_args(obj: Object, method_name: String, args: Array): + var method_info: Dictionary = {} + for m in obj.get_method_list(): + if m.name == method_name: + method_info = m + break + var arg_types: Array = method_info.get("args", []) + var out: Array = [] + for i in args.size(): + if i < arg_types.size(): + var coerced = Coerce.coerce(args[i], arg_types[i].type) + if coerced is Dictionary and coerced.has("_error"): + return _err(-32602, "arg %d: %s" % [i, coerced._error]) + out.append(coerced) + else: + out.append(args[i]) + return out + + +static func _apply_properties(res: Resource, props: Dictionary) -> Dictionary: + var by_name: Dictionary = {} + for p in res.get_property_list(): + by_name[p.name] = p + for key in props: + if not by_name.has(key): + return _err(-32602, "property not found on %s: %s" % [res.get_class(), key]) + var coerced = Coerce.coerce(props[key], by_name[key].type) + if coerced is Dictionary and coerced.has("_error"): + return _err(-32602, "property %s: %s" % [key, coerced._error]) + res.set(key, coerced) + return {} + + +static func _ok(data) -> Dictionary: + return {"data": data} + + +static func _err(code: int, msg: String) -> Dictionary: + return {"error": {"code": code, "message": msg}} diff --git a/addons/agent_tools/tools/resource_tools.gd.uid b/addons/agent_tools/tools/resource_tools.gd.uid new file mode 100644 index 0000000..93f86b9 --- /dev/null +++ b/addons/agent_tools/tools/resource_tools.gd.uid @@ -0,0 +1 @@ +uid://cw3smf38qys7f diff --git a/addons/agent_tools/tools/run_tools.gd b/addons/agent_tools/tools/run_tools.gd new file mode 100644 index 0000000..8b8085f --- /dev/null +++ b/addons/agent_tools/tools/run_tools.gd @@ -0,0 +1,239 @@ +@tool +extends RefCounted + +# run.scene_headless — run a scene in a child process and capture structured results. +# +# MODES +# BARE (no input_script / screenshot / screenshots / state_dump): +# Runs the target directly with --headless. Fast, no window. Right for +# "does _ready not crash" checks. +# +# DRIVEN (anything beyond "path" and "quit_after_seconds"): +# Runs the target under a shipped wrapper driver (addons/agent_tools/headless/ +# driver.tscn) that reads a per-call config, injects scripted input per frame, +# captures one or more screenshots, and dumps final scene state on quit. +# +# SCREENSHOT NOTE +# Godot 4.6's --headless forces a Dummy renderer that makes captures impossible. +# When any screenshot is requested the subprocess runs WITH a visible window +# positioned at -9999,-9999 (offscreen on multi-monitor setups) and the real +# Vulkan/OpenGL renderer. Brief window flash is unavoidable. +# +# Params: +# path: required, "res://scene.tscn" +# quit_after_seconds: default 2.0 +# extra_args: additional CLI args passed to the child godot process +# input_script: [{frame, type, ...}, ...] see event shapes below +# screenshot: single output PNG path (shorthand for one capture at quit_frame) +# screenshots: [{frame, path}, ...] multiple captures at specific frames +# resolution: "WxH", default "320x240". Only applies when screenshots requested. +# state_dump: bool, default false. When true, driver writes a JSON snapshot +# of the final scene tree (name/class/node_path/script/common +# properties) that the tool returns as result.final_state. +# seed: int, optional. Seeds the global RNG before instancing the +# target so runs are reproducible. +# +# Event types for input_script: +# {frame, type: "action_tap", action} +# {frame, type: "action_press", action, strength?} +# {frame, type: "action_release", action} +# {frame, type: "key", keycode, pressed?: true} +# {frame, type: "mouse_click", position: [x, y], button?: 1} +# {frame, type: "mouse_motion", position: [x, y]} +# +# Return includes: exit_code, output (stdout/stderr), command, errors, warnings, +# screenshots (array of {frame, path, captured: bool}), final_state (if state_dump), +# mode, quit_after_frames. + +const STATE_DUMP_PATH := "res://.godot/agent_tools/headless/state_dump.json" + + +static func scene_headless(params: Dictionary) -> Dictionary: + var scene_path: String = params.get("path", "") + var quit_after_seconds: float = float(params.get("quit_after_seconds", 2.0)) + var extra_args: Array = params.get("extra_args", []) + var input_script = params.get("input_script", null) + var resolution: String = params.get("resolution", "320x240") + var state_dump: bool = params.get("state_dump", false) + var rng_seed = params.get("seed", null) + + # Normalize both screenshot forms into a single list. + var screenshots: Array = [] + var single_shot: String = params.get("screenshot", "") + if single_shot != "": + screenshots.append({"frame": int(round(quit_after_seconds * 60.0)), "path": single_shot}) + var multi: Array = params.get("screenshots", []) + for s in multi: + if typeof(s) != TYPE_DICTIONARY or not s.has("frame") or not s.has("path"): + return _err(-32602, "each 'screenshots' entry needs {frame, path}") + screenshots.append({"frame": int(s.frame), "path": str(s.path)}) + + if scene_path == "": + return _err(-32602, "missing 'path'") + if not ResourceLoader.exists(scene_path, "PackedScene"): + return _err(-32001, "scene not found: %s" % scene_path) + + var frames: int = max(1, int(round(quit_after_seconds * 60.0))) + var use_driver := input_script != null or not screenshots.is_empty() or state_dump or rng_seed != null + var needs_window := not screenshots.is_empty() + + var run_scene_path := scene_path + if use_driver: + # Wipe any stale state dump from prior run so "file exists" is a reliable signal. + if FileAccess.file_exists(STATE_DUMP_PATH): + @warning_ignore("return_value_discarded") + DirAccess.remove_absolute(STATE_DUMP_PATH) + # Strict-typed arg: can't pass a Variant-valued ternary directly into an + # Array param (parse error). Normalize first. + var script_list: Array = [] + if input_script is Array: + script_list = input_script + var prep := _prepare_driver_config(scene_path, script_list, frames, screenshots, state_dump, rng_seed) + if prep.has("_error"): + return _err(-32001, prep._error) + run_scene_path = "res://addons/agent_tools/headless/driver.tscn" + + var exe := OS.get_executable_path() + var project_dir := ProjectSettings.globalize_path("res://") + var hard_quit_frames: int = frames + 30 + + var args: Array = ["--path", project_dir] + if needs_window: + # Real renderer required for screenshots; run windowed but offscreen. + args.append("--windowed") + args.append("--resolution") + args.append(resolution) + args.append("--position") + args.append("-9999,-9999") + else: + args.append("--headless") + args.append("--quit-after") + args.append(str(hard_quit_frames)) + for a in extra_args: + args.append(str(a)) + args.append(run_scene_path) + + var output: Array = [] + var exit_code := OS.execute(exe, args, output, true) + # str() always returns a String; avoids Variant-via-ternary parse issue. + var stdout: String = "" + if output.size() > 0: + stdout = str(output[0]) + + var diagnostics := _extract_diagnostics(stdout) + + var result: Dictionary = { + "exit_code": exit_code, + "output": stdout, + "command": "%s %s" % [exe, " ".join(args)], + "quit_after_frames": frames, + "mode": "driven" if use_driver else "bare", + "errors": diagnostics.errors, + "warnings": diagnostics.warnings, + } + if use_driver: + result["driver_scene"] = run_scene_path + if not screenshots.is_empty(): + var shot_results: Array = [] + for s in screenshots: + shot_results.append({ + "frame": s.frame, + "path": s.path, + "captured": FileAccess.file_exists(s.path), + }) + result["screenshots"] = shot_results + if state_dump: + # = not := — _load_state_dump has no return annotation so := can't infer. + var state = _load_state_dump() + if state != null: + result["final_state"] = state + return _ok(result) + + +static func _prepare_driver_config(target: String, input_script: Array, quit_frame: int, screenshots: Array, state_dump: bool, rng_seed) -> Dictionary: + var cfg_dir := "res://.godot/agent_tools/headless" + if not DirAccess.dir_exists_absolute(cfg_dir): + var derr := DirAccess.make_dir_recursive_absolute(cfg_dir) + if derr != OK: + return {"_error": "mkdir failed for %s (%d)" % [cfg_dir, derr]} + + var config := { + "target_scene": target, + "input_script": input_script, + "quit_frame": quit_frame, + "screenshots": screenshots, + "state_dump": state_dump, + "state_dump_path": STATE_DUMP_PATH, + } + if rng_seed != null: + config["seed"] = int(rng_seed) + + var cfg_path := cfg_dir + "/config.json" + var f := FileAccess.open(cfg_path, FileAccess.WRITE) + if f == null: + return {"_error": "failed to write driver config at %s" % cfg_path} + f.store_string(JSON.stringify(config, " ")) + f.close() + return {"config_path": cfg_path} + + +# Parse Godot's stdout for ERROR:/WARNING:/SCRIPT ERROR:/USER ERROR:/USER WARNING: lines. +# Returns {errors: [{category, message, context}], warnings: [{category, message}]}. +# Context is the next line if it starts with whitespace (typical "at: file:line" trailer). +static func _extract_diagnostics(stdout: String) -> Dictionary: + var errors: Array = [] + var warnings: Array = [] + var lines: PackedStringArray = stdout.split("\n") + var i := 0 + while i < lines.size(): + var line: String = lines[i].trim_suffix("\r") + var category := "" + var is_error := false + var is_warning := false + for prefix in ["ERROR:", "USER ERROR:", "SCRIPT ERROR:"]: + if line.begins_with(prefix): + category = prefix.trim_suffix(":") + is_error = true + break + if not is_error: + for prefix in ["WARNING:", "USER WARNING:"]: + if line.begins_with(prefix): + category = prefix.trim_suffix(":") + is_warning = true + break + if is_error or is_warning: + var msg: String = line.substr(category.length() + 1).strip_edges() + var context := "" + if i + 1 < lines.size(): + var next := lines[i + 1].trim_suffix("\r") + if next.begins_with(" ") or next.begins_with("\t"): + context = next.strip_edges() + i += 1 + if is_error: + errors.append({"category": category, "message": msg, "context": context}) + else: + warnings.append({"category": category, "message": msg, "context": context}) + i += 1 + return {"errors": errors, "warnings": warnings} + + +static func _load_state_dump(): + if not FileAccess.file_exists(STATE_DUMP_PATH): + return null + var f := FileAccess.open(STATE_DUMP_PATH, FileAccess.READ) + if f == null: + return null + var text := f.get_as_text() + f.close() + var parsed = JSON.parse_string(text) + if typeof(parsed) != TYPE_DICTIONARY: + return null + return parsed + + +static func _ok(data) -> Dictionary: + return {"data": data} + + +static func _err(code: int, msg: String) -> Dictionary: + return {"error": {"code": code, "message": msg}} diff --git a/addons/agent_tools/tools/run_tools.gd.uid b/addons/agent_tools/tools/run_tools.gd.uid new file mode 100644 index 0000000..eb190e1 --- /dev/null +++ b/addons/agent_tools/tools/run_tools.gd.uid @@ -0,0 +1 @@ +uid://7r486ud37chv diff --git a/addons/agent_tools/tools/scene_tools.gd b/addons/agent_tools/tools/scene_tools.gd new file mode 100644 index 0000000..ad8ce26 --- /dev/null +++ b/addons/agent_tools/tools/scene_tools.gd @@ -0,0 +1,734 @@ +@tool +extends RefCounted + +const Coerce := preload("res://addons/agent_tools/tools/_coerce.gd") + +# scene.inspect — read-only. Accepts {"path": "res://..."} or omits path to use the currently-edited scene. +# Returns {root: , path: ""} where NodeDict = {name, class, node_path, script, children}. +static func inspect(params: Dictionary) -> Dictionary: + var path: String = params.get("path", "") + var root: Node + var owns_root := false + var scene_file_path: String + + if path == "": + root = EditorInterface.get_edited_scene_root() + if root == null: + return _err(-32001, "no scene open and no 'path' provided") + scene_file_path = root.scene_file_path + else: + var packed := ResourceLoader.load(path, "PackedScene") as PackedScene + if packed == null: + return _err(-32001, "failed to load scene: %s" % path) + root = packed.instantiate(PackedScene.GEN_EDIT_STATE_DISABLED) + if root == null: + return _err(-32001, "instantiate failed: %s" % path) + owns_root = true + scene_file_path = path + + var tree := _node_to_dict(root, root) + if owns_root: + root.free() + return _ok({"path": scene_file_path, "root": tree}) + + +static func _node_to_dict(node: Node, scene_root: Node) -> Dictionary: + var d := { + "name": String(node.name), + "class": node.get_class(), + "node_path": "." if node == scene_root else String(scene_root.get_path_to(node)), + "script": "", + "children": [], + } + var scr := node.get_script() as Script + if scr: + d.script = scr.resource_path + for child in node.get_children(): + # Only include nodes belonging to this scene (skip runtime-added children of instanced sub-scenes). + if child.owner == scene_root or child == scene_root: + d.children.append(_node_to_dict(child, scene_root)) + return d + + +# scene.add_node — operates on the currently-edited scene. +# Params: {type: "Node2D", name?: "Foo", parent_path?: "."} +static func add_node(params: Dictionary) -> Dictionary: + var node_type: String = params.get("type", "") + var node_name: String = params.get("name", "") + var parent_path: String = params.get("parent_path", ".") + + if node_type == "": + return _err(-32602, "missing 'type'") + if not ClassDB.class_exists(node_type): + return _err(-32602, "unknown class: %s" % node_type) + if not ClassDB.is_parent_class(node_type, "Node"): + return _err(-32602, "type must derive from Node: %s" % node_type) + if not ClassDB.can_instantiate(node_type): + return _err(-32602, "class is not instantiable: %s" % node_type) + + var root := EditorInterface.get_edited_scene_root() + if root == null: + return _err(-32001, "no scene open") + + var parent: Node = root if parent_path == "." or parent_path == "" else root.get_node_or_null(parent_path) + if parent == null: + return _err(-32001, "parent not found: %s" % parent_path) + + var inst := ClassDB.instantiate(node_type) as Node + if inst == null: + return _err(-32001, "failed to instantiate %s" % node_type) + + parent.add_child(inst) + if node_name != "": + inst.name = node_name + # Owner MUST be set to the scene root, otherwise the node won't be saved. + inst.owner = root + + EditorInterface.mark_scene_as_unsaved() + return _ok({ + "node_path": String(root.get_path_to(inst)), + "name": String(inst.name), + "class": inst.get_class(), + }) + + +# scene.set_property — operates on the currently-edited scene. +# Params: {node_path, property, value} — value is coerced via Coerce.coerce based on +# the target property's type. See _coerce.gd for the full supported-type list. +static func set_property(params: Dictionary) -> Dictionary: + var node_path: String = params.get("node_path", "") + var property_name: String = params.get("property", "") + if node_path == "" or property_name == "": + return _err(-32602, "missing 'node_path' or 'property'") + + var root := EditorInterface.get_edited_scene_root() + if root == null: + return _err(-32001, "no scene open") + + var node: Node = root if node_path == "." else root.get_node_or_null(node_path) + if node == null: + return _err(-32001, "node not found: %s" % node_path) + + var prop_info: Dictionary = {} + for p in node.get_property_list(): + if p.name == property_name: + prop_info = p + break + if prop_info.is_empty(): + return _err(-32602, "property not found on %s: %s" % [node.get_class(), property_name]) + # Reject read-only properties upfront — Godot would silently ignore the set(). + if int(prop_info.get("usage", 0)) & PROPERTY_USAGE_READ_ONLY: + return _err(-32602, "property '%s' on %s is read-only" % [property_name, node.get_class()]) + + var assigned := _assign_property(node, property_name, prop_info, params.get("value")) + if assigned.has("error"): + return _err(int(assigned.get("code", -32001)), assigned.error) + + EditorInterface.mark_scene_as_unsaved() + + return _ok({ + "node_path": node_path, + "property": property_name, + "value": Coerce.to_json(assigned.get("stored")), + }) + + +# Assign a JSON-native value into node[property_name], handling three failure +# modes the bare `node.set()` path gets wrong: +# +# 1. Node-typed @exports (`@export var foo: Node2D`) — TYPE_OBJECT with a +# PROPERTY_HINT_NODE_TYPE hint. A path string must be resolved to the actual +# Node so PackedScene.pack() emits the `node_paths=PackedStringArray(...)` +# metadata and serializes it (the inspector drag-drop equivalent). A bare +# string assignment is silently dropped at save time. +# 2. Setter-routed properties (notably PROPERTY_USAGE_NO_EDITOR ones like +# `unique_name_in_owner`) — `node.set()` doesn't reliably dispatch to the +# registered setter. If the readback doesn't match, route through the +# explicit `set_` method. +# 3. Silently-dropped assignments — Godot accepts the set() call but drops the +# value (type mismatch on a Resource slot, or a primitive that didn't take). +# Surface an error instead of echoing a misleading value. +# +# Returns {"stored": } on success, or {"error": msg, "code": int} on failure. +static func _assign_property(node: Node, property_name: String, prop_info: Dictionary, raw_value) -> Dictionary: + # Case 1: node-typed export. Resolve a path string to the actual Node. + if prop_info.type == TYPE_OBJECT and int(prop_info.get("hint", 0)) == PROPERTY_HINT_NODE_TYPE: + if raw_value == null: + node.set(property_name, null) + return {"stored": null} + if not (raw_value is String): + return {"code": -32602, "error": + "node-typed export '%s' expects a node path string (e.g. '../Target'), got %s" % + [property_name, type_string(typeof(raw_value))]} + var target := node.get_node_or_null(NodePath(raw_value)) + if target == null: + return {"code": -32001, "error": + "node-typed export '%s' on %s: no node at path '%s' (resolved relative to '%s'). " % + [property_name, node.get_class(), raw_value, node.name] + + "The target node must already exist in the scene."} + node.set(property_name, target) + var stored_node = node.get(property_name) + if stored_node != target: + return {"code": -32001, "error": + "node-typed export '%s' on %s: assignment dropped — resolved node is a %s but the slot expects a %s" % + [property_name, node.get_class(), target.get_class(), prop_info.get("hint_string", "Node")]} + return {"stored": stored_node} + + var coerced = Coerce.coerce(raw_value, prop_info.type) + if coerced is Dictionary and coerced.has("_error"): + return {"code": -32602, "error": coerced._error} + + node.set(property_name, coerced) + var stored = node.get(property_name) + + # Case 2: setter routing. Some properties don't dispatch through node.set(); + # if the readback diverged and an explicit setter exists, go through it. + if not _values_match(stored, coerced) and node.has_method("set_" + property_name): + node.call("set_" + property_name, coerced) + stored = node.get(property_name) + + # Case 3a: Resource-typed slot silently rejected a non-Resource value. + if coerced != null and stored == null and prop_info.type == TYPE_OBJECT: + return {"code": -32001, "error": + "property '%s' on %s was not accepted (assignment silently dropped — target expects a %s; passed value type didn't match)" % + [property_name, node.get_class(), prop_info.get("hint_string", "Resource")]} + + # Case 3b: a bool that didn't take. Limited to bool (the documented pitfall, + # e.g. unique_name_in_owner) because other primitives legitimately transform + # on set (name auto-deduplicates, ints clamp to range, floats lose precision), + # which would make a strict match check fire false positives. + if prop_info.type == TYPE_BOOL and not _values_match(stored, coerced): + return {"code": -32001, "error": + "property '%s' on %s was not accepted (set silently dropped: requested %s, stored %s)" % + [property_name, node.get_class(), str(coerced), str(stored)]} + + return {"stored": stored} + + +# Equality check tolerant of GDScript's cross-type Variant comparison. Used to +# detect whether an assignment actually took before deciding to route through a +# setter or flag a silent drop. +static func _values_match(a, b) -> bool: + return a == b + + +# scene.remove_node — operates on the currently-edited scene. Cannot remove the scene root. +# Params: {node_path: "Player/Sprite2D"} +static func remove_node(params: Dictionary) -> Dictionary: + var node_path: String = params.get("node_path", "") + if node_path == "": + return _err(-32602, "missing 'node_path'") + if node_path == ".": + return _err(-32602, "cannot remove scene root") + + var root := EditorInterface.get_edited_scene_root() + if root == null: + return _err(-32001, "no scene open") + var node: Node = root.get_node_or_null(node_path) + if node == null: + return _err(-32001, "node not found: %s" % node_path) + + node.get_parent().remove_child(node) + node.queue_free() + EditorInterface.mark_scene_as_unsaved() + return _ok({"removed": node_path}) + + +# scene.reparent — move a node under a new parent in the currently-edited scene. +# Params: {node_path, new_parent_path, keep_global_transform?: true} +static func reparent(params: Dictionary) -> Dictionary: + var node_path: String = params.get("node_path", "") + var new_parent_path: String = params.get("new_parent_path", "") + var keep_xform: bool = params.get("keep_global_transform", true) + if node_path == "" or new_parent_path == "": + return _err(-32602, "missing 'node_path' or 'new_parent_path'") + if node_path == ".": + return _err(-32602, "cannot reparent scene root") + + var root := EditorInterface.get_edited_scene_root() + if root == null: + return _err(-32001, "no scene open") + var node: Node = root.get_node_or_null(node_path) + if node == null: + return _err(-32001, "node not found: %s" % node_path) + var new_parent: Node = root if new_parent_path == "." else root.get_node_or_null(new_parent_path) + if new_parent == null: + return _err(-32001, "new parent not found: %s" % new_parent_path) + if new_parent == node or node.is_ancestor_of(new_parent): + return _err(-32602, "cannot reparent under self or descendant (would create cycle)") + + node.reparent(new_parent, keep_xform) + # reparent() preserves owner in 4.3+, but be explicit so the node still serializes. + node.owner = root + EditorInterface.mark_scene_as_unsaved() + return _ok({"node_path": String(root.get_path_to(node))}) + + +# scene.open — open a scene in the editor, making it the currently-edited scene. +# Params: {path: "res://Main.tscn"} +static func open_scene(params: Dictionary) -> Dictionary: + var path: String = params.get("path", "") + if path == "": + return _err(-32602, "missing 'path'") + if not ResourceLoader.exists(path, "PackedScene"): + return _err(-32001, "scene not found: %s" % path) + EditorInterface.open_scene_from_path(path) + return _ok({"path": path}) + + +# scene.save — save the currently-edited scene. Pass 'path' to save-as (rebinds +# the scene to that path). +# +# Fresh scenes (created via `scene.add_node`-only workflows with no backing file, +# or never-saved manual scenes) have an empty `scene_file_path`. Godot's bare +# `save_scene()` responds by opening the native Save-As file dialog — blocking +# the editor and requiring a human click. We detect that case and return an +# error instead, so the agent can react cleanly by re-calling with `path`. +static func save_scene(params: Dictionary) -> Dictionary: + var root := EditorInterface.get_edited_scene_root() + if root == null: + return _err(-32001, "no scene open") + var path: String = params.get("path", "") + var current_path: String = root.scene_file_path + + if path == "" and current_path == "": + return _err(-32602, + "scene has never been saved and no 'path' was given. " + + "Calling bare save would open the Save-As dialog and block the editor. " + + "Re-call with 'path' set to a res:// target (e.g. 'res://scenes/foo.tscn').") + + if path == "": + var err := EditorInterface.save_scene() + if err != OK: + return _err(-32001, "save failed: error %d" % err) + else: + # save_scene_as does NOT auto-create parent directories — it silently + # fails if the target dir doesn't exist. Ensure the dir first. + var dir_path := path.get_base_dir() + if not DirAccess.dir_exists_absolute(dir_path): + var derr := DirAccess.make_dir_recursive_absolute(dir_path) + if derr != OK: + return _err(-32001, "mkdir failed (%d): %s" % [derr, dir_path]) + # save_scene_as returns void in Godot 4.x — no error to check directly. + EditorInterface.save_scene_as(path) + + var final_path: String = EditorInterface.get_edited_scene_root().scene_file_path + # If we passed a path but the scene still isn't bound, the save didn't land. + # Surface that explicitly instead of returning {"path": ""} which looks like success. + if path != "" and final_path == "": + return _err(-32001, + "save_scene_as('%s') did not bind the scene — verify the path is writable and ends in .tscn" % path) + return _ok({"path": final_path}) + + +# scene.new — create a new .tscn file with a root node of the given type. By default +# opens the new scene in the editor so subsequent scene.* calls operate on it. +# Params: {path, root_type?: "Node", root_name?, overwrite?: false, open_after?: true} +static func new_scene(params: Dictionary) -> Dictionary: + var path: String = params.get("path", "") + var root_type: String = params.get("root_type", "Node") + var root_name: String = params.get("root_name", "") + var overwrite: bool = params.get("overwrite", false) + var open_after: bool = params.get("open_after", true) + + if path == "" or not path.begins_with("res://") or not path.ends_with(".tscn"): + return _err(-32602, "'path' must be 'res://...' ending in .tscn") + if FileAccess.file_exists(path) and not overwrite: + return _err(-32602, "file exists (pass overwrite:true to replace): %s" % path) + if not ClassDB.class_exists(root_type): + return _err(-32602, "unknown class: %s" % root_type) + if not ClassDB.is_parent_class(root_type, "Node"): + return _err(-32602, "root_type must derive from Node: %s" % root_type) + if not ClassDB.can_instantiate(root_type): + return _err(-32602, "class is not instantiable: %s" % root_type) + + var root := ClassDB.instantiate(root_type) as Node + if root == null: + return _err(-32001, "failed to instantiate %s" % root_type) + if root_name != "": + root.name = root_name + + var dir_path := path.get_base_dir() + if not DirAccess.dir_exists_absolute(dir_path): + var derr := DirAccess.make_dir_recursive_absolute(dir_path) + if derr != OK: + root.free() + return _err(-32001, "mkdir failed (%d): %s" % [derr, dir_path]) + + var packed := PackedScene.new() + var pack_err := packed.pack(root) + var final_name := String(root.name) + root.free() + if pack_err != OK: + return _err(-32001, "PackedScene.pack failed: %d" % pack_err) + var save_err := ResourceSaver.save(packed, path) + if save_err != OK: + return _err(-32001, "ResourceSaver.save failed: %d" % save_err) + + EditorInterface.get_resource_filesystem().update_file(path) + if open_after: + # If the target path is already the currently-edited scene, open_scene_from_path + # will just switch to the existing (stale) in-memory tab. Force a reload so the + # freshly-written file is what the editor shows. + var current_root := EditorInterface.get_edited_scene_root() + if current_root != null and current_root.scene_file_path == path: + EditorInterface.reload_scene_from_path(path) + else: + EditorInterface.open_scene_from_path(path) + return _ok({"path": path, "root_type": root_type, "root_name": final_name}) + + +# scene.instance_packed — add an existing .tscn as a sub-scene child in the currently-edited scene. +# Params: {scene_path, parent_path?: ".", name?} +static func instance_packed(params: Dictionary) -> Dictionary: + var scene_path: String = params.get("scene_path", "") + var parent_path: String = params.get("parent_path", ".") + var node_name: String = params.get("name", "") + + if scene_path == "": + return _err(-32602, "missing 'scene_path'") + var packed := ResourceLoader.load(scene_path, "PackedScene") as PackedScene + if packed == null: + return _err(-32001, "failed to load scene: %s" % scene_path) + + var root := EditorInterface.get_edited_scene_root() + if root == null: + return _err(-32001, "no scene open") + var parent: Node = root if parent_path == "." or parent_path == "" else root.get_node_or_null(parent_path) + if parent == null: + return _err(-32001, "parent not found: %s" % parent_path) + if scene_path == root.scene_file_path: + return _err(-32602, "cannot instance a scene into itself (would recurse)") + + var inst := packed.instantiate() + if inst == null: + return _err(-32001, "instantiate failed") + + parent.add_child(inst) + if node_name != "": + inst.name = node_name + # Owner is the scene root so the sub-scene instance serializes with this scene. + inst.owner = root + EditorInterface.mark_scene_as_unsaved() + return _ok({ + "node_path": String(root.get_path_to(inst)), + "scene_path": scene_path, + "name": String(inst.name), + }) + + +# scene.get_property — read a property from a node. Mirror of set_property. +# Params: {node_path, property} +static func get_property(params: Dictionary) -> Dictionary: + var node_path: String = params.get("node_path", "") + var property_name: String = params.get("property", "") + if node_path == "" or property_name == "": + return _err(-32602, "missing 'node_path' or 'property'") + + var root := EditorInterface.get_edited_scene_root() + if root == null: + return _err(-32001, "no scene open") + var node: Node = root if node_path == "." else root.get_node_or_null(node_path) + if node == null: + return _err(-32001, "node not found: %s" % node_path) + + var prop_type: int = TYPE_NIL + var found := false + for p in node.get_property_list(): + if p.name == property_name: + prop_type = p.type + found = true + break + if not found: + return _err(-32602, "property not found on %s: %s" % [node.get_class(), property_name]) + + return _ok({ + "node_path": node_path, + "property": property_name, + "value": Coerce.to_json(node.get(property_name)), + "type": type_string(prop_type), + }) + + +# scene.capture_screenshot — save a PNG of what the editor is currently showing +# for the open scene. Captures the appropriate editor viewport (2D for 2D/Control +# root nodes, 3D for Node3D roots). Includes the editor's grid/gizmos — this is +# "what the user sees" rather than a clean render, which is actually useful for +# the agent to spot layout problems. +# Params: {output?: "res://screenshot.png"} +# Returns: {path, width, height} +static func capture_screenshot(params: Dictionary) -> Dictionary: + var output_path: String = params.get("output", "") + + var root := EditorInterface.get_edited_scene_root() + if root == null: + return _err(-32001, "no scene open — open one first with scene.open") + + var viewport: SubViewport + if root is Node3D: + viewport = EditorInterface.get_editor_viewport_3d(0) + else: + viewport = EditorInterface.get_editor_viewport_2d() + if viewport == null: + return _err(-32001, "could not obtain editor viewport") + + var tex := viewport.get_texture() + if tex == null: + return _err(-32001, "viewport has no texture yet (try again after the editor renders a frame)") + var img := tex.get_image() + if img == null or img.is_empty(): + return _err(-32001, "failed to capture image from viewport") + + if output_path == "": + var base: String = "screenshot" + if root.scene_file_path != "": + base = root.scene_file_path.get_file().get_basename() + output_path = "res://.godot/agent_tools/%s.png" % base + + var dir_path := output_path.get_base_dir() + if not DirAccess.dir_exists_absolute(dir_path): + var derr := DirAccess.make_dir_recursive_absolute(dir_path) + if derr != OK: + return _err(-32001, "mkdir failed: %d" % derr) + + var save_err := img.save_png(output_path) + if save_err != OK: + return _err(-32001, "save_png failed: %d" % save_err) + + return _ok({ + "path": output_path, + "width": img.get_width(), + "height": img.get_height(), + "note": "clean viewport capture (no editor grid/gizmos); empty scenes render as the viewport background color", + }) + + +# scene.duplicate_node — clone a node (with its descendants) into the currently-edited scene. +# Owner is set recursively so the duplicated subtree serializes with the scene. +# Params: {node_path, new_name?, parent_path?: same parent} +static func duplicate_node(params: Dictionary) -> Dictionary: + var node_path: String = params.get("node_path", "") + var new_name: String = params.get("new_name", "") + var parent_path: String = params.get("parent_path", "") + if node_path == "": + return _err(-32602, "missing 'node_path'") + if node_path == ".": + return _err(-32602, "cannot duplicate scene root") + + var root := EditorInterface.get_edited_scene_root() + if root == null: + return _err(-32001, "no scene open") + var src: Node = root.get_node_or_null(node_path) + if src == null: + return _err(-32001, "node not found: %s" % node_path) + + # DUPLICATE_SIGNALS | DUPLICATE_GROUPS | DUPLICATE_SCRIPTS — default flags. + var copy: Node = src.duplicate(7) + if copy == null: + return _err(-32001, "duplicate() failed") + + var parent: Node = src.get_parent() if parent_path == "" else root.get_node_or_null(parent_path) + if parent == null: + return _err(-32001, "parent not found: %s" % parent_path) + parent.add_child(copy) + if new_name != "": + copy.name = new_name + _set_owner_recursive(copy, root) + EditorInterface.mark_scene_as_unsaved() + return _ok({ + "source": node_path, + "node_path": String(root.get_path_to(copy)), + "name": String(copy.name), + }) + + +static func _set_owner_recursive(node: Node, owner: Node) -> void: + node.owner = owner + for child in node.get_children(): + _set_owner_recursive(child, owner) + + +# scene.build_tree — build a subtree in one call. +# Spec is recursive: each entry is {type (required), name?, properties?, script?, children?}. +# Collapses what would otherwise be N scene.add_node + M scene.set_property + K script.attach +# calls into a single request, which matters for any UI-heavy scene where hand-writing the +# tree by calling the atomic tools balloons into dozens of round trips. +# Properties use the same Coerce rules as scene.set_property (Vectors from arrays, Resources +# auto-loaded from res:// paths, etc.). +# On any failure, all nodes created during the call are rolled back so the scene doesn't +# end up partly built. +# Params: {parent_path?: ".", nodes: [TreeNode, ...]} +# TreeNode: {type: "ClassName", name?, properties?: {name: value}, script?: "res://...", children?: [TreeNode, ...]} +static func build_tree(params: Dictionary) -> Dictionary: + var parent_path: String = params.get("parent_path", ".") + var nodes: Array = params.get("nodes", []) + if nodes.is_empty(): + return _err(-32602, "missing 'nodes' (array of tree entries)") + + var root := EditorInterface.get_edited_scene_root() + if root == null: + return _err(-32001, "no scene open") + var parent: Node = root if parent_path == "." or parent_path == "" else root.get_node_or_null(parent_path) + if parent == null: + return _err(-32001, "parent not found: %s" % parent_path) + + var created_nodes: Array = [] + var created_paths: Array = [] + for spec in nodes: + var err_msg := _build_subtree(spec, parent, root, created_nodes, created_paths) + if err_msg != "": + # Rollback: free everything created during this call in reverse order. + for i in range(created_nodes.size() - 1, -1, -1): + var n: Node = created_nodes[i] + if is_instance_valid(n): + if n.get_parent(): + n.get_parent().remove_child(n) + n.queue_free() + return _err(-32001, err_msg) + + EditorInterface.mark_scene_as_unsaved() + return _ok({ + "parent_path": parent_path, + "created": created_paths, + "count": created_paths.size(), + }) + + +# Recursive worker. Appends to the shared created_nodes/created_paths arrays so +# the caller can roll back on failure. Returns "" on success, or an error message. +static func _build_subtree(spec, parent: Node, scene_root: Node, created_nodes: Array, created_paths: Array) -> String: + if typeof(spec) != TYPE_DICTIONARY: + return "tree entry must be an object, got %s" % type_string(typeof(spec)) + + var node_type: String = spec.get("type", "") + if node_type == "": + return "tree entry missing 'type'" + if not ClassDB.class_exists(node_type): + return "unknown class: %s" % node_type + if not ClassDB.is_parent_class(node_type, "Node"): + return "type must derive from Node: %s" % node_type + if not ClassDB.can_instantiate(node_type): + return "class is not instantiable: %s" % node_type + + var inst := ClassDB.instantiate(node_type) as Node + if inst == null: + return "failed to instantiate %s" % node_type + + var node_name: String = spec.get("name", "") + if node_name != "": + inst.name = node_name + + parent.add_child(inst) + inst.owner = scene_root + created_nodes.append(inst) + var this_path := String(scene_root.get_path_to(inst)) + created_paths.append(this_path) + + # Attach a script before applying properties so script-exported properties + # become settable in this same call. + var script_path: String = spec.get("script", "") + if script_path != "": + var script := ResourceLoader.load(script_path, "Script") as Script + if script == null: + return "failed to load script '%s' for '%s'" % [script_path, this_path] + inst.set_script(script) + + var properties: Dictionary = spec.get("properties", {}) + if not properties.is_empty(): + var by_name: Dictionary = {} + for p in inst.get_property_list(): + by_name[p.name] = p + for prop_name in properties: + if not by_name.has(prop_name): + return "property not found on %s ('%s'): %s" % [node_type, this_path, prop_name] + var prop_info: Dictionary = by_name[prop_name] + if int(prop_info.get("usage", 0)) & PROPERTY_USAGE_READ_ONLY: + return "property '%s' on '%s' is read-only" % [prop_name, this_path] + var assigned := _assign_property(inst, prop_name, prop_info, properties[prop_name]) + if assigned.has("error"): + return "property '%s' on '%s': %s" % [prop_name, this_path, assigned.error] + + var children: Array = spec.get("children", []) + for child_spec in children: + var child_err := _build_subtree(child_spec, inst, scene_root, created_nodes, created_paths) + if child_err != "": + return child_err + + return "" + + +# scene.call_method — invoke a method on a node in the currently-edited scene. +# Args are coerced based on the target method's declared parameter types +# (same rules as scene.set_property — including res:// → Resource auto-load). +# Return value is serialized via Coerce.to_json so the echo is unambiguous data. +# Params: {node_path, method, args?: []} +static func call_method(params: Dictionary) -> Dictionary: + var node_path: String = params.get("node_path", "") + var method_name: String = params.get("method", "") + var args: Array = params.get("args", []) + if node_path == "" or method_name == "": + return _err(-32602, "missing 'node_path' or 'method'") + + var root := EditorInterface.get_edited_scene_root() + if root == null: + return _err(-32001, "no scene open") + var node: Node = root if node_path == "." else root.get_node_or_null(node_path) + if node == null: + return _err(-32001, "node not found: %s" % node_path) + if not node.has_method(method_name): + return _err(-32602, "method not found: %s.%s" % [node.get_class(), method_name]) + + # Not typed as Array — _coerce_args returns a Dictionary on error, and strict + # typing would reject the assignment before we can branch on it. + var coerced_args = _coerce_args(node, method_name, args) + if coerced_args is Dictionary: + return coerced_args # error passthrough + var result = node.callv(method_name, coerced_args) + EditorInterface.mark_scene_as_unsaved() + return _ok({ + "node_path": node_path, + "method": method_name, + "return": Coerce.to_json(result), + }) + + +# Look up the method's parameter types and coerce each arg accordingly. +# Returns a coerced Array, or a Dictionary with {"error": ...} on failure. +static func _coerce_args(obj: Object, method_name: String, args: Array): + var method_info: Dictionary = {} + for m in obj.get_method_list(): + if m.name == method_name: + method_info = m + break + var arg_types: Array = method_info.get("args", []) + var out: Array = [] + for i in args.size(): + if i < arg_types.size(): + var coerced = Coerce.coerce(args[i], arg_types[i].type) + if coerced is Dictionary and coerced.has("_error"): + return _err(-32602, "arg %d: %s" % [i, coerced._error]) + out.append(coerced) + else: + out.append(args[i]) + return out + + +# scene.current — describe the currently-edited scene, or {open: false} if none. +static func current(_params: Dictionary) -> Dictionary: + var root := EditorInterface.get_edited_scene_root() + if root == null: + return _ok({"open": false}) + return _ok({ + "open": true, + "path": root.scene_file_path, + "root_name": String(root.name), + "root_class": root.get_class(), + }) + + +static func _ok(data) -> Dictionary: + return {"data": data} + + +static func _err(code: int, msg: String) -> Dictionary: + return {"error": {"code": code, "message": msg}} diff --git a/addons/agent_tools/tools/scene_tools.gd.uid b/addons/agent_tools/tools/scene_tools.gd.uid new file mode 100644 index 0000000..69acc56 --- /dev/null +++ b/addons/agent_tools/tools/scene_tools.gd.uid @@ -0,0 +1 @@ +uid://b2d5doqppnqvn diff --git a/addons/agent_tools/tools/script_tools.gd b/addons/agent_tools/tools/script_tools.gd new file mode 100644 index 0000000..c83751b --- /dev/null +++ b/addons/agent_tools/tools/script_tools.gd @@ -0,0 +1,171 @@ +@tool +extends RefCounted + +# script.create — write a new .gd file with an extends/class_name header. +# Params: {path: "res://scripts/Player.gd", extends?: "Node", class_name?: "Player", +# attach_to_node?: "Player", overwrite?: false} +static func create(params: Dictionary) -> Dictionary: + var path: String = params.get("path", "") + var base_class: String = params.get("extends", "Node") + var class_name_val: String = params.get("class_name", "") + var overwrite: bool = params.get("overwrite", false) + var attach_to: String = params.get("attach_to_node", "") + + if path == "" or not path.begins_with("res://") or not path.ends_with(".gd"): + return _err(-32602, "'path' must be 'res://...' ending in .gd") + if FileAccess.file_exists(path) and not overwrite: + return _err(-32602, "file exists (pass overwrite:true to replace): %s" % path) + + var dir_path := path.get_base_dir() + if not DirAccess.dir_exists_absolute(dir_path): + var derr := DirAccess.make_dir_recursive_absolute(dir_path) + if derr != OK: + return _err(-32001, "mkdir failed (%d): %s" % [derr, dir_path]) + + var body := "extends %s\n" % base_class + if class_name_val != "": + body += "class_name %s\n" % class_name_val + body += "\n" + + var f := FileAccess.open(path, FileAccess.WRITE) + if f == null: + return _err(-32001, "failed to open for write: %s" % path) + f.store_string(body) + f.close() + + # Let the editor discover the new file so load() succeeds immediately. + EditorInterface.get_resource_filesystem().update_file(path) + + var data := {"path": path, "extends": base_class, "class_name": class_name_val} + + if attach_to != "": + var attach_res := _attach_script_to_node(attach_to, path) + if attach_res.has("error"): + return attach_res + data["attached_to"] = attach_to + + return _ok(data) + + +# script.attach — attach an existing script to a node in the currently-edited scene. +# Params: {node_path, script_path} +static func attach(params: Dictionary) -> Dictionary: + var node_path: String = params.get("node_path", "") + var script_path: String = params.get("script_path", "") + if node_path == "" or script_path == "": + return _err(-32602, "missing 'node_path' or 'script_path'") + return _attach_script_to_node(node_path, script_path) + + +# script.patch — targeted edits to an existing .gd file. Two modes: +# replacements: [{old, new}] — each 'old' must appear exactly once in the file; +# fails atomically if any replacement is ambiguous +# or missing. Useful for surgical edits. +# full_source: string — overwrite the whole file (same as script.create +# overwrite, kept here for symmetry). +# After writing, the tool parse-checks the result via ResourceLoader.load(); if +# that fails, the original file is restored and an error is returned. +# Params: {path, replacements?: [{old, new}], full_source?: string, dry_run?: false} +static func patch(params: Dictionary) -> Dictionary: + var path: String = params.get("path", "") + var replacements: Array = params.get("replacements", []) + var full_source: String = params.get("full_source", "") + var dry_run: bool = params.get("dry_run", false) + + if path == "" or not path.ends_with(".gd"): + return _err(-32602, "'path' must be a .gd file") + if not FileAccess.file_exists(path): + return _err(-32001, "file not found: %s" % path) + if replacements.is_empty() and full_source == "": + return _err(-32602, "provide either 'replacements' or 'full_source'") + + var f := FileAccess.open(path, FileAccess.READ) + if f == null: + return _err(-32001, "could not read %s" % path) + var original := f.get_as_text() + f.close() + + var patched: String = original + var applied: Array = [] + if full_source != "": + patched = full_source + applied.append("full_source") + else: + for entry in replacements: + if typeof(entry) != TYPE_DICTIONARY or not entry.has("old") or not entry.has("new"): + return _err(-32602, "each replacement needs {old, new}") + var needle: String = str(entry.old) + var replacement: String = str(entry.new) + var count := patched.count(needle) + if count == 0: + return _err(-32602, "'old' string not found (would leave file unchanged): %s" % needle.substr(0, 60)) + if count > 1: + return _err(-32602, "'old' string appears %d times — ambiguous. Include more context to make it unique: %s" % [count, needle.substr(0, 60)]) + patched = patched.replace(needle, replacement) + applied.append("replaced %d chars" % needle.length()) + + if patched == original: + return _ok({"path": path, "changed": false, "note": "patched content identical to original"}) + + if dry_run: + return _ok({ + "path": path, + "changed": true, + "applied": applied, + "new_length": patched.length(), + "original_length": original.length(), + }) + + # Write, then parse-check. If parse fails, restore. + var wf := FileAccess.open(path, FileAccess.WRITE) + if wf == null: + return _err(-32001, "could not write %s" % path) + wf.store_string(patched) + wf.close() + + EditorInterface.get_resource_filesystem().update_file(path) + # Force a fresh parse — ResourceLoader.load returns a cached/placeholder + # Script for malformed sources, so we can't just null-check. Call reload() + # which actually re-parses and returns an Error. + var check := ResourceLoader.load(path, "Script", ResourceLoader.CACHE_MODE_REPLACE) + var parse_ok := false + if check != null and check is GDScript: + parse_ok = (check.reload(true) == OK) + if not parse_ok: + # Restore original to keep the file loadable. + var rf := FileAccess.open(path, FileAccess.WRITE) + if rf: + rf.store_string(original) + rf.close() + EditorInterface.get_resource_filesystem().update_file(path) + return _err(-32001, "patched script failed to parse — changes rolled back. Check Godot's Output panel for the exact error.") + + return _ok({ + "path": path, + "changed": true, + "applied": applied, + "new_length": patched.length(), + }) + + +static func _attach_script_to_node(node_path: String, script_path: String) -> Dictionary: + var root := EditorInterface.get_edited_scene_root() + if root == null: + return _err(-32001, "no scene open") + var node: Node = root if node_path == "." else root.get_node_or_null(node_path) + if node == null: + return _err(-32001, "node not found: %s" % node_path) + var script := ResourceLoader.load(script_path, "Script") as Script + if script == null: + return _err(-32001, "failed to load script: %s" % script_path) + node.set_script(script) + EditorInterface.mark_scene_as_unsaved() + return _ok({"node_path": node_path, "script": script_path}) + + +static func _ok(data) -> Dictionary: + return {"data": data} + + +static func _err(code: int, msg: String) -> Dictionary: + return {"error": {"code": code, "message": msg}} diff --git a/addons/agent_tools/tools/script_tools.gd.uid b/addons/agent_tools/tools/script_tools.gd.uid new file mode 100644 index 0000000..1380e39 --- /dev/null +++ b/addons/agent_tools/tools/script_tools.gd.uid @@ -0,0 +1 @@ +uid://bfewrbdp2o52o diff --git a/addons/agent_tools/tools/signal_tools.gd b/addons/agent_tools/tools/signal_tools.gd new file mode 100644 index 0000000..ddd1a9e --- /dev/null +++ b/addons/agent_tools/tools/signal_tools.gd @@ -0,0 +1,148 @@ +@tool +extends RefCounted + +# signal.connect — operates on the currently-edited scene. +# Params: {from: "Player", signal: "died", to: ".", method: "_on_player_died"} +# Uses CONNECT_PERSIST so the connection is serialized into the .tscn as a [connection] block. +static func connect_signal(params: Dictionary) -> Dictionary: + var from_path: String = params.get("from", "") + var signal_name: String = params.get("signal", "") + var to_path: String = params.get("to", "") + var method_name: String = params.get("method", "") + + for required in ["from", "signal", "to", "method"]: + if params.get(required, "") == "": + return _err(-32602, "missing '%s'" % required) + + var root := EditorInterface.get_edited_scene_root() + if root == null: + return _err(-32001, "no scene open") + + var source: Node = root if from_path == "." else root.get_node_or_null(from_path) + if source == null: + return _err(-32001, "source node not found: %s" % from_path) + var target: Node = root if to_path == "." else root.get_node_or_null(to_path) + if target == null: + return _err(-32001, "target node not found: %s" % to_path) + + if not source.has_signal(signal_name): + return _err(-32602, "%s has no signal '%s'" % [source.get_class(), signal_name]) + if not target.has_method(method_name): + return _err(-32602, "target has no method '%s' — attach a script with it first" % method_name) + + # Arity check: number of non-default signal args must be <= target method arg count. + var sig_info := _find_signal(source, signal_name) + var method_info := _find_method(target, method_name) + if sig_info and method_info: + var sig_args: int = (sig_info.args as Array).size() + var method_args: int = (method_info.args as Array).size() + if method_args < sig_args: + return _err(-32602, "arity mismatch: signal '%s' emits %d args, method '%s' accepts %d" % + [signal_name, sig_args, method_name, method_args]) + + var callable := Callable(target, method_name) + if source.is_connected(signal_name, callable): + return _ok({"already_connected": true}) + + var err := source.connect(signal_name, callable, CONNECT_PERSIST) + if err != OK: + return _err(-32001, "connect() returned %d" % err) + + EditorInterface.mark_scene_as_unsaved() + return _ok({ + "from": from_path, + "signal": signal_name, + "to": to_path, + "method": method_name, + }) + + +# signal.disconnect — remove a specific wiring in the currently-edited scene. +# Params: {from, signal, to, method} — same shape as signal.connect. +static func disconnect_signal(params: Dictionary) -> Dictionary: + for required in ["from", "signal", "to", "method"]: + if params.get(required, "") == "": + return _err(-32602, "missing '%s'" % required) + + var root := EditorInterface.get_edited_scene_root() + if root == null: + return _err(-32001, "no scene open") + + var from_path: String = params["from"] + var to_path: String = params["to"] + var source: Node = root if from_path == "." else root.get_node_or_null(from_path) + var target: Node = root if to_path == "." else root.get_node_or_null(to_path) + if source == null: + return _err(-32001, "source not found: %s" % from_path) + if target == null: + return _err(-32001, "target not found: %s" % to_path) + + var callable := Callable(target, params["method"]) + if not source.is_connected(params["signal"], callable): + return _err(-32001, "no such connection") + source.disconnect(params["signal"], callable) + EditorInterface.mark_scene_as_unsaved() + return _ok({"disconnected": true}) + + +# signal.list — list outgoing connections on a node in the currently-edited scene. +# Params: {node_path, persistent_only?: false} +# Returns connections with their flags so callers can distinguish editor-serialized +# connections from runtime ones. +static func list_signals(params: Dictionary) -> Dictionary: + var node_path: String = params.get("node_path", "") + var persistent_only: bool = params.get("persistent_only", false) + if node_path == "": + return _err(-32602, "missing 'node_path'") + + var root := EditorInterface.get_edited_scene_root() + if root == null: + return _err(-32001, "no scene open") + var node: Node = root if node_path == "." else root.get_node_or_null(node_path) + if node == null: + return _err(-32001, "node not found: %s" % node_path) + + var outgoing: Array = [] + for sig in node.get_signal_list(): + var sig_name: String = sig.name + for c in node.get_signal_connection_list(sig_name): + var flags: int = c.flags + var is_persistent: bool = bool(flags & CONNECT_PERSIST) + if persistent_only and not is_persistent: + continue + var cb: Callable = c.callable + var target: Object = cb.get_object() + var target_path := "" + if target is Node and (target == root or root.is_ancestor_of(target)): + target_path = String(root.get_path_to(target)) + outgoing.append({ + "signal": sig_name, + "to": target_path, + "to_object": str(target) if target_path == "" else "", + "method": cb.get_method(), + "flags": flags, + "persistent": is_persistent, + }) + return _ok({"node_path": node_path, "outgoing": outgoing}) + + +static func _find_signal(obj: Object, name: String) -> Dictionary: + for s in obj.get_signal_list(): + if s.name == name: + return s + return {} + + +static func _find_method(obj: Object, name: String) -> Dictionary: + for m in obj.get_method_list(): + if m.name == name: + return m + return {} + + +static func _ok(data) -> Dictionary: + return {"data": data} + + +static func _err(code: int, msg: String) -> Dictionary: + return {"error": {"code": code, "message": msg}} diff --git a/addons/agent_tools/tools/signal_tools.gd.uid b/addons/agent_tools/tools/signal_tools.gd.uid new file mode 100644 index 0000000..c9f5fce --- /dev/null +++ b/addons/agent_tools/tools/signal_tools.gd.uid @@ -0,0 +1 @@ +uid://cirt1dodck6pm diff --git a/addons/agent_tools/tools/test_tools.gd b/addons/agent_tools/tools/test_tools.gd new file mode 100644 index 0000000..7770062 --- /dev/null +++ b/addons/agent_tools/tools/test_tools.gd @@ -0,0 +1,230 @@ +@tool +extends RefCounted + +# test.run — detect and run a GDScript test framework, return structured results. +# +# Supports GUT (addons/gut) and GdUnit4 (addons/gdUnit4). Auto-detects which is +# installed; caller can force with 'framework'. Runs via headless subprocess so +# it doesn't block the editor. +# +# Params: { +# framework?: "auto" | "gut" | "gdunit4", +# directory?: "res://test", # test directory; defaults by framework +# pattern?: "test_*.gd", # file name pattern; defaults by framework +# timeout_seconds?: 60, +# } +# +# Returns: { +# framework: "gut" | "gdunit4", +# total, passed, failed, skipped, +# failures: [{name, file, line, message}], +# raw_output: string, +# exit_code, +# } +static func run(params: Dictionary) -> Dictionary: + var framework: String = params.get("framework", "auto") + var directory: String = params.get("directory", "") + var pattern: String = params.get("pattern", "") + var timeout_seconds: int = int(params.get("timeout_seconds", 60)) + + if framework == "auto": + framework = _detect_framework() + if framework == "": + return _err(-32001, + "no test framework detected. Install GUT (addons/gut) or GdUnit4 (addons/gdUnit4), " + + "or pass 'framework' explicitly.") + + var runner_info := _runner_for(framework, directory, pattern) + if runner_info.has("_error"): + return _err(-32001, runner_info._error) + + var exe := OS.get_executable_path() + var project_dir := ProjectSettings.globalize_path("res://") + var args: Array = ["--path", project_dir, "--headless"] + for a in runner_info.args: + args.append(str(a)) + + var output: Array = [] + var exit_code := OS.execute(exe, args, output, true) + var stdout: String = "" + if output.size() > 0: + stdout = str(output[0]) + + var parsed: Dictionary + match framework: + "gut": parsed = _parse_gut_output(stdout) + "gdunit4": parsed = _parse_gdunit4_output(stdout) + _: parsed = {"total": 0, "passed": 0, "failed": 0, "skipped": 0, "failures": []} + + parsed["framework"] = framework + parsed["exit_code"] = exit_code + # Strip ANSI escape codes from raw_output so the JSON wire is always clean. + # Godot's JSON.stringify doesn't escape every control char; leaving \x1b in + # the string breaks strict JSON parsers downstream. + parsed["raw_output"] = _strip_ansi(stdout) + parsed["command"] = "%s %s" % [exe, " ".join(args)] + return _ok(parsed) + + +static func _detect_framework() -> String: + if DirAccess.dir_exists_absolute("res://addons/gut"): + return "gut" + if DirAccess.dir_exists_absolute("res://addons/gdUnit4"): + return "gdunit4" + return "" + + +static func _runner_for(framework: String, directory: String, pattern: String) -> Dictionary: + match framework: + "gut": + var gut_cmdln := "res://addons/gut/gut_cmdln.gd" + if not FileAccess.file_exists(gut_cmdln): + return {"_error": "GUT framework dir exists but gut_cmdln.gd not found — GUT install may be broken"} + var args: Array = ["-s", gut_cmdln, "-gexit"] + if directory != "": + args.append("-gdir=%s" % directory) + else: + args.append("-gdir=res://test") + if pattern != "": + args.append("-gprefix=%s" % pattern) + return {"args": args} + "gdunit4": + # GdUnit4 ships GdUnitRunner.gd for CLI execution. + var runner := "res://addons/gdUnit4/bin/GdUnitCmdTool.gd" + if not FileAccess.file_exists(runner): + return {"_error": "GdUnit4 framework dir exists but GdUnitCmdTool.gd not found"} + var args: Array = ["-s", runner] + if directory != "": + args.append("--add=%s" % directory) + else: + args.append("--add=res://test") + return {"args": args} + _: + return {"_error": "unknown framework: %s (supported: gut, gdunit4)" % framework} + + +# Parse GUT's text output. GUT's actual format (not the documented :-separated form): +# +# Totals +# ------ +# Scripts 1 +# Tests 3 +# Passing Tests 2 +# Failing Tests 1 +# Pending Tests 0 <- optional, only if tests are pending +# Asserts 2/3 +# +# Failures appear under a per-file header in the "Run Summary" section: +# +# res://test/foo.gd +# - test_failing_case +# [Failed]: +# at line 14 +# +# Output has ANSI color escapes; strip them first so regex matches cleanly. +static func _parse_gut_output(raw: String) -> Dictionary: + var text := _strip_ansi(raw) + var total := _gut_extract_count(text, "Tests") + var passed := _gut_extract_count(text, "Passing Tests") + var failed := _gut_extract_count(text, "Failing Tests") + var skipped := _gut_extract_count(text, "Pending Tests") + + var failures: Array = _extract_gut_failures(text) + + return {"total": total, "passed": passed, "failed": failed, "skipped": skipped, "failures": failures} + + +# Match either "Label 12" (space-aligned) or "Label: 12" (colon form) on its own line. +static func _gut_extract_count(text: String, label: String) -> int: + var re := RegEx.new() + re.compile("(?m)^\\s*" + label.replace(" ", "\\s+") + "\\s*[:\\s]\\s*(\\d+)\\s*$") + var m := re.search(text) + if m == null: + return 0 + return int(m.get_string(1)) + + +# Walk the text top-to-bottom, tracking current test file (file headers are plain +# `res://...` lines), collecting failing tests (`- test_name` with a following +# `[Failed]:` assertion and `at line N`). +static func _extract_gut_failures(text: String) -> Array: + var failures: Array = [] + var lines: PackedStringArray = text.split("\n") + var current_file := "" + var current_test := "" + var current_message := "" + var i := 0 + while i < lines.size(): + var line: String = lines[i].trim_suffix("\r").strip_edges(false, true) # strip only trailing + if line.begins_with("res://") and line.ends_with(".gd"): + current_file = line.strip_edges() + i += 1 + continue + var trimmed := line.strip_edges() + if trimmed.begins_with("- "): + current_test = trimmed.substr(2).strip_edges() + current_message = "" + elif trimmed.begins_with("[Failed]:"): + current_message = trimmed.substr("[Failed]:".length()).strip_edges() + elif trimmed.begins_with("at line"): + var n_str := trimmed.substr("at line".length()).strip_edges() + var line_num := int(n_str) + if current_test != "" and current_file != "": + failures.append({ + "name": current_test, + "file": current_file, + "line": line_num, + "message": current_message, + }) + current_test = "" + current_message = "" + i += 1 + return failures + + +# Remove ANSI terminal escape codes (colors) from GUT's output so regex matches +# don't get tripped up on things like "\x1b[32m". +static func _strip_ansi(text: String) -> String: + var re := RegEx.new() + re.compile("\\x1b\\[[0-9;]*[A-Za-z]") + return re.sub(text, "", true) + + +# GdUnit4's default output ends with a summary like: +# "| Tests | Executed | 12 | passed | 10 | skipped | 0 | failed | 2 | ... |" +# and per-failure blocks with file:line. +static func _parse_gdunit4_output(text: String) -> Dictionary: + var total := 0 + var passed := 0 + var failed := 0 + var skipped := 0 + + var summary_re := RegEx.new() + summary_re.compile("Executed\\s*\\|\\s*(\\d+).*?passed\\s*\\|\\s*(\\d+).*?skipped\\s*\\|\\s*(\\d+).*?failed\\s*\\|\\s*(\\d+)") + var m := summary_re.search(text) + if m: + total = int(m.get_string(1)) + passed = int(m.get_string(2)) + skipped = int(m.get_string(3)) + failed = int(m.get_string(4)) + + var failures: Array = [] + var fail_re := RegEx.new() + fail_re.compile("(?m)FAILED\\s+(.+?)\\s*\\(at:\\s*(.+?):(\\d+)") + for fm in fail_re.search_all(text): + failures.append({ + "name": fm.get_string(1), + "file": fm.get_string(2), + "line": int(fm.get_string(3)), + "message": "", + }) + + return {"total": total, "passed": passed, "failed": failed, "skipped": skipped, "failures": failures} + + +static func _ok(data) -> Dictionary: + return {"data": data} + + +static func _err(code: int, msg: String) -> Dictionary: + return {"error": {"code": code, "message": msg}} diff --git a/addons/agent_tools/tools/test_tools.gd.uid b/addons/agent_tools/tools/test_tools.gd.uid new file mode 100644 index 0000000..c7b7422 --- /dev/null +++ b/addons/agent_tools/tools/test_tools.gd.uid @@ -0,0 +1 @@ +uid://blc15tr1vxiek diff --git a/addons/agent_tools/tools/theme_tools.gd b/addons/agent_tools/tools/theme_tools.gd new file mode 100644 index 0000000..1906e3b --- /dev/null +++ b/addons/agent_tools/tools/theme_tools.gd @@ -0,0 +1,110 @@ +@tool +extends RefCounted + +# theme.* — thin wrappers around Godot's awkward Theme API. Theme uses the +# three-argument shape `(item_name, type_name, value)` where type_name is a +# Control subclass like "Button", "Label", "LineEdit". resource.call_method +# works but these helpers save the lookup. + +# theme.set_color — {path, item, type, color} +# color accepts [r,g,b(,a)] or '#rrggbb(aa)' (Coerce handles it). +static func set_color(params: Dictionary) -> Dictionary: + return _set_entry(params, "color", "set_color", TYPE_COLOR) + + +# theme.set_constant — {path, item, type, value: int} +static func set_constant(params: Dictionary) -> Dictionary: + return _set_entry(params, "constant", "set_constant", TYPE_INT) + + +# theme.set_font_size — {path, item, type, value: int} +static func set_font_size(params: Dictionary) -> Dictionary: + return _set_entry(params, "font_size", "set_font_size", TYPE_INT) + + +# theme.set_stylebox_flat — {path, item, type, properties: {bg_color, corner_radius_*, border_width_*, ...}} +# Creates (or replaces) a StyleBoxFlat with the given properties and assigns +# it to theme... Saves the two-step "create stylebox, set props, +# assign" dance. +static func set_stylebox_flat(params: Dictionary) -> Dictionary: + var path: String = params.get("path", "") + var item_name: String = params.get("item", "") + var type_name: String = params.get("type", "") + var props: Dictionary = params.get("properties", {}) + if path == "" or item_name == "" or type_name == "": + return _err(-32602, "missing 'path', 'item', or 'type'") + + var theme := ResourceLoader.load(path) as Theme + if theme == null: + return _err(-32001, "failed to load theme: %s" % path) + + var sb := StyleBoxFlat.new() + const Coerce := preload("res://addons/agent_tools/tools/_coerce.gd") + for k in props: + var prop_info := _find_property(sb, k) + if prop_info.is_empty(): + return _err(-32602, "StyleBoxFlat has no property '%s'" % k) + var coerced = Coerce.coerce(props[k], prop_info.type) + if coerced is Dictionary and coerced.has("_error"): + return _err(-32602, "property %s: %s" % [k, coerced._error]) + sb.set(k, coerced) + + theme.set_stylebox(item_name, type_name, sb) + var save_err := ResourceSaver.save(theme, path) + if save_err != OK: + return _err(-32001, "theme save failed: %d" % save_err) + EditorInterface.get_resource_filesystem().update_file(path) + return _ok({ + "path": path, + "item": item_name, + "type": type_name, + "applied_properties": props.keys(), + }) + + +static func _set_entry(params: Dictionary, expected_kind: String, setter_name: String, expected_type: int) -> Dictionary: + var path: String = params.get("path", "") + var item_name: String = params.get("item", "") + var type_name: String = params.get("type", "") + if path == "" or item_name == "" or type_name == "": + return _err(-32602, "missing 'path', 'item', or 'type'") + + var theme := ResourceLoader.load(path) as Theme + if theme == null: + return _err(-32001, "failed to load theme: %s" % path) + + const Coerce := preload("res://addons/agent_tools/tools/_coerce.gd") + var key := "color" if expected_kind == "color" else "value" + var raw = params.get(key) + if raw == null: + return _err(-32602, "missing '%s'" % key) + var coerced = Coerce.coerce(raw, expected_type) + if coerced is Dictionary and coerced.has("_error"): + return _err(-32602, coerced._error) + + theme.call(setter_name, item_name, type_name, coerced) + var save_err := ResourceSaver.save(theme, path) + if save_err != OK: + return _err(-32001, "theme save failed: %d" % save_err) + EditorInterface.get_resource_filesystem().update_file(path) + return _ok({ + "path": path, + "item": item_name, + "type": type_name, + "kind": expected_kind, + }) + + +static func _find_property(obj: Object, name: String) -> Dictionary: + for p in obj.get_property_list(): + if p.name == name: + return p + return {} + + +static func _ok(data) -> Dictionary: + return {"data": data} + + +static func _err(code: int, msg: String) -> Dictionary: + return {"error": {"code": code, "message": msg}} diff --git a/addons/agent_tools/tools/theme_tools.gd.uid b/addons/agent_tools/tools/theme_tools.gd.uid new file mode 100644 index 0000000..1f3ba13 --- /dev/null +++ b/addons/agent_tools/tools/theme_tools.gd.uid @@ -0,0 +1 @@ +uid://b5p1j8mfeeeav diff --git a/addons/agent_tools/tools/user_fs_tools.gd b/addons/agent_tools/tools/user_fs_tools.gd new file mode 100644 index 0000000..7323c99 --- /dev/null +++ b/addons/agent_tools/tools/user_fs_tools.gd @@ -0,0 +1,92 @@ +@tool +extends RefCounted + +# Read-side tools for the `user://` data directory — where games write save files, +# custom-level JSON, settings, etc. Agents couldn't peek at these without running +# the editor and eyeballing. Kept separate from fs.list (which is res://-only) +# because the semantics are different: user:// is runtime-written state, res:// is +# project source. + +# user_fs.read — return a text file's content from user://. +# Params: {path} +static func read(params: Dictionary) -> Dictionary: + var path: String = params.get("path", "") + if path == "": + return _err(-32602, "missing 'path'") + if not path.begins_with("user://"): + return _err(-32602, "path must begin with 'user://'") + if not _within_sandbox(path, "user://"): + return _err(-32602, "path escapes the user:// sandbox ('..' traversal not allowed): %s" % path) + var f := FileAccess.open(path, FileAccess.READ) + if f == null: + return _err(-32001, "failed to open: %s (error %d)" % [path, FileAccess.get_open_error()]) + var size_bytes := f.get_length() + var content := f.get_as_text() + f.close() + return _ok({ + "path": path, + "content": content, + "size_bytes": size_bytes, + }) + + +# user_fs.list — list entries under a user:// directory. +# Params: {dir?: "user://", recursive?: false} +static func list(params: Dictionary) -> Dictionary: + var dir_path: String = params.get("dir", "user://") + var recursive: bool = params.get("recursive", false) + + if not dir_path.begins_with("user://"): + return _err(-32602, "dir must begin with 'user://'") + if not _within_sandbox(dir_path, "user://"): + return _err(-32602, "dir escapes the user:// sandbox ('..' traversal not allowed): %s" % dir_path) + + var files: Array = [] + var dirs: Array = [] + _walk(dir_path, files, dirs, recursive) + + files.sort() + dirs.sort() + return _ok({ + "dir": dir_path, + "files": files, + "dirs": dirs, + "count": files.size(), + }) + + +static func _walk(dir_path: String, out_files: Array, out_dirs: Array, recursive: bool) -> void: + var d := DirAccess.open(dir_path) + if d == null: + return + d.list_dir_begin() + while true: + var name := d.get_next() + if name == "": + break + if name.begins_with("."): + continue + var full := dir_path.path_join(name) + if d.current_is_dir(): + out_dirs.append(full) + if recursive: + _walk(full, out_files, out_dirs, recursive) + else: + out_files.append(full) + d.list_dir_end() + + +# Path-traversal guard — see fs_tools._within_sandbox. `begins_with("user://")` +# is not a sandbox; `..` segments resolve against the real filesystem. +static func _within_sandbox(path: String, prefix: String) -> bool: + var root := ProjectSettings.globalize_path(prefix).simplify_path().trim_suffix("/") + var abs := ProjectSettings.globalize_path(path).simplify_path() + return abs == root or abs.begins_with(root + "/") + + +static func _ok(data) -> Dictionary: + return {"data": data} + + +static func _err(code: int, msg: String) -> Dictionary: + return {"error": {"code": code, "message": msg}} diff --git a/addons/agent_tools/tools/user_fs_tools.gd.uid b/addons/agent_tools/tools/user_fs_tools.gd.uid new file mode 100644 index 0000000..f4c3f2e --- /dev/null +++ b/addons/agent_tools/tools/user_fs_tools.gd.uid @@ -0,0 +1 @@ +uid://btej8otgjy0kj diff --git a/addons/com.heroiclabs.nakama/LICENSE b/addons/com.heroiclabs.nakama/LICENSE new file mode 100644 index 0000000..6b0b127 --- /dev/null +++ b/addons/com.heroiclabs.nakama/LICENSE @@ -0,0 +1,203 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + diff --git a/addons/com.heroiclabs.nakama/Nakama.gd b/addons/com.heroiclabs.nakama/Nakama.gd new file mode 100644 index 0000000..37781d4 --- /dev/null +++ b/addons/com.heroiclabs.nakama/Nakama.gd @@ -0,0 +1,61 @@ +@tool +extends Node + +# The default host address of the server. +const DEFAULT_HOST : String = "127.0.0.1" + +# The default port number of the server. +const DEFAULT_PORT : int = 7350 + +# The default timeout for the connections. +const DEFAULT_TIMEOUT = 3 + +# The default protocol scheme for the client connection. +const DEFAULT_CLIENT_SCHEME : String = "http" + +# The default protocol scheme for the socket connection. +const DEFAULT_SOCKET_SCHEME : String = "ws" + +# The default log level for the Nakama logger. +const DEFAULT_LOG_LEVEL = NakamaLogger.LOG_LEVEL.DEBUG + +var _http_adapter = null +var logger = NakamaLogger.new() + +func _ready() -> void: + process_mode = Node.PROCESS_MODE_ALWAYS + +func get_client_adapter() -> NakamaHTTPAdapter: + if _http_adapter == null: + _http_adapter = NakamaHTTPAdapter.new() + _http_adapter.logger = logger + _http_adapter.name = "NakamaHTTPAdapter" + add_child(_http_adapter) + return _http_adapter + +func create_socket_adapter() -> NakamaSocketAdapter: + var adapter = NakamaSocketAdapter.new() + adapter.name = "NakamaWebSocketAdapter" + adapter.logger = logger + add_child(adapter) + return adapter + +func create_client(p_server_key : String, + p_host : String = DEFAULT_HOST, + p_port : int = DEFAULT_PORT, + p_scheme : String = DEFAULT_CLIENT_SCHEME, + p_timeout : int = DEFAULT_TIMEOUT, + p_log_level : int = DEFAULT_LOG_LEVEL) -> NakamaClient: + logger._level = p_log_level + return NakamaClient.new(get_client_adapter(), p_server_key, p_scheme, p_host, p_port, p_timeout) + +func create_socket(p_host : String = DEFAULT_HOST, + p_port : int = DEFAULT_PORT, + p_scheme : String = DEFAULT_SOCKET_SCHEME) -> NakamaSocket: + return NakamaSocket.new(create_socket_adapter(), p_host, p_port, p_scheme, true) + +func create_socket_from(p_client : NakamaClient) -> NakamaSocket: + var scheme = "ws" + if p_client.scheme == "https": + scheme = "wss" + return NakamaSocket.new(create_socket_adapter(), p_client.host, p_client.port, scheme, true) diff --git a/addons/com.heroiclabs.nakama/Nakama.gd.uid b/addons/com.heroiclabs.nakama/Nakama.gd.uid new file mode 100644 index 0000000..6d63b0f --- /dev/null +++ b/addons/com.heroiclabs.nakama/Nakama.gd.uid @@ -0,0 +1 @@ +uid://dwg1d18n8xo3d diff --git a/addons/com.heroiclabs.nakama/Satori.gd b/addons/com.heroiclabs.nakama/Satori.gd new file mode 100644 index 0000000..423cfb5 --- /dev/null +++ b/addons/com.heroiclabs.nakama/Satori.gd @@ -0,0 +1,41 @@ +@tool +extends Node + +# The default host address of the server. +const DEFAULT_HOST : String = "127.0.0.1" + +# The default port number of the server. +const DEFAULT_PORT : int = 7450 + +# The default timeout for the connections. +const DEFAULT_TIMEOUT = 15 + +# The default protocol scheme for the client connection. +const DEFAULT_CLIENT_SCHEME : String = "http" + +# The default log level for the Satori logger. +const DEFAULT_LOG_LEVEL = SatoriLogger.LOG_LEVEL.DEBUG + +var _http_adapter = null +var logger = SatoriLogger.new() + +func _ready() -> void: + process_mode = Node.PROCESS_MODE_ALWAYS + +func get_client_adapter() -> SatoriHTTPAdapter: + if _http_adapter == null: + _http_adapter = SatoriHTTPAdapter.new() + _http_adapter.logger = logger + _http_adapter.name = "SatoriHTTPAdapter" + add_child(_http_adapter) + return _http_adapter + +func create_client(p_api_key : String, + p_host : String = DEFAULT_HOST, + p_port : int = DEFAULT_PORT, + p_scheme : String = DEFAULT_CLIENT_SCHEME, + p_timeout : int = DEFAULT_TIMEOUT, + p_log_level : int = DEFAULT_LOG_LEVEL, + ) -> SatoriClient: + logger._level = p_log_level + return SatoriClient.new(get_client_adapter(), p_api_key, p_scheme, p_host, p_port, p_timeout) diff --git a/addons/com.heroiclabs.nakama/Satori.gd.uid b/addons/com.heroiclabs.nakama/Satori.gd.uid new file mode 100644 index 0000000..c434d9f --- /dev/null +++ b/addons/com.heroiclabs.nakama/Satori.gd.uid @@ -0,0 +1 @@ +uid://b054oblo2phs diff --git a/addons/com.heroiclabs.nakama/Satori/Event.gd b/addons/com.heroiclabs.nakama/Satori/Event.gd new file mode 100644 index 0000000..445013d --- /dev/null +++ b/addons/com.heroiclabs.nakama/Satori/Event.gd @@ -0,0 +1,65 @@ +extends SatoriAsyncResult + +class_name Event + +# The name of the event. +var name: String + +# The time when the event was triggered. +var timestamp: String + +# Optional value. +var value: String + +# Event metadata, if any. +var metadata: Dictionary + +# Optional event ID assigned by the client, used to de-duplicate in retransmission scenarios. +# If not supplied the server will assign a randomly generated unique event identifier. +var id: String + +# The event constructor. + # Initializes a new Event object. + # + # @param name The name of the event. + # @param timestamp The timestamp of the event. + # @param value The value associated with the event (optional). + # @param metadata The metadata associated with the event (optional). + # @param id The ID of the event (optional). +func _init(name: String, timestamp: float, value: String = "", metadata: Dictionary = {}, id: String = "", p_exception = null): + super(p_exception) + + self.name = name + self.timestamp = unix_to_protobuf_timestamp_format(timestamp) + self.value = value + self.metadata = metadata + self.id = id + +func to_api_event_dict() -> Dictionary: + return { + "name": self.name, + "timestamp": self.timestamp, + "value": self.value, + "metadata": self.metadata, + "id": self.id + } + +func unix_to_protobuf_timestamp_format(unix_time: float) -> String: + # Extract microseconds precision from unix time + var microseconds = int(fmod(unix_time, 1.0) * 1_000_000) + + # Convert seconds to datetime structure + var datetime = Time.get_datetime_dict_from_unix_time(int(unix_time)) + + var year = datetime.year + var month = str(datetime.month).pad_zeros(2) + var day = str(datetime.day).pad_zeros(2) + var hour = str(datetime.hour).pad_zeros(2) + var minute = str(datetime.minute).pad_zeros(2) + var second = str(datetime.second).pad_zeros(2) + var microsecond = str(microseconds).pad_zeros(6) + + # Construct the protobuf timestamp format string + var timestamp_str = "%s-%s-%sT%s:%s:%s.%sZ" % [year, month, day, hour, minute, second, microsecond] + + return timestamp_str \ No newline at end of file diff --git a/addons/com.heroiclabs.nakama/Satori/Event.gd.uid b/addons/com.heroiclabs.nakama/Satori/Event.gd.uid new file mode 100644 index 0000000..e2a9603 --- /dev/null +++ b/addons/com.heroiclabs.nakama/Satori/Event.gd.uid @@ -0,0 +1 @@ +uid://ia0o54hg21m diff --git a/addons/com.heroiclabs.nakama/Satori/SatoriAPI.gd b/addons/com.heroiclabs.nakama/Satori/SatoriAPI.gd new file mode 100644 index 0000000..21aee34 --- /dev/null +++ b/addons/com.heroiclabs.nakama/Satori/SatoriAPI.gd @@ -0,0 +1,1396 @@ +### Code generated by codegen/main.go. DO NOT EDIT. ### + +extends RefCounted +class_name SatoriAPI + +# Log out a session, invalidate a refresh token, or log out all sessions/refresh tokens for a user. +class ApiAuthenticateLogoutRequest extends SatoriAsyncResult: + + const _SCHEMA = { + "refresh_token": {"name": "_refresh_token", "type": TYPE_STRING, "required": false}, + "token": {"name": "_token", "type": TYPE_STRING, "required": false}, + } + + # Refresh token to invalidate. + var _refresh_token + var refresh_token : String: + get: + return "" if not _refresh_token is String else String(_refresh_token) + + # Session token to log out. + var _token + var token : String: + get: + return "" if not _token is String else String(_token) + + func _init(p_exception = null): + super(p_exception) + + static func create(p_ns : GDScript, p_dict : Dictionary) -> ApiAuthenticateLogoutRequest: + return _safe_ret(SatoriSerializer.deserialize(p_ns, "ApiAuthenticateLogoutRequest", p_dict), ApiAuthenticateLogoutRequest) as ApiAuthenticateLogoutRequest + + func serialize() -> Dictionary: + return SatoriSerializer.serialize(self) + + func _to_string() -> String: + if is_exception(): + return get_exception()._to_string() + var output : String = "" + var map_string : String = "" + output += "refresh_token: %s, " % _refresh_token + output += "token: %s, " % _token + output += map_string + return output + +# Authenticate against the server with a refresh token. +class ApiAuthenticateRefreshRequest extends SatoriAsyncResult: + + const _SCHEMA = { + "refresh_token": {"name": "_refresh_token", "type": TYPE_STRING, "required": false}, + } + + # Refresh token. + var _refresh_token + var refresh_token : String: + get: + return "" if not _refresh_token is String else String(_refresh_token) + + func _init(p_exception = null): + super(p_exception) + + static func create(p_ns : GDScript, p_dict : Dictionary) -> ApiAuthenticateRefreshRequest: + return _safe_ret(SatoriSerializer.deserialize(p_ns, "ApiAuthenticateRefreshRequest", p_dict), ApiAuthenticateRefreshRequest) as ApiAuthenticateRefreshRequest + + func serialize() -> Dictionary: + return SatoriSerializer.serialize(self) + + func _to_string() -> String: + if is_exception(): + return get_exception()._to_string() + var output : String = "" + var map_string : String = "" + output += "refresh_token: %s, " % _refresh_token + output += map_string + return output + +# +class ApiAuthenticateRequest extends SatoriAsyncResult: + + const _SCHEMA = { + "custom": {"name": "_custom", "type": TYPE_DICTIONARY, "required": false, "content": TYPE_STRING}, + "default": {"name": "_default", "type": TYPE_DICTIONARY, "required": false, "content": TYPE_STRING}, + "id": {"name": "_id", "type": TYPE_STRING, "required": false}, + } + + # Optional custom properties to update with this call. + # If not set, properties are left as they are on the server. + var _custom + var custom : Dictionary: + get: + return Dictionary() if not _custom is Dictionary else _custom.duplicate() + + # Optional default properties to update with this call. + # If not set, properties are left as they are on the server. + var _default + var default : Dictionary: + get: + return Dictionary() if not _default is Dictionary else _default.duplicate() + + # Identity ID. Must be between eight and 128 characters (inclusive). + # Must be an alphanumeric string with only underscores and hyphens allowed. + var _id + var id : String: + get: + return "" if not _id is String else String(_id) + + func _init(p_exception = null): + super(p_exception) + + static func create(p_ns : GDScript, p_dict : Dictionary) -> ApiAuthenticateRequest: + return _safe_ret(SatoriSerializer.deserialize(p_ns, "ApiAuthenticateRequest", p_dict), ApiAuthenticateRequest) as ApiAuthenticateRequest + + func serialize() -> Dictionary: + return SatoriSerializer.serialize(self) + + func _to_string() -> String: + if is_exception(): + return get_exception()._to_string() + var output : String = "" + var map_string : String = "" + if typeof(_custom) == TYPE_DICTIONARY: + for k in _custom: + map_string += "{%s=%s}, " % [k, _custom[k]] + output += "custom: [%s], " % map_string + map_string = "" + if typeof(_default) == TYPE_DICTIONARY: + for k in _default: + map_string += "{%s=%s}, " % [k, _default[k]] + output += "default: [%s], " % map_string + map_string = "" + output += "id: %s, " % _id + output += map_string + return output + +# A single event. Usually, but not necessarily, part of a batch. +class ApiEvent extends SatoriAsyncResult: + + const _SCHEMA = { + "id": {"name": "_id", "type": TYPE_STRING, "required": false}, + "metadata": {"name": "_metadata", "type": TYPE_DICTIONARY, "required": false, "content": TYPE_STRING}, + "name": {"name": "_name", "type": TYPE_STRING, "required": false}, + "timestamp": {"name": "_timestamp", "type": TYPE_STRING, "required": false}, + "value": {"name": "_value", "type": TYPE_STRING, "required": false}, + } + + # Optional event ID assigned by the client, used to de-duplicate in retransmission scenarios. + # If not supplied the server will assign a randomly generated unique event identifier. + var _id + var id : String: + get: + return "" if not _id is String else String(_id) + + # Event metadata, if any. + var _metadata + var metadata : Dictionary: + get: + return Dictionary() if not _metadata is Dictionary else _metadata.duplicate() + + # Event name. + var _name + var name : String: + get: + return "" if not _name is String else String(_name) + + # The time when the event was triggered on the producer side. + var _timestamp + var timestamp : String: + get: + return "" if not _timestamp is String else String(_timestamp) + + # Optional value. + var _value + var value : String: + get: + return "" if not _value is String else String(_value) + + func _init(p_exception = null): + super(p_exception) + + static func create(p_ns : GDScript, p_dict : Dictionary) -> ApiEvent: + return _safe_ret(SatoriSerializer.deserialize(p_ns, "ApiEvent", p_dict), ApiEvent) as ApiEvent + + func serialize() -> Dictionary: + return SatoriSerializer.serialize(self) + + func _to_string() -> String: + if is_exception(): + return get_exception()._to_string() + var output : String = "" + var map_string : String = "" + output += "id: %s, " % _id + if typeof(_metadata) == TYPE_DICTIONARY: + for k in _metadata: + map_string += "{%s=%s}, " % [k, _metadata[k]] + output += "metadata: [%s], " % map_string + map_string = "" + output += "name: %s, " % _name + output += "timestamp: %s, " % _timestamp + output += "value: %s, " % _value + output += map_string + return output + +# +class ApiEventRequest extends SatoriAsyncResult: + + const _SCHEMA = { + "events": {"name": "_events", "type": TYPE_ARRAY, "required": false, "content": TYPE_DICTIONARY}, + } + + # Some number of events produced by a client. + var _events + var events : Array: + get: + return Array() if not _events is Array else Array(_events) + + func _init(p_exception = null): + super(p_exception) + + static func create(p_ns : GDScript, p_dict : Dictionary) -> ApiEventRequest: + return _safe_ret(SatoriSerializer.deserialize(p_ns, "ApiEventRequest", p_dict), ApiEventRequest) as ApiEventRequest + + func serialize() -> Dictionary: + return SatoriSerializer.serialize(self) + + func _to_string() -> String: + if is_exception(): + return get_exception()._to_string() + var output : String = "" + var map_string : String = "" + output += "events: %s, " % [_events] + output += map_string + return output + +# An experiment that this user is partaking. +class ApiExperiment extends SatoriAsyncResult: + + const _SCHEMA = { + "name": {"name": "_name", "type": TYPE_STRING, "required": false}, + "value": {"name": "_value", "type": TYPE_STRING, "required": false}, + } + + # + var _name + var name : String: + get: + return "" if not _name is String else String(_name) + + # Value associated with this Experiment. + var _value + var value : String: + get: + return "" if not _value is String else String(_value) + + func _init(p_exception = null): + super(p_exception) + + static func create(p_ns : GDScript, p_dict : Dictionary) -> ApiExperiment: + return _safe_ret(SatoriSerializer.deserialize(p_ns, "ApiExperiment", p_dict), ApiExperiment) as ApiExperiment + + func serialize() -> Dictionary: + return SatoriSerializer.serialize(self) + + func _to_string() -> String: + if is_exception(): + return get_exception()._to_string() + var output : String = "" + var map_string : String = "" + output += "name: %s, " % _name + output += "value: %s, " % _value + output += map_string + return output + +# All experiments that this identity is involved with. +class ApiExperimentList extends SatoriAsyncResult: + + const _SCHEMA = { + "experiments": {"name": "_experiments", "type": TYPE_ARRAY, "required": false, "content": TYPE_DICTIONARY}, + } + + # All experiments for this identity. + var _experiments + var experiments : Array: + get: + return Array() if not _experiments is Array else Array(_experiments) + + func _init(p_exception = null): + super(p_exception) + + static func create(p_ns : GDScript, p_dict : Dictionary) -> ApiExperimentList: + return _safe_ret(SatoriSerializer.deserialize(p_ns, "ApiExperimentList", p_dict), ApiExperimentList) as ApiExperimentList + + func serialize() -> Dictionary: + return SatoriSerializer.serialize(self) + + func _to_string() -> String: + if is_exception(): + return get_exception()._to_string() + var output : String = "" + var map_string : String = "" + output += "experiments: %s, " % [_experiments] + output += map_string + return output + +# Feature flag available to the identity. +class ApiFlag extends SatoriAsyncResult: + + const _SCHEMA = { + "condition_changed": {"name": "_condition_changed", "type": TYPE_BOOL, "required": false}, + "name": {"name": "_name", "type": TYPE_STRING, "required": false}, + "value": {"name": "_value", "type": TYPE_STRING, "required": false}, + } + + # Whether the value for this flag has conditionally changed from the default state. + var _condition_changed + var condition_changed : bool: + get: + return false if not _condition_changed is bool else bool(_condition_changed) + + # + var _name + var name : String: + get: + return "" if not _name is String else String(_name) + + # Value associated with this flag. + var _value + var value : String: + get: + return "" if not _value is String else String(_value) + + func _init(p_exception = null): + super(p_exception) + + static func create(p_ns : GDScript, p_dict : Dictionary) -> ApiFlag: + return _safe_ret(SatoriSerializer.deserialize(p_ns, "ApiFlag", p_dict), ApiFlag) as ApiFlag + + func serialize() -> Dictionary: + return SatoriSerializer.serialize(self) + + func _to_string() -> String: + if is_exception(): + return get_exception()._to_string() + var output : String = "" + var map_string : String = "" + output += "condition_changed: %s, " % _condition_changed + output += "name: %s, " % _name + output += "value: %s, " % _value + output += map_string + return output + +# +class ApiFlagList extends SatoriAsyncResult: + + const _SCHEMA = { + "flags": {"name": "_flags", "type": TYPE_ARRAY, "required": false, "content": TYPE_DICTIONARY}, + } + + # + var _flags + var flags : Array: + get: + return Array() if not _flags is Array else Array(_flags) + + func _init(p_exception = null): + super(p_exception) + + static func create(p_ns : GDScript, p_dict : Dictionary) -> ApiFlagList: + return _safe_ret(SatoriSerializer.deserialize(p_ns, "ApiFlagList", p_dict), ApiFlagList) as ApiFlagList + + func serialize() -> Dictionary: + return SatoriSerializer.serialize(self) + + func _to_string() -> String: + if is_exception(): + return get_exception()._to_string() + var output : String = "" + var map_string : String = "" + output += "flags: %s, " % [_flags] + output += map_string + return output + +# A response containing all the messages for an identity. +class ApiGetMessageListResponse extends SatoriAsyncResult: + + const _SCHEMA = { + "cacheable_cursor": {"name": "_cacheable_cursor", "type": TYPE_STRING, "required": false}, + "messages": {"name": "_messages", "type": TYPE_ARRAY, "required": false, "content": TYPE_DICTIONARY}, + "next_cursor": {"name": "_next_cursor", "type": TYPE_STRING, "required": false}, + "prev_cursor": {"name": "_prev_cursor", "type": TYPE_STRING, "required": false}, + } + + # Cacheable cursor to list newer messages. Durable and designed to be stored, unlike next/prev cursors. + var _cacheable_cursor + var cacheable_cursor : String: + get: + return "" if not _cacheable_cursor is String else String(_cacheable_cursor) + + # The list of messages. + var _messages + var messages : Array: + get: + return Array() if not _messages is Array else Array(_messages) + + # The cursor to send when retrieving the next page, if any. + var _next_cursor + var next_cursor : String: + get: + return "" if not _next_cursor is String else String(_next_cursor) + + # The cursor to send when retrieving the previous page, if any. + var _prev_cursor + var prev_cursor : String: + get: + return "" if not _prev_cursor is String else String(_prev_cursor) + + func _init(p_exception = null): + super(p_exception) + + static func create(p_ns : GDScript, p_dict : Dictionary) -> ApiGetMessageListResponse: + return _safe_ret(SatoriSerializer.deserialize(p_ns, "ApiGetMessageListResponse", p_dict), ApiGetMessageListResponse) as ApiGetMessageListResponse + + func serialize() -> Dictionary: + return SatoriSerializer.serialize(self) + + func _to_string() -> String: + if is_exception(): + return get_exception()._to_string() + var output : String = "" + var map_string : String = "" + output += "cacheable_cursor: %s, " % _cacheable_cursor + output += "messages: %s, " % [_messages] + output += "next_cursor: %s, " % _next_cursor + output += "prev_cursor: %s, " % _prev_cursor + output += map_string + return output + +# Enrich/replace the current session with a new ID. +class ApiIdentifyRequest extends SatoriAsyncResult: + + const _SCHEMA = { + "custom": {"name": "_custom", "type": TYPE_DICTIONARY, "required": false, "content": TYPE_STRING}, + "default": {"name": "_default", "type": TYPE_DICTIONARY, "required": false, "content": TYPE_STRING}, + "id": {"name": "_id", "type": TYPE_STRING, "required": false}, + } + + # Optional custom properties to update with this call. + # If not set, properties are left as they are on the server. + var _custom + var custom : Dictionary: + get: + return Dictionary() if not _custom is Dictionary else _custom.duplicate() + + # Optional default properties to update with this call. + # If not set, properties are left as they are on the server. + var _default + var default : Dictionary: + get: + return Dictionary() if not _default is Dictionary else _default.duplicate() + + # Identity ID to enrich the current session and return a new session. Old session will no longer be usable. + var _id + var id : String: + get: + return "" if not _id is String else String(_id) + + func _init(p_exception = null): + super(p_exception) + + static func create(p_ns : GDScript, p_dict : Dictionary) -> ApiIdentifyRequest: + return _safe_ret(SatoriSerializer.deserialize(p_ns, "ApiIdentifyRequest", p_dict), ApiIdentifyRequest) as ApiIdentifyRequest + + func serialize() -> Dictionary: + return SatoriSerializer.serialize(self) + + func _to_string() -> String: + if is_exception(): + return get_exception()._to_string() + var output : String = "" + var map_string : String = "" + if typeof(_custom) == TYPE_DICTIONARY: + for k in _custom: + map_string += "{%s=%s}, " % [k, _custom[k]] + output += "custom: [%s], " % map_string + map_string = "" + if typeof(_default) == TYPE_DICTIONARY: + for k in _default: + map_string += "{%s=%s}, " % [k, _default[k]] + output += "default: [%s], " % map_string + map_string = "" + output += "id: %s, " % _id + output += map_string + return output + +# A single live event. +class ApiLiveEvent extends SatoriAsyncResult: + + const _SCHEMA = { + "active_end_time_sec": {"name": "_active_end_time_sec", "type": TYPE_STRING, "required": false}, + "active_start_time_sec": {"name": "_active_start_time_sec", "type": TYPE_STRING, "required": false}, + "description": {"name": "_description", "type": TYPE_STRING, "required": false}, + "id": {"name": "_id", "type": TYPE_STRING, "required": false}, + "name": {"name": "_name", "type": TYPE_STRING, "required": false}, + "value": {"name": "_value", "type": TYPE_STRING, "required": false}, + } + + # End time of current event run. + var _active_end_time_sec + var active_end_time_sec : String: + get: + return "" if not _active_end_time_sec is String else String(_active_end_time_sec) + + # Start time of current event run. + var _active_start_time_sec + var active_start_time_sec : String: + get: + return "" if not _active_start_time_sec is String else String(_active_start_time_sec) + + # Description. + var _description + var description : String: + get: + return "" if not _description is String else String(_description) + + # The live event identifier. + var _id + var id : String: + get: + return "" if not _id is String else String(_id) + + # Name. + var _name + var name : String: + get: + return "" if not _name is String else String(_name) + + # Event value. + var _value + var value : String: + get: + return "" if not _value is String else String(_value) + + func _init(p_exception = null): + super(p_exception) + + static func create(p_ns : GDScript, p_dict : Dictionary) -> ApiLiveEvent: + return _safe_ret(SatoriSerializer.deserialize(p_ns, "ApiLiveEvent", p_dict), ApiLiveEvent) as ApiLiveEvent + + func serialize() -> Dictionary: + return SatoriSerializer.serialize(self) + + func _to_string() -> String: + if is_exception(): + return get_exception()._to_string() + var output : String = "" + var map_string : String = "" + output += "active_end_time_sec: %s, " % _active_end_time_sec + output += "active_start_time_sec: %s, " % _active_start_time_sec + output += "description: %s, " % _description + output += "id: %s, " % _id + output += "name: %s, " % _name + output += "value: %s, " % _value + output += map_string + return output + +# List of Live events. +class ApiLiveEventList extends SatoriAsyncResult: + + const _SCHEMA = { + "live_events": {"name": "_live_events", "type": TYPE_ARRAY, "required": false, "content": TYPE_DICTIONARY}, + } + + # Live events. + var _live_events + var live_events : Array: + get: + return Array() if not _live_events is Array else Array(_live_events) + + func _init(p_exception = null): + super(p_exception) + + static func create(p_ns : GDScript, p_dict : Dictionary) -> ApiLiveEventList: + return _safe_ret(SatoriSerializer.deserialize(p_ns, "ApiLiveEventList", p_dict), ApiLiveEventList) as ApiLiveEventList + + func serialize() -> Dictionary: + return SatoriSerializer.serialize(self) + + func _to_string() -> String: + if is_exception(): + return get_exception()._to_string() + var output : String = "" + var map_string : String = "" + output += "live_events: %s, " % [_live_events] + output += map_string + return output + +# A scheduled message. +class ApiMessage extends SatoriAsyncResult: + + const _SCHEMA = { + "consume_time": {"name": "_consume_time", "type": TYPE_STRING, "required": false}, + "create_time": {"name": "_create_time", "type": TYPE_STRING, "required": false}, + "metadata": {"name": "_metadata", "type": TYPE_DICTIONARY, "required": false, "content": TYPE_STRING}, + "read_time": {"name": "_read_time", "type": TYPE_STRING, "required": false}, + "schedule_id": {"name": "_schedule_id", "type": TYPE_STRING, "required": false}, + "send_time": {"name": "_send_time", "type": TYPE_STRING, "required": false}, + "text": {"name": "_text", "type": TYPE_STRING, "required": false}, + "update_time": {"name": "_update_time", "type": TYPE_STRING, "required": false}, + } + + # The time the message was consumed by the identity. + var _consume_time + var consume_time : String: + get: + return "" if not _consume_time is String else String(_consume_time) + + # The time the message was created. + var _create_time + var create_time : String: + get: + return "" if not _create_time is String else String(_create_time) + + # A key-value pairs of metadata. + var _metadata + var metadata : Dictionary: + get: + return Dictionary() if not _metadata is Dictionary else _metadata.duplicate() + + # The time the message was read by the client. + var _read_time + var read_time : String: + get: + return "" if not _read_time is String else String(_read_time) + + # The identifier of the schedule. + var _schedule_id + var schedule_id : String: + get: + return "" if not _schedule_id is String else String(_schedule_id) + + # The send time for the message. + var _send_time + var send_time : String: + get: + return "" if not _send_time is String else String(_send_time) + + # The message's text. + var _text + var text : String: + get: + return "" if not _text is String else String(_text) + + # The time the message was updated. + var _update_time + var update_time : String: + get: + return "" if not _update_time is String else String(_update_time) + + func _init(p_exception = null): + super(p_exception) + + static func create(p_ns : GDScript, p_dict : Dictionary) -> ApiMessage: + return _safe_ret(SatoriSerializer.deserialize(p_ns, "ApiMessage", p_dict), ApiMessage) as ApiMessage + + func serialize() -> Dictionary: + return SatoriSerializer.serialize(self) + + func _to_string() -> String: + if is_exception(): + return get_exception()._to_string() + var output : String = "" + var map_string : String = "" + output += "consume_time: %s, " % _consume_time + output += "create_time: %s, " % _create_time + if typeof(_metadata) == TYPE_DICTIONARY: + for k in _metadata: + map_string += "{%s=%s}, " % [k, _metadata[k]] + output += "metadata: [%s], " % map_string + map_string = "" + output += "read_time: %s, " % _read_time + output += "schedule_id: %s, " % _schedule_id + output += "send_time: %s, " % _send_time + output += "text: %s, " % _text + output += "update_time: %s, " % _update_time + output += map_string + return output + +# Properties associated with an identity. +class ApiProperties extends SatoriAsyncResult: + + const _SCHEMA = { + "computed": {"name": "_computed", "type": TYPE_DICTIONARY, "required": false, "content": TYPE_STRING}, + "custom": {"name": "_custom", "type": TYPE_DICTIONARY, "required": false, "content": TYPE_STRING}, + "default": {"name": "_default", "type": TYPE_DICTIONARY, "required": false, "content": TYPE_STRING}, + } + + # Event computed properties. + var _computed + var computed : Dictionary: + get: + return Dictionary() if not _computed is Dictionary else _computed.duplicate() + + # Event custom properties. + var _custom + var custom : Dictionary: + get: + return Dictionary() if not _custom is Dictionary else _custom.duplicate() + + # Event default properties. + var _default + var default : Dictionary: + get: + return Dictionary() if not _default is Dictionary else _default.duplicate() + + func _init(p_exception = null): + super(p_exception) + + static func create(p_ns : GDScript, p_dict : Dictionary) -> ApiProperties: + return _safe_ret(SatoriSerializer.deserialize(p_ns, "ApiProperties", p_dict), ApiProperties) as ApiProperties + + func serialize() -> Dictionary: + return SatoriSerializer.serialize(self) + + func _to_string() -> String: + if is_exception(): + return get_exception()._to_string() + var output : String = "" + var map_string : String = "" + if typeof(_computed) == TYPE_DICTIONARY: + for k in _computed: + map_string += "{%s=%s}, " % [k, _computed[k]] + output += "computed: [%s], " % map_string + map_string = "" + if typeof(_custom) == TYPE_DICTIONARY: + for k in _custom: + map_string += "{%s=%s}, " % [k, _custom[k]] + output += "custom: [%s], " % map_string + map_string = "" + if typeof(_default) == TYPE_DICTIONARY: + for k in _default: + map_string += "{%s=%s}, " % [k, _default[k]] + output += "default: [%s], " % map_string + map_string = "" + output += map_string + return output + +# A session. +class ApiSession extends SatoriAsyncResult: + + const _SCHEMA = { + "properties": {"name": "_properties", "type": "ApiProperties", "required": false}, + "refresh_token": {"name": "_refresh_token", "type": TYPE_STRING, "required": false}, + "token": {"name": "_token", "type": TYPE_STRING, "required": false}, + } + + # Properties associated with this identity. + var _properties + var properties : ApiProperties: + get: + return _properties as ApiProperties + + # Refresh token. + var _refresh_token + var refresh_token : String: + get: + return "" if not _refresh_token is String else String(_refresh_token) + + # Token credential. + var _token + var token : String: + get: + return "" if not _token is String else String(_token) + + func _init(p_exception = null): + super(p_exception) + + static func create(p_ns : GDScript, p_dict : Dictionary) -> ApiSession: + return _safe_ret(SatoriSerializer.deserialize(p_ns, "ApiSession", p_dict), ApiSession) as ApiSession + + func serialize() -> Dictionary: + return SatoriSerializer.serialize(self) + + func _to_string() -> String: + if is_exception(): + return get_exception()._to_string() + var output : String = "" + var map_string : String = "" + output += "properties: %s, " % _properties + output += "refresh_token: %s, " % _refresh_token + output += "token: %s, " % _token + output += map_string + return output + +# Update Properties associated with this identity. +class ApiUpdatePropertiesRequest extends SatoriAsyncResult: + + const _SCHEMA = { + "custom": {"name": "_custom", "type": TYPE_DICTIONARY, "required": false, "content": TYPE_STRING}, + "default": {"name": "_default", "type": TYPE_DICTIONARY, "required": false, "content": TYPE_STRING}, + "recompute": {"name": "_recompute", "type": TYPE_BOOL, "required": false}, + } + + # Event custom properties. + var _custom + var custom : Dictionary: + get: + return Dictionary() if not _custom is Dictionary else _custom.duplicate() + + # Event default properties. + var _default + var default : Dictionary: + get: + return Dictionary() if not _default is Dictionary else _default.duplicate() + + # Informs the server to recompute the audience membership of the identity. + var _recompute + var recompute : bool: + get: + return false if not _recompute is bool else bool(_recompute) + + func _init(p_exception = null): + super(p_exception) + + static func create(p_ns : GDScript, p_dict : Dictionary) -> ApiUpdatePropertiesRequest: + return _safe_ret(SatoriSerializer.deserialize(p_ns, "ApiUpdatePropertiesRequest", p_dict), ApiUpdatePropertiesRequest) as ApiUpdatePropertiesRequest + + func serialize() -> Dictionary: + return SatoriSerializer.serialize(self) + + func _to_string() -> String: + if is_exception(): + return get_exception()._to_string() + var output : String = "" + var map_string : String = "" + if typeof(_custom) == TYPE_DICTIONARY: + for k in _custom: + map_string += "{%s=%s}, " % [k, _custom[k]] + output += "custom: [%s], " % map_string + map_string = "" + if typeof(_default) == TYPE_DICTIONARY: + for k in _default: + map_string += "{%s=%s}, " % [k, _default[k]] + output += "default: [%s], " % map_string + map_string = "" + output += "recompute: %s, " % _recompute + output += map_string + return output + +# +class ProtobufAny extends SatoriAsyncResult: + + const _SCHEMA = { + "type": {"name": "_type", "type": TYPE_STRING, "required": false}, + } + + # + var _type + var type : String: + get: + return "" if not _type is String else String(_type) + + func _init(p_exception = null): + super(p_exception) + + static func create(p_ns : GDScript, p_dict : Dictionary) -> ProtobufAny: + return _safe_ret(SatoriSerializer.deserialize(p_ns, "ProtobufAny", p_dict), ProtobufAny) as ProtobufAny + + func serialize() -> Dictionary: + return SatoriSerializer.serialize(self) + + func _to_string() -> String: + if is_exception(): + return get_exception()._to_string() + var output : String = "" + var map_string : String = "" + output += "type: %s, " % _type + output += map_string + return output + +# +class RpcStatus extends SatoriAsyncResult: + + const _SCHEMA = { + "code": {"name": "_code", "type": TYPE_INT, "required": false}, + "details": {"name": "_details", "type": TYPE_ARRAY, "required": false, "content": TYPE_DICTIONARY}, + "message": {"name": "_message", "type": TYPE_STRING, "required": false}, + } + + # + var _code + var code : int: + get: + return 0 if not _code is int else int(_code) + + # + var _details + var details : Array: + get: + return Array() if not _details is Array else Array(_details) + + # + var _message + var message : String: + get: + return "" if not _message is String else String(_message) + + func _init(p_exception = null): + super(p_exception) + + static func create(p_ns : GDScript, p_dict : Dictionary) -> RpcStatus: + return _safe_ret(SatoriSerializer.deserialize(p_ns, "RpcStatus", p_dict), RpcStatus) as RpcStatus + + func serialize() -> Dictionary: + return SatoriSerializer.serialize(self) + + func _to_string() -> String: + if is_exception(): + return get_exception()._to_string() + var output : String = "" + var map_string : String = "" + output += "code: %s, " % _code + output += "details: %s, " % [_details] + output += "message: %s, " % _message + output += map_string + return output + +# The low level client for the Satori API. +class ApiClient extends RefCounted: + + var _base_uri : String + + var _http_adapter + var _namespace : GDScript + var _server_key : String + var auto_refresh := true + var auto_refresh_time := 300 + + var auto_retry : bool: + set(p_value): + _http_adapter.auto_retry = p_value + get: + return _http_adapter.auto_retry + + var auto_retry_count : int: + set(p_value): + _http_adapter.auto_retry_count = p_value + get: + return _http_adapter.auto_retry_count + + var auto_retry_backoff_base : int: + set(p_value): + _http_adapter.auto_retry_backoff_base = p_value + get: + return _http_adapter.auto_retry_backoff_base + + var last_cancel_token: + get: + return _http_adapter.get_last_token() + + func _init(p_base_uri : String, p_http_adapter, p_namespace : GDScript, p_server_key : String, p_timeout : int = 10): + _base_uri = p_base_uri + _http_adapter = p_http_adapter + _http_adapter.timeout = p_timeout + _namespace = p_namespace + _server_key = p_server_key + + + func _refresh_session(p_session : SatoriSession): + if auto_refresh and p_session.is_valid() and p_session.refresh_token and not p_session.is_refresh_expired() and p_session.would_expire_in(auto_refresh_time): + var request = ApiAuthenticateRefreshRequest.new() + request._token = p_session.refresh_token + return await authenticate_refresh_async(_server_key, "", request) + return null + + func cancel_request(p_token): + if p_token: + _http_adapter.cancel_request(p_token) + + # A healthcheck which load balancers can use to check the service. + func healthcheck_async( + p_session : SatoriSession + ) -> SatoriAsyncResult: + var try_refresh = await _refresh_session(p_session) + if try_refresh != null: + if try_refresh.is_exception(): + return SatoriAsyncResult.new(try_refresh.get_exception()) + await p_session.refresh(try_refresh) + var urlpath : String = "/healthcheck" + var query_params = "" + var uri = "%s%s%s" % [_base_uri, urlpath, "?" + query_params if query_params else ""] + var method = "GET" + var headers = {} + var header = "Bearer %s" % p_session.token + headers["Authorization"] = header + + var content : PackedByteArray = PackedByteArray() + + var result = await _http_adapter.send_async(method, uri, headers, content) + if result is SatoriException: + return SatoriAsyncResult.new(result) + return SatoriAsyncResult.new() + + # A readycheck which load balancers can use to check the service. + func readycheck_async( + p_session : SatoriSession + ) -> SatoriAsyncResult: + var try_refresh = await _refresh_session(p_session) + if try_refresh != null: + if try_refresh.is_exception(): + return SatoriAsyncResult.new(try_refresh.get_exception()) + await p_session.refresh(try_refresh) + var urlpath : String = "/readycheck" + var query_params = "" + var uri = "%s%s%s" % [_base_uri, urlpath, "?" + query_params if query_params else ""] + var method = "GET" + var headers = {} + var header = "Bearer %s" % p_session.token + headers["Authorization"] = header + + var content : PackedByteArray = PackedByteArray() + + var result = await _http_adapter.send_async(method, uri, headers, content) + if result is SatoriException: + return SatoriAsyncResult.new(result) + return SatoriAsyncResult.new() + + # Authenticate against the server. + func authenticate_async( + p_basic_auth_username : String + , p_basic_auth_password : String + , p_body : ApiAuthenticateRequest + ) -> ApiSession: + var urlpath : String = "/v1/authenticate" + var query_params = "" + var uri = "%s%s%s" % [_base_uri, urlpath, "?" + query_params if query_params else ""] + var method = "POST" + var headers = {} + var credentials = Marshalls.utf8_to_base64(p_basic_auth_username + ":" + p_basic_auth_password) + var header = "Basic %s" % credentials + headers["Authorization"] = header + + var content : PackedByteArray = PackedByteArray() + content = JSON.stringify(p_body.serialize()).to_utf8_buffer() + + var result = await _http_adapter.send_async(method, uri, headers, content) + if result is SatoriException: + return ApiSession.new(result) + var out : ApiSession = SatoriSerializer.deserialize(_namespace, "ApiSession", result) + return out + + # Log out a session, invalidate a refresh token, or log out all sessions/refresh tokens for a user. + func authenticate_logout_async( + p_session : SatoriSession + , p_body : ApiAuthenticateLogoutRequest + ) -> SatoriAsyncResult: + var try_refresh = await _refresh_session(p_session) + if try_refresh != null: + if try_refresh.is_exception(): + return SatoriAsyncResult.new(try_refresh.get_exception()) + await p_session.refresh(try_refresh) + var urlpath : String = "/v1/authenticate/logout" + var query_params = "" + var uri = "%s%s%s" % [_base_uri, urlpath, "?" + query_params if query_params else ""] + var method = "POST" + var headers = {} + var header = "Bearer %s" % p_session.token + headers["Authorization"] = header + + var content : PackedByteArray = PackedByteArray() + content = JSON.stringify(p_body.serialize()).to_utf8_buffer() + + var result = await _http_adapter.send_async(method, uri, headers, content) + if result is SatoriException: + return SatoriAsyncResult.new(result) + return SatoriAsyncResult.new() + + # Refresh a user's session using a refresh token retrieved from a previous authentication request. + func authenticate_refresh_async( + p_basic_auth_username : String + , p_basic_auth_password : String + , p_body : ApiAuthenticateRefreshRequest + ) -> ApiSession: + var urlpath : String = "/v1/authenticate/refresh" + var query_params = "" + var uri = "%s%s%s" % [_base_uri, urlpath, "?" + query_params if query_params else ""] + var method = "POST" + var headers = {} + var credentials = Marshalls.utf8_to_base64(p_basic_auth_username + ":" + p_basic_auth_password) + var header = "Basic %s" % credentials + headers["Authorization"] = header + + var content : PackedByteArray = PackedByteArray() + content = JSON.stringify(p_body.serialize()).to_utf8_buffer() + + var result = await _http_adapter.send_async(method, uri, headers, content) + if result is SatoriException: + return ApiSession.new(result) + var out : ApiSession = SatoriSerializer.deserialize(_namespace, "ApiSession", result) + return out + + # Publish an event for this session. + func event_async( + p_session : SatoriSession + , p_body : ApiEventRequest + ) -> SatoriAsyncResult: + var try_refresh = await _refresh_session(p_session) + if try_refresh != null: + if try_refresh.is_exception(): + return SatoriAsyncResult.new(try_refresh.get_exception()) + await p_session.refresh(try_refresh) + var urlpath : String = "/v1/event" + var query_params = "" + var uri = "%s%s%s" % [_base_uri, urlpath, "?" + query_params if query_params else ""] + var method = "POST" + var headers = {} + var header = "Bearer %s" % p_session.token + headers["Authorization"] = header + + var content : PackedByteArray = PackedByteArray() + content = JSON.stringify(p_body.serialize()).to_utf8_buffer() + + var result = await _http_adapter.send_async(method, uri, headers, content) + if result is SatoriException: + return SatoriAsyncResult.new(result) + return SatoriAsyncResult.new() + + # Get or list all available experiments for this identity. + func get_experiments_async( + p_session : SatoriSession + , p_names = null # : array + ) -> ApiExperimentList: + var try_refresh = await _refresh_session(p_session) + if try_refresh != null: + if try_refresh.is_exception(): + return ApiExperimentList.new(try_refresh.get_exception()) + await p_session.refresh(try_refresh) + var urlpath : String = "/v1/experiment" + var query_params = "" + if p_names != null: + for elem in p_names: + query_params += "names=%s&" % elem + var uri = "%s%s%s" % [_base_uri, urlpath, "?" + query_params if query_params else ""] + var method = "GET" + var headers = {} + var header = "Bearer %s" % p_session.token + headers["Authorization"] = header + + var content : PackedByteArray = PackedByteArray() + + var result = await _http_adapter.send_async(method, uri, headers, content) + if result is SatoriException: + return ApiExperimentList.new(result) + var out : ApiExperimentList = SatoriSerializer.deserialize(_namespace, "ApiExperimentList", result) + return out + + # List all available flags for this identity. + func get_flags_async( + p_bearer_token : String + , p_names = null # : array + ) -> ApiFlagList: + var urlpath : String = "/v1/flag" + var query_params = "" + if p_names != null: + for elem in p_names: + query_params += "names=%s&" % elem + var uri = "%s%s%s" % [_base_uri, urlpath, "?" + query_params if query_params else ""] + var method = "GET" + var headers = {} + if (p_bearer_token): + var header = "Bearer %s" % p_bearer_token + headers["Authorization"] = header + + var content : PackedByteArray = PackedByteArray() + + var result = await _http_adapter.send_async(method, uri, headers, content) + if result is SatoriException: + return ApiFlagList.new(result) + var out : ApiFlagList = SatoriSerializer.deserialize(_namespace, "ApiFlagList", result) + return out + + # Enrich/replace the current session with new identifier. + func identify_async( + p_session : SatoriSession + , p_body : ApiIdentifyRequest + ) -> ApiSession: + var try_refresh = await _refresh_session(p_session) + if try_refresh != null: + if try_refresh.is_exception(): + return ApiSession.new(try_refresh.get_exception()) + await p_session.refresh(try_refresh) + var urlpath : String = "/v1/identify" + var query_params = "" + var uri = "%s%s%s" % [_base_uri, urlpath, "?" + query_params if query_params else ""] + var method = "PUT" + var headers = {} + var header = "Bearer %s" % p_session.token + headers["Authorization"] = header + + var content : PackedByteArray = PackedByteArray() + content = JSON.stringify(p_body.serialize()).to_utf8_buffer() + + var result = await _http_adapter.send_async(method, uri, headers, content) + if result is SatoriException: + return ApiSession.new(result) + var out : ApiSession = SatoriSerializer.deserialize(_namespace, "ApiSession", result) + return out + + # Delete the caller's identity and associated data. + func delete_identity_async( + p_session : SatoriSession + ) -> SatoriAsyncResult: + var try_refresh = await _refresh_session(p_session) + if try_refresh != null: + if try_refresh.is_exception(): + return SatoriAsyncResult.new(try_refresh.get_exception()) + await p_session.refresh(try_refresh) + var urlpath : String = "/v1/identity" + var query_params = "" + var uri = "%s%s%s" % [_base_uri, urlpath, "?" + query_params if query_params else ""] + var method = "DELETE" + var headers = {} + var header = "Bearer %s" % p_session.token + headers["Authorization"] = header + + var content : PackedByteArray = PackedByteArray() + + var result = await _http_adapter.send_async(method, uri, headers, content) + if result is SatoriException: + return SatoriAsyncResult.new(result) + return SatoriAsyncResult.new() + + # List available live events. + func get_live_events_async( + p_session : SatoriSession + , p_names = null # : array + ) -> ApiLiveEventList: + var try_refresh = await _refresh_session(p_session) + if try_refresh != null: + if try_refresh.is_exception(): + return ApiLiveEventList.new(try_refresh.get_exception()) + await p_session.refresh(try_refresh) + var urlpath : String = "/v1/live-event" + var query_params = "" + if p_names != null: + for elem in p_names: + query_params += "names=%s&" % elem + var uri = "%s%s%s" % [_base_uri, urlpath, "?" + query_params if query_params else ""] + var method = "GET" + var headers = {} + var header = "Bearer %s" % p_session.token + headers["Authorization"] = header + + var content : PackedByteArray = PackedByteArray() + + var result = await _http_adapter.send_async(method, uri, headers, content) + if result is SatoriException: + return ApiLiveEventList.new(result) + var out : ApiLiveEventList = SatoriSerializer.deserialize(_namespace, "ApiLiveEventList", result) + return out + + # Get the list of messages for the identity. + func get_message_list_async( + p_session : SatoriSession + , p_limit = null # : integer + , p_forward = null # : boolean + , p_cursor = null # : string + ) -> ApiGetMessageListResponse: + var try_refresh = await _refresh_session(p_session) + if try_refresh != null: + if try_refresh.is_exception(): + return ApiGetMessageListResponse.new(try_refresh.get_exception()) + await p_session.refresh(try_refresh) + var urlpath : String = "/v1/message" + var query_params = "" + if p_limit != null: + query_params += "limit=%d&" % p_limit + if p_forward != null: + query_params += "forward=%s&" % str(bool(p_forward)).to_lower() + if p_cursor != null: + query_params += "cursor=%s&" % SatoriSerializer.escape_http(p_cursor) + var uri = "%s%s%s" % [_base_uri, urlpath, "?" + query_params if query_params else ""] + var method = "GET" + var headers = {} + var header = "Bearer %s" % p_session.token + headers["Authorization"] = header + + var content : PackedByteArray = PackedByteArray() + + var result = await _http_adapter.send_async(method, uri, headers, content) + if result is SatoriException: + return ApiGetMessageListResponse.new(result) + var out : ApiGetMessageListResponse = SatoriSerializer.deserialize(_namespace, "ApiGetMessageListResponse", result) + return out + + # Deletes a message for an identity. + func delete_message_async( + p_session : SatoriSession + , p_id : String + ) -> SatoriAsyncResult: + var try_refresh = await _refresh_session(p_session) + if try_refresh != null: + if try_refresh.is_exception(): + return SatoriAsyncResult.new(try_refresh.get_exception()) + await p_session.refresh(try_refresh) + var urlpath : String = "/v1/message/{id}" + urlpath = urlpath.replace("{id}", SatoriSerializer.escape_http(p_id)) + var query_params = "" + var uri = "%s%s%s" % [_base_uri, urlpath, "?" + query_params if query_params else ""] + var method = "DELETE" + var headers = {} + var header = "Bearer %s" % p_session.token + headers["Authorization"] = header + + var content : PackedByteArray = PackedByteArray() + + var result = await _http_adapter.send_async(method, uri, headers, content) + if result is SatoriException: + return SatoriAsyncResult.new(result) + return SatoriAsyncResult.new() + + # Updates a message for an identity. + func update_message_async( + p_session : SatoriSession + , p_id : String + , p_body : + ) -> SatoriAsyncResult: + var try_refresh = await _refresh_session(p_session) + if try_refresh != null: + if try_refresh.is_exception(): + return SatoriAsyncResult.new(try_refresh.get_exception()) + await p_session.refresh(try_refresh) + var urlpath : String = "/v1/message/{id}" + urlpath = urlpath.replace("{id}", SatoriSerializer.escape_http(p_id)) + var query_params = "" + var uri = "%s%s%s" % [_base_uri, urlpath, "?" + query_params if query_params else ""] + var method = "PUT" + var headers = {} + var header = "Bearer %s" % p_session.token + headers["Authorization"] = header + + var content : PackedByteArray = PackedByteArray() + content = JSON.stringify(p_body.serialize()).to_utf8_buffer() + + var result = await _http_adapter.send_async(method, uri, headers, content) + if result is SatoriException: + return SatoriAsyncResult.new(result) + return SatoriAsyncResult.new() + + # List properties associated with this identity. + func list_properties_async( + p_session : SatoriSession + ) -> ApiProperties: + var try_refresh = await _refresh_session(p_session) + if try_refresh != null: + if try_refresh.is_exception(): + return ApiProperties.new(try_refresh.get_exception()) + await p_session.refresh(try_refresh) + var urlpath : String = "/v1/properties" + var query_params = "" + var uri = "%s%s%s" % [_base_uri, urlpath, "?" + query_params if query_params else ""] + var method = "GET" + var headers = {} + var header = "Bearer %s" % p_session.token + headers["Authorization"] = header + + var content : PackedByteArray = PackedByteArray() + + var result = await _http_adapter.send_async(method, uri, headers, content) + if result is SatoriException: + return ApiProperties.new(result) + var out : ApiProperties = SatoriSerializer.deserialize(_namespace, "ApiProperties", result) + return out + + # Update identity properties. + func update_properties_async( + p_session : SatoriSession + , p_body : ApiUpdatePropertiesRequest + ) -> SatoriAsyncResult: + var try_refresh = await _refresh_session(p_session) + if try_refresh != null: + if try_refresh.is_exception(): + return SatoriAsyncResult.new(try_refresh.get_exception()) + await p_session.refresh(try_refresh) + var urlpath : String = "/v1/properties" + var query_params = "" + var uri = "%s%s%s" % [_base_uri, urlpath, "?" + query_params if query_params else ""] + var method = "PUT" + var headers = {} + var header = "Bearer %s" % p_session.token + headers["Authorization"] = header + + var content : PackedByteArray = PackedByteArray() + content = JSON.stringify(p_body.serialize()).to_utf8_buffer() + + var result = await _http_adapter.send_async(method, uri, headers, content) + if result is SatoriException: + return SatoriAsyncResult.new(result) + return SatoriAsyncResult.new() diff --git a/addons/com.heroiclabs.nakama/Satori/SatoriAPI.gd.uid b/addons/com.heroiclabs.nakama/Satori/SatoriAPI.gd.uid new file mode 100644 index 0000000..1423ab1 --- /dev/null +++ b/addons/com.heroiclabs.nakama/Satori/SatoriAPI.gd.uid @@ -0,0 +1 @@ +uid://bvfs02d0j8q7g diff --git a/addons/com.heroiclabs.nakama/Satori/SatoriClient.gd b/addons/com.heroiclabs.nakama/Satori/SatoriClient.gd new file mode 100644 index 0000000..f67f8d7 --- /dev/null +++ b/addons/com.heroiclabs.nakama/Satori/SatoriClient.gd @@ -0,0 +1,212 @@ +extends RefCounted + +## A client for the API in Satori Server. +class_name SatoriClient + +#region Properties + +var _host +## The host address of the server. +var host : String: + get: + return _host + +var _port +## The port number of the server. +var port : int: + get: + return _port + +var _scheme +## The protocol scheme used to connect with the server. Must be either "http" or "https". +var scheme : String: + get: + return _scheme + +## The key used to authenticate with the server without a session. +var api_key : String + +## Set the timeout in seconds on requests sent to the server. +var timeout : int + +var _api_client : SatoriAPI.ApiClient + +var auto_refresh : bool = false: + set(v): + set_auto_refresh(v) + get: + return get_auto_refresh() + +func get_auto_refresh(): + return _api_client.auto_refresh + +func set_auto_refresh(p_value): + _api_client.auto_refresh = p_value + +#endregion + +#region Initialization + +func _init(p_adapter : SatoriHTTPAdapter, + p_api_key : String, + p_scheme : String, + p_host : String, + p_port : int, + p_timeout : int): + + api_key = p_api_key + _scheme = p_scheme + _host = p_host + _port = p_port + timeout = p_timeout + _api_client = SatoriAPI.ApiClient.new(_scheme + "://" + _host + ":" + str(_port), p_adapter, SatoriAPI, api_key, p_timeout) + +#endregion + +#region Client APIs + +## Authenticate against the server. +## [p_id]: An optional user id. +## [p_default_properties]: Optional default properties to update with this call. +## If not set, properties are left as they are on the server. +## [p_custom_properties]: Optional custom properties to update with this call. +## If not set, properties are left as they are on the server. +func authenticate_async(p_id: String, p_default_properties: Dictionary = {}, p_custom_properties: Dictionary = {}) -> SatoriSession: + return _parse_session(await _api_client.authenticate_async(api_key, "", + SatoriAPI.ApiAuthenticateRequest.create(SatoriAPI, { + "id": p_id, + "default": p_default_properties, + "custom": p_custom_properties + }))) + +## Log out a session, invalidate a refresh token, or log out all sessions/refresh tokens for a user. +## [p_session]: The session of the user. +func authenticate_logout_async(p_session: SatoriSession) -> SatoriAsyncResult: + return await _api_client.authenticate_logout_async(p_session, + SatoriAPI.ApiAuthenticateLogoutRequest.create(SatoriAPI, { + "refresh_token": p_session.refresh_token, + "token": p_session.token + })) + +# Parses the Satori API session and returns a SatoriSession object. +func _parse_session(p_session: SatoriAPI.ApiSession) -> SatoriSession: + if p_session.is_exception(): + return SatoriSession.new(null, null, p_session.get_exception()) + + return SatoriSession.new(p_session.token, p_session.refresh_token) + +## Refresh a user's session using a refresh token retrieved from a previous authentication request. +## [p_sesison]: The session of the user. +func session_refresh_async(p_session : SatoriSession) -> SatoriSession: + return _parse_session(await _api_client.authenticate_refresh_async(api_key, "", + SatoriAPI.ApiAuthenticateRefreshRequest.create(SatoriAPI, { + "refresh_token": p_session.refresh_token, + }) + )) + +## Send an event for this session. +## [p_session]: The session of the user. +## [p_event]: The event which will be sent. +func event_async(p_session: SatoriSession, p_event: Event) -> SatoriAsyncResult: + return await events_async(p_session, [ + p_event + ]) + +## Send a batch of events for this session. +## [p_session]: The session of the user. +## [p_events]: The batch of events which will be sent. +func events_async(p_session: SatoriSession, p_events: Array) -> SatoriAsyncResult: + var p_dict = { + "events": p_events.map(func(e): + return e.to_api_event_dict()) + } + + var req = SatoriAPI.ApiEventRequest.create(SatoriAPI, p_dict) + return await _api_client.event_async(p_session, + req) + +## Get all experiments data. +## [p_session]: The session of the user. +func get_all_experiments_async(p_session: SatoriSession) -> SatoriAsyncResult: + return await _api_client.get_experiments_async(p_session) + +## Get specific experiments data. +## [p_session]: The session of the user. +## [p_names]: Experiment names. +func get_experiments_async(p_session: SatoriSession, p_names: Array) -> SatoriAPI.ApiExperimentList: + return await _api_client.get_experiments_async(p_session, p_names) + +## Get a single flag for this identity. +## This method will return the default value +## specified and will not raise an exception if the network is unavailable +## [p_session]: The session of the user. +## [p_name]: The name of the flag. +## [p_default]: The default value if the server is unreachable. +func get_flag_async(p_session: SatoriSession, p_name: String, p_default: String = "") -> SatoriAPI.ApiFlag: + var p_names = [p_name] + var flags = await get_flags_async(p_session, p_names) + + if flags.is_exception(): + return SatoriAPI.ApiFlag.create(SatoriAPI, { + "name": p_name, + "value": p_default + }) + + for flag in flags.flags: + if flag.name == p_name: + return flag + + return null + +## List all available flags for this identity. +## [p_session]: The session of the user. +## [p_names]: Flag names, if empty all flags will be returned. +func get_flags_async(p_session: SatoriSession, p_names: Array) -> SatoriAPI.ApiFlagList: + return await _api_client.get_flags_async(p_session.token, p_names) + +## List available live events. +## [p_session]: The session of the user. +## [p_names]: Live event names, if null or empty all live events are returned. +func get_live_events_async(p_session: SatoriSession, p_names: Array = []) -> SatoriAPI.ApiLiveEventList: + return await _api_client.get_live_events_async(p_session, p_names) + +## Identify a session with a new ID. +## [p_session]: The session of the user. +## [p_id]: Identity ID to enrich the current session and return a new session. +## The old session will no longer be usable. +## Must be between eight and 128 characters (inclusive). +## Must be an alphanumeric string with only underscores and hyphens allowed. +## [p_default_properties]: The default properties. +## [p_custom_properties]: The custom event properties. +func identify_async(p_session: SatoriSession, p_id: String, p_default_properties: Dictionary = {}, p_custom_properties: Dictionary = {}) -> SatoriSession: + var req = SatoriAPI.ApiIdentifyRequest.create(SatoriAPI, { + "id": p_id, + "default": p_default_properties, + "custom": p_custom_properties + }) + return _parse_session(await _api_client.identify_async(p_session, req)) + +## List properties associated with this identity. +## [p_session]: The session of the user. +func list_properties_async(p_session: SatoriSession) -> SatoriAsyncResult: + return await _api_client.list_properties_async(p_session) + +## Update properties associated with this identity. +## [p_session]: The session of the user. +## [p_default_properties]: The default properties to update. +## [p_custom_properties]: The custom properties to update. +## [p_recompute]: Whether or not to recompute the user's audience membership immediately after property update. +func update_properties_async(p_session: SatoriSession, p_default_properties: Dictionary, p_custom_properties: Dictionary, p_recompute: bool = false) -> SatoriAsyncResult: + var req = SatoriAPI.ApiUpdatePropertiesRequest.create(SatoriAPI, { + "default": p_default_properties, + "custom": p_custom_properties, + "recompute": p_recompute + }) + return await _api_client.update_properties_async(p_session, req) + +## Delete the caller's identity and associated data. +## [p_session]: The session of the user. +func delete_identity_async(p_session: SatoriSession) -> SatoriAsyncResult: + return await _api_client.delete_identity_async(p_session) + +#endregion diff --git a/addons/com.heroiclabs.nakama/Satori/SatoriClient.gd.uid b/addons/com.heroiclabs.nakama/Satori/SatoriClient.gd.uid new file mode 100644 index 0000000..b6fadf4 --- /dev/null +++ b/addons/com.heroiclabs.nakama/Satori/SatoriClient.gd.uid @@ -0,0 +1 @@ +uid://cd25d8rmdqk2c diff --git a/addons/com.heroiclabs.nakama/Satori/SatoriHttpAdapter.gd b/addons/com.heroiclabs.nakama/Satori/SatoriHttpAdapter.gd new file mode 100644 index 0000000..9aac03e --- /dev/null +++ b/addons/com.heroiclabs.nakama/Satori/SatoriHttpAdapter.gd @@ -0,0 +1,208 @@ +@tool +extends Node + +# An adapter which implements the HTTP protocol. +class_name SatoriHTTPAdapter + +# The logger to use with the adapter. +var logger : RefCounted = SatoriLogger.new() + +# The timeout for requests +var timeout : int = 3 +# If request should be automatically retried when a network error occurs. +var auto_retry : bool = true +# The maximum number of time a request will be retried when auto_retry is true +var auto_retry_count : int = 3 +var auto_retry_backoff_base : int = 10 +# Whether or not to use threads when making HTTP requests. +var use_threads : bool = true + +var _pending = {} +var id : int = 0 + +class AsyncRequest: + var id : int + var request : HTTPRequest + var uri : String + var method : int + var headers : PackedStringArray + var body : PackedByteArray + var retry_count := 3 + var backoff_time := 10 + var logger : SatoriLogger + + var cancelled = false + var result : int = HTTPRequest.RESULT_NO_RESPONSE + var response_code : int = -1 + var response_body : PackedByteArray + var timer : SceneTreeTimer = null + var cur_try : int = 1 + var rng = RandomNumberGenerator.new() + + func _init(p_id : int, p_request : HTTPRequest, p_uri : String, + p_method : int, p_headers : PackedStringArray, p_body : PackedByteArray, + p_retry_count : int, p_backoff_time : int, p_logger : SatoriLogger): + rng.seed = Time.get_ticks_usec() + id = p_id + request = p_request + uri = p_uri + method = p_method + headers = p_headers + body = p_body + retry_count = p_retry_count + backoff_time = p_backoff_time + logger = p_logger + + func should_retry(): + return cur_try < retry_count and not cancelled + + func retry(): + var time = pow(backoff_time, cur_try) * rng.randf_range(0.5, 1) + logger.debug("Retrying request %d. Tries left: %d. Backoff: %d ms" % [ + id, retry_count - cur_try, time + ]) + cur_try += 1 + await backoff(time) + if cancelled: + return + return await make_request() + + func make_request(): + var err = request.request(uri, headers, method, body.get_string_from_utf8()) + if err != OK: + await request.get_tree().process_frame + result = HTTPRequest.RESULT_CANT_CONNECT + logger.debug("Request %d failed to start, error: %d" % [id, err]) + return + + var args = await request.request_completed + result = args[0] + response_code = args[1] + response_body = args[3] + + func backoff(p_time : int): + timer = request.get_tree().create_timer(p_time / 1000.0) + await timer.timeout + timer = null + + func cancel(): + cancelled = true + request.cancel_request() + if timer: + timer.time_left = 0 + else: + request.call_deferred("emit_signal", "request_completed", HTTPRequest.RESULT_REQUEST_FAILED, 0, [], []) + + func parse_result(): + if cancelled: + return SatoriException.new("Request cancelled", -1, -1, true) + elif result != HTTPRequest.RESULT_SUCCESS: + if result == null: + result = 0 + return SatoriException.new("HTTPRequest failed!", result) + + var json = JSON.new() + + var json_error = json.parse(response_body.get_string_from_utf8()) + if json_error != OK: + logger.debug("Unable to parse request %d response. JSON error: %d, JSON error message: %s, response code: %d" % [ + id, json_error, json.get_error_message(), response_code + ]) + return SatoriException.new("Failed to decode JSON response", response_code) + + var parsed = json.get_data() + + if response_code != HTTPClient.RESPONSE_OK: + var error = "" + var code = -1 + if typeof(parsed) == TYPE_DICTIONARY: + if "message" in parsed: + error = parsed["message"] + elif "error" in parsed: + error = parsed["error"] + else: + error = str(parsed) + code = parsed["code"] if "code" in parsed else -1 + else: + error = str(parsed) + if typeof(error) == TYPE_DICTIONARY: + error = JSON.stringify(error) + logger.debug("Request %d returned response code: %d, RPC code: %d, error: %s" % [ + id, response_code, code, error + ]) + return SatoriException.new(error, response_code, code) + + return parsed + + +# Send a HTTP request. +# @param method - HTTP method to use for this request. +# @param uri - The fully qualified URI to use. +# @param headers - Request headers to set. +# @param body - Request content body to set. +# @param timeoutSec - Request timeout. +# Returns a task which resolves to the contents of the response. +func send_async(p_method : String, p_uri : String, p_headers : Dictionary, p_body : PackedByteArray): + var req = HTTPRequest.new() + req.timeout = timeout + if use_threads and OS.get_name() != 'Web': + req.use_threads = true # Threads not available nor needed on the web. + + # Parse method + var method = HTTPClient.METHOD_GET + if p_method == "POST": + method = HTTPClient.METHOD_POST + elif p_method == "PUT": + method = HTTPClient.METHOD_PUT + elif p_method == "DELETE": + method = HTTPClient.METHOD_DELETE + elif p_method == "HEAD": + method = HTTPClient.METHOD_HEAD + var headers = PackedStringArray() + + # Parse headers + headers.append("Accept: application/json") + for k in p_headers: + headers.append("%s: %s" % [k, p_headers[k]]) + + id += 1 + var retry = auto_retry_count if auto_retry else 0 + var backoff = auto_retry_backoff_base + _pending[id] = AsyncRequest.new(id, req, p_uri, method, headers, p_body, retry, backoff, logger) + + logger.debug("Sending request [ID: %d, Method: %s, Uri: %s, Headers: %s, Body: %s, Timeout: %d, Retries: %d, Backoff base: %d ms]" % [ + id, p_method, p_uri, p_headers, p_body.get_string_from_utf8(), timeout, retry, backoff + ]) + + add_child(req) + + return await _send_async(id, _pending) + +func get_last_token(): + return id + +func cancel_request(p_token): + if _pending.has(p_token): + _pending[p_token].cancel() + +static func _clear_request(p_request : AsyncRequest, p_pending : Dictionary, p_id : int): + if not p_request.request.is_queued_for_deletion(): + p_request.logger.debug("Freeing request %d" % p_id) + p_request.request.queue_free() + p_pending.erase(p_id) + +static func _send_async(p_id : int, p_pending : Dictionary): + + var req : AsyncRequest = p_pending[p_id] + await req.make_request() + + while req.result != HTTPRequest.RESULT_SUCCESS: + req.logger.debug("Request %d failed with result: %d, response code: %d" % [ + p_id, req.result, req.response_code + ]) + if not req.should_retry(): + break + await req.retry() + + _clear_request(req, p_pending, p_id) + return req.parse_result() diff --git a/addons/com.heroiclabs.nakama/Satori/SatoriHttpAdapter.gd.uid b/addons/com.heroiclabs.nakama/Satori/SatoriHttpAdapter.gd.uid new file mode 100644 index 0000000..d6837f8 --- /dev/null +++ b/addons/com.heroiclabs.nakama/Satori/SatoriHttpAdapter.gd.uid @@ -0,0 +1 @@ +uid://d4cdv6h3e50q3 diff --git a/addons/com.heroiclabs.nakama/Satori/SatoriSession.gd b/addons/com.heroiclabs.nakama/Satori/SatoriSession.gd new file mode 100644 index 0000000..09052c4 --- /dev/null +++ b/addons/com.heroiclabs.nakama/Satori/SatoriSession.gd @@ -0,0 +1,107 @@ +extends SatoriAsyncResult +class_name SatoriSession + +var _token: String = "" +var token: String: + get: + return _token + +var _refresh_token: String = "" +var refresh_token: String: + get: + return _refresh_token + +var _expire_time: int = 0 +var expire_time: int: + get: + return _expire_time + +var expired: bool: + get: + return is_expired() + +var _refresh_expire_time: int = 0 +var refresh_expire_time: int: + get: + return _refresh_expire_time + +var _identity_id: String = "" +var identity_id: String: + get: + return _identity_id + +var _valid : bool = false +var valid : bool: + get: + return _valid + +func is_expired() -> bool: + return _expire_time < Time.get_unix_time_from_system() + +func would_expire_in(p_secs : int) -> bool: + return _expire_time < Time.get_unix_time_from_system() + p_secs + +func has_refresh_expired(offset: float) -> bool: + return _expire_time < offset + +func is_refresh_expired() -> bool: + return _refresh_expire_time < Time.get_unix_time_from_system() + +func is_valid(): + return _valid + +# Initializes a new instance of the SatoriSession class. +# +# @param p_token - The authentication token. +# @param p_refresh_token - The refresh token. +# @param p_exception - The exception to be thrown, if any. +func _init(p_token = null, p_refresh_token = null, p_exception = null): + super(p_exception) + + _refresh_expire_time = 0 + if p_token: + _update(p_token, p_refresh_token) + +func _update(p_token, p_refresh_token): + _token = p_token + _refresh_token = p_refresh_token + + var decoded = _jwt_unpack(p_token) + if decoded.is_empty(): + _valid = false + return + + _valid = true + _expire_time = int(decoded.get("exp", 0)) + _identity_id = str(decoded.get("iid", "")) + _refresh_expire_time = int(_jwt_unpack(refresh_token).get("exp", 0)) if !refresh_token.is_empty() else 0 + +func _to_string(): + if is_exception(): + return get_exception()._to_string() + + return "Session" % [ + _token, _expire_time, _refresh_token, _refresh_expire_time, identity_id] + +func _jwt_unpack(p_token : String) -> Dictionary: + # Hack decode JSON payload from JWT. + if p_token.find(".") == -1: + _ex = SatoriException.new("Missing payload: %s" % p_token) + return {} + var payload = p_token.split('.')[1]; + var pad_length = ceil(payload.length() / 4.0) * 4; + # Pad base64 + for i in range(0, pad_length - payload.length()): + payload += "=" + payload = payload.replace("-", "+").replace("_", "/") + var unpacked = Marshalls.base64_to_utf8(payload) + + var json = JSON.new() + var error = json.parse(unpacked) + + if error == OK: + var decoded = json.get_data() + if typeof(decoded) == TYPE_DICTIONARY: + return decoded + _ex = SatoriException.new("Unable to unpack token: %s" % p_token) + return {} \ No newline at end of file diff --git a/addons/com.heroiclabs.nakama/Satori/SatoriSession.gd.uid b/addons/com.heroiclabs.nakama/Satori/SatoriSession.gd.uid new file mode 100644 index 0000000..08dc402 --- /dev/null +++ b/addons/com.heroiclabs.nakama/Satori/SatoriSession.gd.uid @@ -0,0 +1 @@ +uid://btjsbd35ljish diff --git a/addons/com.heroiclabs.nakama/Satori/utils/SatoriAsyncResult.gd b/addons/com.heroiclabs.nakama/Satori/utils/SatoriAsyncResult.gd new file mode 100644 index 0000000..bf43a90 --- /dev/null +++ b/addons/com.heroiclabs.nakama/Satori/utils/SatoriAsyncResult.gd @@ -0,0 +1,34 @@ +extends RefCounted +class_name SatoriAsyncResult + +var exception : SatoriException: + set(v): + pass + get: + return get_exception() + +var _ex = null + +func _init(p_ex = null): + _ex = p_ex + +func is_exception(): + return get_exception() != null + +func was_cancelled(): + return is_exception() and get_exception().cancelled + +func get_exception() -> SatoriException: + return _ex as SatoriException + +func _to_string(): + if is_exception(): + return get_exception()._to_string() + return "SatoriAsyncResult<>" + +static func _safe_ret(p_obj, p_type : GDScript): + if is_instance_of(p_obj, p_type): + return p_obj + elif p_obj is SatoriException: + return p_type.new(p_obj) + return p_type.new(SatoriException.new()) diff --git a/addons/com.heroiclabs.nakama/Satori/utils/SatoriAsyncResult.gd.uid b/addons/com.heroiclabs.nakama/Satori/utils/SatoriAsyncResult.gd.uid new file mode 100644 index 0000000..8e362a9 --- /dev/null +++ b/addons/com.heroiclabs.nakama/Satori/utils/SatoriAsyncResult.gd.uid @@ -0,0 +1 @@ +uid://7fruq6sqrt8x diff --git a/addons/com.heroiclabs.nakama/Satori/utils/SatoriException.gd b/addons/com.heroiclabs.nakama/Satori/utils/SatoriException.gd new file mode 100644 index 0000000..ed5452a --- /dev/null +++ b/addons/com.heroiclabs.nakama/Satori/utils/SatoriException.gd @@ -0,0 +1,42 @@ +extends RefCounted + +# An exception generated during a request. +# Usually contains at least an error message. +class_name SatoriException + +var _status_code : int = -1 +var status_code : int: + set(v): + pass + get: + return _status_code + +var _grpc_status_code : int = -1 +var grpc_status_code : int: + set(v): + pass + get: + return _grpc_status_code + +var _message : String = "" +var message : String: + set(v): + pass + get: + return _message + +var _cancelled : bool = false +var cancelled : bool: + set(v): + pass + get: + return _cancelled + +func _init(p_message : String = "", p_status_code : int = -1, p_grpc_status_code : int = -1, p_cancelled : bool = false): + _status_code = p_status_code + _grpc_status_code = p_grpc_status_code + _message = p_message + _cancelled = p_cancelled + +func _to_string() -> String: + return "SatoriException(StatusCode={%s}, Message='{%s}', GrpcStatusCode={%s})" % [_status_code, _message, _grpc_status_code] diff --git a/addons/com.heroiclabs.nakama/Satori/utils/SatoriException.gd.uid b/addons/com.heroiclabs.nakama/Satori/utils/SatoriException.gd.uid new file mode 100644 index 0000000..fb6fe63 --- /dev/null +++ b/addons/com.heroiclabs.nakama/Satori/utils/SatoriException.gd.uid @@ -0,0 +1 @@ +uid://byri0ssrp022t diff --git a/addons/com.heroiclabs.nakama/Satori/utils/SatoriLogger.gd b/addons/com.heroiclabs.nakama/Satori/utils/SatoriLogger.gd new file mode 100644 index 0000000..172ea07 --- /dev/null +++ b/addons/com.heroiclabs.nakama/Satori/utils/SatoriLogger.gd @@ -0,0 +1,38 @@ +extends RefCounted +class_name SatoriLogger + +enum LOG_LEVEL {NONE, ERROR, WARNING, INFO, VERBOSE, DEBUG} + +var _level = LOG_LEVEL.ERROR +var _module = "Satori" + +func _init(p_module : String = "Satori", p_level : int = LOG_LEVEL.ERROR): + _level = p_level + _module = p_module + +func _log(level : int, msg): + if level <= _level: + if level == LOG_LEVEL.ERROR: + printerr("=== %s : ERROR === %s" % [_module, str(msg)]) + else: + var what = "=== UNKNOWN === " + for k in LOG_LEVEL: + if level == LOG_LEVEL[k]: + what = "=== %s : %s === " % [_module, k] + break + print(what + str(msg)) + +func error(msg): + _log(LOG_LEVEL.ERROR, msg) + +func warning(msg): + _log(LOG_LEVEL.WARNING, msg) + +func info(msg): + _log(LOG_LEVEL.INFO, msg) + +func verbose(msg): + _log(LOG_LEVEL.VERBOSE, msg) + +func debug(msg): + _log(LOG_LEVEL.DEBUG, msg) diff --git a/addons/com.heroiclabs.nakama/Satori/utils/SatoriLogger.gd.uid b/addons/com.heroiclabs.nakama/Satori/utils/SatoriLogger.gd.uid new file mode 100644 index 0000000..9fb19d3 --- /dev/null +++ b/addons/com.heroiclabs.nakama/Satori/utils/SatoriLogger.gd.uid @@ -0,0 +1 @@ +uid://cmslry0dhyhhc diff --git a/addons/com.heroiclabs.nakama/Satori/utils/SatoriSerializer.gd b/addons/com.heroiclabs.nakama/Satori/utils/SatoriSerializer.gd new file mode 100644 index 0000000..fa28fba --- /dev/null +++ b/addons/com.heroiclabs.nakama/Satori/utils/SatoriSerializer.gd @@ -0,0 +1,163 @@ +extends RefCounted +class_name SatoriSerializer + +static func serialize(p_obj : Object) -> Dictionary: + var out = {} + var schema = p_obj.get("_SCHEMA") + if schema == null: + return {} # No schema defined + for k in schema: + var prop = schema[k] + var val = p_obj.get(prop["name"]) + if val == null: + continue + var type = prop["type"] + var content = prop.get("content", TYPE_NIL) + if typeof(content) == TYPE_STRING: + content = TYPE_OBJECT + var val_type = typeof(val) + match val_type: + TYPE_OBJECT: # Simple objects + out[k] = serialize(val) + TYPE_ARRAY: # Array of objects + var arr = [] + if val.size() > 0 and typeof(val[0]) == TYPE_OBJECT: # Array of objects + for e in val: + arr.append(serialize(e)) + else: + arr = val + out[k] = arr + TYPE_PACKED_INT32_ARRAY, TYPE_PACKED_STRING_ARRAY: # Array of ints, bools, or strings + var arr = [] + for e in val: + if content == TYPE_BOOL: + e = bool(e) + if typeof(e) != content: + continue + arr.append(e) + out[k] = arr + TYPE_DICTIONARY: # Maps + var dict = {} + if content == TYPE_OBJECT: # Map of objects + for l in val: + if typeof(val[l]) != TYPE_OBJECT: + continue + dict[l] = serialize(val[l]) + else: # Map of simple types + for l in val: + var e = val[l] + if content == TYPE_FLOAT: + e = float(e) + elif content == TYPE_INT: + e = int(e) + elif content == TYPE_BOOL: + e = bool(e) + if typeof(e) != content: + continue + dict[l] = e + out[k] = dict + _: + out[k] = val + return out + +static func deserialize(p_ns : GDScript, p_cls_name : String, p_dict : Dictionary) -> Object: + var cls : GDScript = p_ns.get(p_cls_name) + var schema = cls.get("_SCHEMA") + if schema == null: + return SatoriException.new() # No schema defined + var obj = cls.new() + for k in schema: + var prop = schema[k] + var pname = prop["name"] + var type = prop["type"] + var required = prop["required"] + var content = prop.get("content", TYPE_NIL) + var type_cmp = type + if typeof(type) == TYPE_STRING: # A class + type_cmp = TYPE_DICTIONARY + if type_cmp == TYPE_PACKED_STRING_ARRAY or type_cmp == TYPE_PACKED_INT32_ARRAY: # A specialized array + type_cmp = TYPE_ARRAY + + var content_cmp = content + if typeof(content) == TYPE_STRING: # A dictionary or array of classes + content_cmp = TYPE_DICTIONARY + + var val = p_dict.get(k, null) + + # Ints might and up being recognized as floats. Change that if needed + if type_cmp == TYPE_INT: + if typeof(val) == TYPE_FLOAT: + val = int(val) + elif typeof(val) == TYPE_STRING and val.is_valid_int(): + val = val.to_int() + + if typeof(val) == type_cmp: + if typeof(type) == TYPE_STRING: + obj.set(pname, deserialize(p_ns, type, val)) + elif type_cmp == TYPE_DICTIONARY: + var v = {} + for l in val: + if typeof(content) == TYPE_STRING: + v[l] = deserialize(p_ns, content, val[l]) + elif content == TYPE_FLOAT: + v[l] = float(val[l]) + elif content == TYPE_INT: + v[l] = int(val[l]) + elif content == TYPE_BOOL: + v[l] = bool(val[l]) + else: + v[l] = str(val[l]) + obj.set(pname, v) + elif type_cmp == TYPE_ARRAY: + var v + match content: + TYPE_INT, TYPE_BOOL: v = PackedInt32Array() + TYPE_STRING: v = PackedStringArray() + _: v = Array() + for e in val: + if typeof(e) == TYPE_DICTIONARY: + v.append(e) # Avoid deserialization if e is already a dictionary + elif typeof(content) == TYPE_STRING: + v.append(deserialize(p_ns, content, e)) + elif content == TYPE_FLOAT: + v.append(float(e)) + elif content == TYPE_INT: + v.append(int(e)) + elif content == TYPE_BOOL: + v.append(bool(e)) + else: + v.append(str(e)) + obj.set(pname, v) + else: + obj.set(pname, val) + elif required: + obj._ex = SatoriException.new("ERROR [%s]: Missing or invalid required prop %s = %s:\n\t%s" % [p_cls_name, prop, p_dict.get(k), p_dict]) + return obj + return obj + + +### +# Compatibility with Godot 3.1 which does not expose String.http_escape +### +const HEX = ["0","1","2","3","4","5","6","7","8","9","A","B","C","D","E","F"] + +static func escape_http(p_str : String) -> String: + var out : String = "" + for o in p_str: + if (o == '.' or o == '-' or o == '_' or o == '~' or + (o >= 'a' and o <= 'z') or + (o >= 'A' and o <= 'Z') or + (o >= '0' and o <= '9')): + out += o + else: + for b in o.to_utf8_buffer(): + out += "%%%s" % to_hex(b) + return out + +static func to_hex(p_val : int) -> String: + var v := p_val + var o := "" + while v != 0: + o = HEX[v % 16] + o + v /= 16 + return o diff --git a/addons/com.heroiclabs.nakama/Satori/utils/SatoriSerializer.gd.uid b/addons/com.heroiclabs.nakama/Satori/utils/SatoriSerializer.gd.uid new file mode 100644 index 0000000..a9ed867 --- /dev/null +++ b/addons/com.heroiclabs.nakama/Satori/utils/SatoriSerializer.gd.uid @@ -0,0 +1 @@ +uid://cg7wctbblbvn7 diff --git a/addons/com.heroiclabs.nakama/api/NakamaAPI.gd b/addons/com.heroiclabs.nakama/api/NakamaAPI.gd new file mode 100644 index 0000000..fc680bd --- /dev/null +++ b/addons/com.heroiclabs.nakama/api/NakamaAPI.gd @@ -0,0 +1,6120 @@ +### Code generated by codegen/main.go. DO NOT EDIT. ### + +extends RefCounted +class_name NakamaAPI + +# A single user-role pair. +class GroupUserListGroupUser extends NakamaAsyncResult: + + const _SCHEMA = { + "state": {"name": "_state", "type": TYPE_INT, "required": false}, + "user": {"name": "_user", "type": "ApiUser", "required": false}, + } + + # Their relationship to the group. + var _state + var state : int: + get: + return 0 if not _state is int else int(_state) + + # User. + var _user + var user : ApiUser: + get: + return _user as ApiUser + + func _init(p_exception = null): + super(p_exception) + + static func create(p_ns : GDScript, p_dict : Dictionary) -> GroupUserListGroupUser: + return _safe_ret(NakamaSerializer.deserialize(p_ns, "GroupUserListGroupUser", p_dict), GroupUserListGroupUser) as GroupUserListGroupUser + + func serialize() -> Dictionary: + return NakamaSerializer.serialize(self) + + func _to_string() -> String: + if is_exception(): + return get_exception()._to_string() + var output : String = "" + output += "state: %s, " % _state + output += "user: %s, " % _user + return output + +# A single group-role pair. +class UserGroupListUserGroup extends NakamaAsyncResult: + + const _SCHEMA = { + "group": {"name": "_group", "type": "ApiGroup", "required": false}, + "state": {"name": "_state", "type": TYPE_INT, "required": false}, + } + + # Group. + var _group + var group : ApiGroup: + get: + return _group as ApiGroup + + # The user's relationship to the group. + var _state + var state : int: + get: + return 0 if not _state is int else int(_state) + + func _init(p_exception = null): + super(p_exception) + + static func create(p_ns : GDScript, p_dict : Dictionary) -> UserGroupListUserGroup: + return _safe_ret(NakamaSerializer.deserialize(p_ns, "UserGroupListUserGroup", p_dict), UserGroupListUserGroup) as UserGroupListUserGroup + + func serialize() -> Dictionary: + return NakamaSerializer.serialize(self) + + func _to_string() -> String: + if is_exception(): + return get_exception()._to_string() + var output : String = "" + output += "group: %s, " % _group + output += "state: %s, " % _state + return output + +# Record values to write. +class WriteLeaderboardRecordRequestLeaderboardRecordWrite extends NakamaAsyncResult: + + const _SCHEMA = { + "metadata": {"name": "_metadata", "type": TYPE_STRING, "required": false}, + "operator": {"name": "_operator", "type": TYPE_INT, "required": false}, + "score": {"name": "_score", "type": TYPE_STRING, "required": false}, + "subscore": {"name": "_subscore", "type": TYPE_STRING, "required": false}, + } + + # Optional record metadata. + var _metadata + var metadata : String: + get: + return "" if not _metadata is String else String(_metadata) + + # Operator override. + var _operator + var operator : int: + get: + return ApiOperator.values()[0] if not ApiOperator.values().has(_operator) else _operator + + # The score value to submit. + var _score + var score : String: + get: + return "" if not _score is String else String(_score) + + # An optional secondary value. + var _subscore + var subscore : String: + get: + return "" if not _subscore is String else String(_subscore) + + func _init(p_exception = null): + super(p_exception) + + static func create(p_ns : GDScript, p_dict : Dictionary) -> WriteLeaderboardRecordRequestLeaderboardRecordWrite: + return _safe_ret(NakamaSerializer.deserialize(p_ns, "WriteLeaderboardRecordRequestLeaderboardRecordWrite", p_dict), WriteLeaderboardRecordRequestLeaderboardRecordWrite) as WriteLeaderboardRecordRequestLeaderboardRecordWrite + + func serialize() -> Dictionary: + return NakamaSerializer.serialize(self) + + func _to_string() -> String: + if is_exception(): + return get_exception()._to_string() + var output : String = "" + output += "metadata: %s, " % _metadata + output += "operator: %s, " % _operator + output += "score: %s, " % _score + output += "subscore: %s, " % _subscore + return output + +# Record values to write. +class WriteTournamentRecordRequestTournamentRecordWrite extends NakamaAsyncResult: + + const _SCHEMA = { + "metadata": {"name": "_metadata", "type": TYPE_STRING, "required": false}, + "operator": {"name": "_operator", "type": TYPE_INT, "required": false}, + "score": {"name": "_score", "type": TYPE_STRING, "required": false}, + "subscore": {"name": "_subscore", "type": TYPE_STRING, "required": false}, + } + + # A JSON object of additional properties (optional). + var _metadata + var metadata : String: + get: + return "" if not _metadata is String else String(_metadata) + + # Operator override. + var _operator + var operator : int: + get: + return ApiOperator.values()[0] if not ApiOperator.values().has(_operator) else _operator + + # The score value to submit. + var _score + var score : String: + get: + return "" if not _score is String else String(_score) + + # An optional secondary value. + var _subscore + var subscore : String: + get: + return "" if not _subscore is String else String(_subscore) + + func _init(p_exception = null): + super(p_exception) + + static func create(p_ns : GDScript, p_dict : Dictionary) -> WriteTournamentRecordRequestTournamentRecordWrite: + return _safe_ret(NakamaSerializer.deserialize(p_ns, "WriteTournamentRecordRequestTournamentRecordWrite", p_dict), WriteTournamentRecordRequestTournamentRecordWrite) as WriteTournamentRecordRequestTournamentRecordWrite + + func serialize() -> Dictionary: + return NakamaSerializer.serialize(self) + + func _to_string() -> String: + if is_exception(): + return get_exception()._to_string() + var output : String = "" + output += "metadata: %s, " % _metadata + output += "operator: %s, " % _operator + output += "score: %s, " % _score + output += "subscore: %s, " % _subscore + return output + +# A user with additional account details. Always the current user. +class ApiAccount extends NakamaAsyncResult: + + const _SCHEMA = { + "custom_id": {"name": "_custom_id", "type": TYPE_STRING, "required": false}, + "devices": {"name": "_devices", "type": TYPE_ARRAY, "required": false, "content": "ApiAccountDevice"}, + "disable_time": {"name": "_disable_time", "type": TYPE_STRING, "required": false}, + "email": {"name": "_email", "type": TYPE_STRING, "required": false}, + "user": {"name": "_user", "type": "ApiUser", "required": false}, + "verify_time": {"name": "_verify_time", "type": TYPE_STRING, "required": false}, + "wallet": {"name": "_wallet", "type": TYPE_STRING, "required": false}, + } + + # The custom id in the user's account. + var _custom_id + var custom_id : String: + get: + return "" if not _custom_id is String else String(_custom_id) + + # The devices which belong to the user's account. + var _devices + var devices : Array: + get: + return Array() if not _devices is Array else Array(_devices) + + # The UNIX time when the user's account was disabled/banned. + var _disable_time + var disable_time : String: + get: + return "" if not _disable_time is String else String(_disable_time) + + # The email address of the user. + var _email + var email : String: + get: + return "" if not _email is String else String(_email) + + # The user object. + var _user + var user : ApiUser: + get: + return _user as ApiUser + + # The UNIX time when the user's email was verified. + var _verify_time + var verify_time : String: + get: + return "" if not _verify_time is String else String(_verify_time) + + # The user's wallet data. + var _wallet + var wallet : String: + get: + return "" if not _wallet is String else String(_wallet) + + var _wallet_dict = null + var wallet_dict : Dictionary: + get: + if _wallet_dict == null: + if _wallet == null: + return {} + var json = JSON.new() + if json.parse(_wallet) != OK: + return {} + _wallet_dict = json.get_data() + return _wallet_dict as Dictionary + + + func _init(p_exception = null): + super(p_exception) + + static func create(p_ns : GDScript, p_dict : Dictionary) -> ApiAccount: + return _safe_ret(NakamaSerializer.deserialize(p_ns, "ApiAccount", p_dict), ApiAccount) as ApiAccount + + func serialize() -> Dictionary: + return NakamaSerializer.serialize(self) + + func _to_string() -> String: + if is_exception(): + return get_exception()._to_string() + var output : String = "" + output += "custom_id: %s, " % _custom_id + output += "devices: %s, " % [_devices] + output += "disable_time: %s, " % _disable_time + output += "email: %s, " % _email + output += "user: %s, " % _user + output += "verify_time: %s, " % _verify_time + output += "wallet: %s, " % _wallet + return output + +# Send a Apple Sign In token to the server. Used with authenticate/link/unlink. +class ApiAccountApple extends NakamaAsyncResult: + + const _SCHEMA = { + "token": {"name": "_token", "type": TYPE_STRING, "required": false}, + "vars": {"name": "_vars", "type": TYPE_DICTIONARY, "required": false, "content": TYPE_STRING}, + } + + # The ID token received from Apple to validate. + var _token + var token : String: + get: + return "" if not _token is String else String(_token) + + # Extra information that will be bundled in the session token. + var _vars + var vars : Dictionary: + get: + return Dictionary() if not _vars is Dictionary else _vars.duplicate() + + func _init(p_exception = null): + super(p_exception) + + static func create(p_ns : GDScript, p_dict : Dictionary) -> ApiAccountApple: + return _safe_ret(NakamaSerializer.deserialize(p_ns, "ApiAccountApple", p_dict), ApiAccountApple) as ApiAccountApple + + func serialize() -> Dictionary: + return NakamaSerializer.serialize(self) + + func _to_string() -> String: + if is_exception(): + return get_exception()._to_string() + var output : String = "" + output += "token: %s, " % _token + var map_string : String = "" + if typeof(_vars) == TYPE_DICTIONARY: + for k in _vars: + map_string += "{%s=%s}, " % [k, _vars[k]] + output += "vars: [%s], " % map_string + return output + +# Send a custom ID to the server. Used with authenticate/link/unlink. +class ApiAccountCustom extends NakamaAsyncResult: + + const _SCHEMA = { + "id": {"name": "_id", "type": TYPE_STRING, "required": false}, + "vars": {"name": "_vars", "type": TYPE_DICTIONARY, "required": false, "content": TYPE_STRING}, + } + + # A custom identifier. + var _id + var id : String: + get: + return "" if not _id is String else String(_id) + + # Extra information that will be bundled in the session token. + var _vars + var vars : Dictionary: + get: + return Dictionary() if not _vars is Dictionary else _vars.duplicate() + + func _init(p_exception = null): + super(p_exception) + + static func create(p_ns : GDScript, p_dict : Dictionary) -> ApiAccountCustom: + return _safe_ret(NakamaSerializer.deserialize(p_ns, "ApiAccountCustom", p_dict), ApiAccountCustom) as ApiAccountCustom + + func serialize() -> Dictionary: + return NakamaSerializer.serialize(self) + + func _to_string() -> String: + if is_exception(): + return get_exception()._to_string() + var output : String = "" + output += "id: %s, " % _id + var map_string : String = "" + if typeof(_vars) == TYPE_DICTIONARY: + for k in _vars: + map_string += "{%s=%s}, " % [k, _vars[k]] + output += "vars: [%s], " % map_string + return output + +# Send a device to the server. Used with authenticate/link/unlink and user. +class ApiAccountDevice extends NakamaAsyncResult: + + const _SCHEMA = { + "id": {"name": "_id", "type": TYPE_STRING, "required": false}, + "vars": {"name": "_vars", "type": TYPE_DICTIONARY, "required": false, "content": TYPE_STRING}, + } + + # A device identifier. Should be obtained by a platform-specific device API. + var _id + var id : String: + get: + return "" if not _id is String else String(_id) + + # Extra information that will be bundled in the session token. + var _vars + var vars : Dictionary: + get: + return Dictionary() if not _vars is Dictionary else _vars.duplicate() + + func _init(p_exception = null): + super(p_exception) + + static func create(p_ns : GDScript, p_dict : Dictionary) -> ApiAccountDevice: + return _safe_ret(NakamaSerializer.deserialize(p_ns, "ApiAccountDevice", p_dict), ApiAccountDevice) as ApiAccountDevice + + func serialize() -> Dictionary: + return NakamaSerializer.serialize(self) + + func _to_string() -> String: + if is_exception(): + return get_exception()._to_string() + var output : String = "" + output += "id: %s, " % _id + var map_string : String = "" + if typeof(_vars) == TYPE_DICTIONARY: + for k in _vars: + map_string += "{%s=%s}, " % [k, _vars[k]] + output += "vars: [%s], " % map_string + return output + +# Send an email with password to the server. Used with authenticate/link/unlink. +class ApiAccountEmail extends NakamaAsyncResult: + + const _SCHEMA = { + "email": {"name": "_email", "type": TYPE_STRING, "required": false}, + "password": {"name": "_password", "type": TYPE_STRING, "required": false}, + "vars": {"name": "_vars", "type": TYPE_DICTIONARY, "required": false, "content": TYPE_STRING}, + } + + # A valid RFC-5322 email address. + var _email + var email : String: + get: + return "" if not _email is String else String(_email) + + # A password for the user account. + var _password + var password : String: + get: + return "" if not _password is String else String(_password) + + # Extra information that will be bundled in the session token. + var _vars + var vars : Dictionary: + get: + return Dictionary() if not _vars is Dictionary else _vars.duplicate() + + func _init(p_exception = null): + super(p_exception) + + static func create(p_ns : GDScript, p_dict : Dictionary) -> ApiAccountEmail: + return _safe_ret(NakamaSerializer.deserialize(p_ns, "ApiAccountEmail", p_dict), ApiAccountEmail) as ApiAccountEmail + + func serialize() -> Dictionary: + return NakamaSerializer.serialize(self) + + func _to_string() -> String: + if is_exception(): + return get_exception()._to_string() + var output : String = "" + output += "email: %s, " % _email + output += "password: %s, " % _password + var map_string : String = "" + if typeof(_vars) == TYPE_DICTIONARY: + for k in _vars: + map_string += "{%s=%s}, " % [k, _vars[k]] + output += "vars: [%s], " % map_string + return output + +# Send a Facebook token to the server. Used with authenticate/link/unlink. +class ApiAccountFacebook extends NakamaAsyncResult: + + const _SCHEMA = { + "token": {"name": "_token", "type": TYPE_STRING, "required": false}, + "vars": {"name": "_vars", "type": TYPE_DICTIONARY, "required": false, "content": TYPE_STRING}, + } + + # The OAuth token received from Facebook to access their profile API. + var _token + var token : String: + get: + return "" if not _token is String else String(_token) + + # Extra information that will be bundled in the session token. + var _vars + var vars : Dictionary: + get: + return Dictionary() if not _vars is Dictionary else _vars.duplicate() + + func _init(p_exception = null): + super(p_exception) + + static func create(p_ns : GDScript, p_dict : Dictionary) -> ApiAccountFacebook: + return _safe_ret(NakamaSerializer.deserialize(p_ns, "ApiAccountFacebook", p_dict), ApiAccountFacebook) as ApiAccountFacebook + + func serialize() -> Dictionary: + return NakamaSerializer.serialize(self) + + func _to_string() -> String: + if is_exception(): + return get_exception()._to_string() + var output : String = "" + output += "token: %s, " % _token + var map_string : String = "" + if typeof(_vars) == TYPE_DICTIONARY: + for k in _vars: + map_string += "{%s=%s}, " % [k, _vars[k]] + output += "vars: [%s], " % map_string + return output + +# Send a Facebook Instant Game token to the server. Used with authenticate/link/unlink. +class ApiAccountFacebookInstantGame extends NakamaAsyncResult: + + const _SCHEMA = { + "signed_player_info": {"name": "_signed_player_info", "type": TYPE_STRING, "required": false}, + "vars": {"name": "_vars", "type": TYPE_DICTIONARY, "required": false, "content": TYPE_STRING}, + } + + # + var _signed_player_info + var signed_player_info : String: + get: + return "" if not _signed_player_info is String else String(_signed_player_info) + + # Extra information that will be bundled in the session token. + var _vars + var vars : Dictionary: + get: + return Dictionary() if not _vars is Dictionary else _vars.duplicate() + + func _init(p_exception = null): + super(p_exception) + + static func create(p_ns : GDScript, p_dict : Dictionary) -> ApiAccountFacebookInstantGame: + return _safe_ret(NakamaSerializer.deserialize(p_ns, "ApiAccountFacebookInstantGame", p_dict), ApiAccountFacebookInstantGame) as ApiAccountFacebookInstantGame + + func serialize() -> Dictionary: + return NakamaSerializer.serialize(self) + + func _to_string() -> String: + if is_exception(): + return get_exception()._to_string() + var output : String = "" + output += "signed_player_info: %s, " % _signed_player_info + var map_string : String = "" + if typeof(_vars) == TYPE_DICTIONARY: + for k in _vars: + map_string += "{%s=%s}, " % [k, _vars[k]] + output += "vars: [%s], " % map_string + return output + +# Send Apple's Game Center account credentials to the server. Used with authenticate/link/unlink. +class ApiAccountGameCenter extends NakamaAsyncResult: + + const _SCHEMA = { + "bundle_id": {"name": "_bundle_id", "type": TYPE_STRING, "required": false}, + "player_id": {"name": "_player_id", "type": TYPE_STRING, "required": false}, + "public_key_url": {"name": "_public_key_url", "type": TYPE_STRING, "required": false}, + "salt": {"name": "_salt", "type": TYPE_STRING, "required": false}, + "signature": {"name": "_signature", "type": TYPE_STRING, "required": false}, + "timestamp_seconds": {"name": "_timestamp_seconds", "type": TYPE_STRING, "required": false}, + "vars": {"name": "_vars", "type": TYPE_DICTIONARY, "required": false, "content": TYPE_STRING}, + } + + # Bundle ID (generated by GameCenter). + var _bundle_id + var bundle_id : String: + get: + return "" if not _bundle_id is String else String(_bundle_id) + + # Player ID (generated by GameCenter). + var _player_id + var player_id : String: + get: + return "" if not _player_id is String else String(_player_id) + + # The URL for the public encryption key. + var _public_key_url + var public_key_url : String: + get: + return "" if not _public_key_url is String else String(_public_key_url) + + # A random "NSString" used to compute the hash and keep it randomized. + var _salt + var salt : String: + get: + return "" if not _salt is String else String(_salt) + + # The verification signature data generated. + var _signature + var signature : String: + get: + return "" if not _signature is String else String(_signature) + + # Time since UNIX epoch when the signature was created. + var _timestamp_seconds + var timestamp_seconds : String: + get: + return "" if not _timestamp_seconds is String else String(_timestamp_seconds) + + # Extra information that will be bundled in the session token. + var _vars + var vars : Dictionary: + get: + return Dictionary() if not _vars is Dictionary else _vars.duplicate() + + func _init(p_exception = null): + super(p_exception) + + static func create(p_ns : GDScript, p_dict : Dictionary) -> ApiAccountGameCenter: + return _safe_ret(NakamaSerializer.deserialize(p_ns, "ApiAccountGameCenter", p_dict), ApiAccountGameCenter) as ApiAccountGameCenter + + func serialize() -> Dictionary: + return NakamaSerializer.serialize(self) + + func _to_string() -> String: + if is_exception(): + return get_exception()._to_string() + var output : String = "" + output += "bundle_id: %s, " % _bundle_id + output += "player_id: %s, " % _player_id + output += "public_key_url: %s, " % _public_key_url + output += "salt: %s, " % _salt + output += "signature: %s, " % _signature + output += "timestamp_seconds: %s, " % _timestamp_seconds + var map_string : String = "" + if typeof(_vars) == TYPE_DICTIONARY: + for k in _vars: + map_string += "{%s=%s}, " % [k, _vars[k]] + output += "vars: [%s], " % map_string + return output + +# Send a Google token to the server. Used with authenticate/link/unlink. +class ApiAccountGoogle extends NakamaAsyncResult: + + const _SCHEMA = { + "token": {"name": "_token", "type": TYPE_STRING, "required": false}, + "vars": {"name": "_vars", "type": TYPE_DICTIONARY, "required": false, "content": TYPE_STRING}, + } + + # The OAuth token received from Google to access their profile API. + var _token + var token : String: + get: + return "" if not _token is String else String(_token) + + # Extra information that will be bundled in the session token. + var _vars + var vars : Dictionary: + get: + return Dictionary() if not _vars is Dictionary else _vars.duplicate() + + func _init(p_exception = null): + super(p_exception) + + static func create(p_ns : GDScript, p_dict : Dictionary) -> ApiAccountGoogle: + return _safe_ret(NakamaSerializer.deserialize(p_ns, "ApiAccountGoogle", p_dict), ApiAccountGoogle) as ApiAccountGoogle + + func serialize() -> Dictionary: + return NakamaSerializer.serialize(self) + + func _to_string() -> String: + if is_exception(): + return get_exception()._to_string() + var output : String = "" + output += "token: %s, " % _token + var map_string : String = "" + if typeof(_vars) == TYPE_DICTIONARY: + for k in _vars: + map_string += "{%s=%s}, " % [k, _vars[k]] + output += "vars: [%s], " % map_string + return output + +# Send a Steam token to the server. Used with authenticate/link/unlink. +class ApiAccountSteam extends NakamaAsyncResult: + + const _SCHEMA = { + "token": {"name": "_token", "type": TYPE_STRING, "required": false}, + "vars": {"name": "_vars", "type": TYPE_DICTIONARY, "required": false, "content": TYPE_STRING}, + } + + # The account token received from Steam to access their profile API. + var _token + var token : String: + get: + return "" if not _token is String else String(_token) + + # Extra information that will be bundled in the session token. + var _vars + var vars : Dictionary: + get: + return Dictionary() if not _vars is Dictionary else _vars.duplicate() + + func _init(p_exception = null): + super(p_exception) + + static func create(p_ns : GDScript, p_dict : Dictionary) -> ApiAccountSteam: + return _safe_ret(NakamaSerializer.deserialize(p_ns, "ApiAccountSteam", p_dict), ApiAccountSteam) as ApiAccountSteam + + func serialize() -> Dictionary: + return NakamaSerializer.serialize(self) + + func _to_string() -> String: + if is_exception(): + return get_exception()._to_string() + var output : String = "" + output += "token: %s, " % _token + var map_string : String = "" + if typeof(_vars) == TYPE_DICTIONARY: + for k in _vars: + map_string += "{%s=%s}, " % [k, _vars[k]] + output += "vars: [%s], " % map_string + return output + +# A message sent on a channel. +class ApiChannelMessage extends NakamaAsyncResult: + + const _SCHEMA = { + "channel_id": {"name": "_channel_id", "type": TYPE_STRING, "required": false}, + "code": {"name": "_code", "type": TYPE_INT, "required": false}, + "content": {"name": "_content", "type": TYPE_STRING, "required": false}, + "create_time": {"name": "_create_time", "type": TYPE_STRING, "required": false}, + "group_id": {"name": "_group_id", "type": TYPE_STRING, "required": false}, + "message_id": {"name": "_message_id", "type": TYPE_STRING, "required": false}, + "persistent": {"name": "_persistent", "type": TYPE_BOOL, "required": false}, + "room_name": {"name": "_room_name", "type": TYPE_STRING, "required": false}, + "sender_id": {"name": "_sender_id", "type": TYPE_STRING, "required": false}, + "update_time": {"name": "_update_time", "type": TYPE_STRING, "required": false}, + "user_id_one": {"name": "_user_id_one", "type": TYPE_STRING, "required": false}, + "user_id_two": {"name": "_user_id_two", "type": TYPE_STRING, "required": false}, + "username": {"name": "_username", "type": TYPE_STRING, "required": false}, + } + + # The channel this message belongs to. + var _channel_id + var channel_id : String: + get: + return "" if not _channel_id is String else String(_channel_id) + + # The code representing a message type or category. + var _code + var code : int: + get: + return 0 if not _code is int else int(_code) + + # The content payload. + var _content + var content : String: + get: + return "" if not _content is String else String(_content) + + # The UNIX time when the message was created. + var _create_time + var create_time : String: + get: + return "" if not _create_time is String else String(_create_time) + + # The ID of the group, or an empty string if this message was not sent through a group channel. + var _group_id + var group_id : String: + get: + return "" if not _group_id is String else String(_group_id) + + # The unique ID of this message. + var _message_id + var message_id : String: + get: + return "" if not _message_id is String else String(_message_id) + + # True if the message was persisted to the channel's history, false otherwise. + var _persistent + var persistent : bool: + get: + return false if not _persistent is bool else bool(_persistent) + + # The name of the chat room, or an empty string if this message was not sent through a chat room. + var _room_name + var room_name : String: + get: + return "" if not _room_name is String else String(_room_name) + + # Message sender, usually a user ID. + var _sender_id + var sender_id : String: + get: + return "" if not _sender_id is String else String(_sender_id) + + # The UNIX time when the message was last updated. + var _update_time + var update_time : String: + get: + return "" if not _update_time is String else String(_update_time) + + # The ID of the first DM user, or an empty string if this message was not sent through a DM chat. + var _user_id_one + var user_id_one : String: + get: + return "" if not _user_id_one is String else String(_user_id_one) + + # The ID of the second DM user, or an empty string if this message was not sent through a DM chat. + var _user_id_two + var user_id_two : String: + get: + return "" if not _user_id_two is String else String(_user_id_two) + + # The username of the message sender, if any. + var _username + var username : String: + get: + return "" if not _username is String else String(_username) + + func _init(p_exception = null): + super(p_exception) + + static func create(p_ns : GDScript, p_dict : Dictionary) -> ApiChannelMessage: + return _safe_ret(NakamaSerializer.deserialize(p_ns, "ApiChannelMessage", p_dict), ApiChannelMessage) as ApiChannelMessage + + func serialize() -> Dictionary: + return NakamaSerializer.serialize(self) + + func _to_string() -> String: + if is_exception(): + return get_exception()._to_string() + var output : String = "" + output += "channel_id: %s, " % _channel_id + output += "code: %s, " % _code + output += "content: %s, " % _content + output += "create_time: %s, " % _create_time + output += "group_id: %s, " % _group_id + output += "message_id: %s, " % _message_id + output += "persistent: %s, " % _persistent + output += "room_name: %s, " % _room_name + output += "sender_id: %s, " % _sender_id + output += "update_time: %s, " % _update_time + output += "user_id_one: %s, " % _user_id_one + output += "user_id_two: %s, " % _user_id_two + output += "username: %s, " % _username + return output + +# A list of channel messages, usually a result of a list operation. +class ApiChannelMessageList extends NakamaAsyncResult: + + const _SCHEMA = { + "cacheable_cursor": {"name": "_cacheable_cursor", "type": TYPE_STRING, "required": false}, + "messages": {"name": "_messages", "type": TYPE_ARRAY, "required": false, "content": "ApiChannelMessage"}, + "next_cursor": {"name": "_next_cursor", "type": TYPE_STRING, "required": false}, + "prev_cursor": {"name": "_prev_cursor", "type": TYPE_STRING, "required": false}, + } + + # Cacheable cursor to list newer messages. Durable and designed to be stored, unlike next/prev cursors. + var _cacheable_cursor + var cacheable_cursor : String: + get: + return "" if not _cacheable_cursor is String else String(_cacheable_cursor) + + # A list of messages. + var _messages + var messages : Array: + get: + return Array() if not _messages is Array else Array(_messages) + + # The cursor to send when retrieving the next page, if any. + var _next_cursor + var next_cursor : String: + get: + return "" if not _next_cursor is String else String(_next_cursor) + + # The cursor to send when retrieving the previous page, if any. + var _prev_cursor + var prev_cursor : String: + get: + return "" if not _prev_cursor is String else String(_prev_cursor) + + func _init(p_exception = null): + super(p_exception) + + static func create(p_ns : GDScript, p_dict : Dictionary) -> ApiChannelMessageList: + return _safe_ret(NakamaSerializer.deserialize(p_ns, "ApiChannelMessageList", p_dict), ApiChannelMessageList) as ApiChannelMessageList + + func serialize() -> Dictionary: + return NakamaSerializer.serialize(self) + + func _to_string() -> String: + if is_exception(): + return get_exception()._to_string() + var output : String = "" + output += "cacheable_cursor: %s, " % _cacheable_cursor + output += "messages: %s, " % [_messages] + output += "next_cursor: %s, " % _next_cursor + output += "prev_cursor: %s, " % _prev_cursor + return output + +# Create a group with the current user as owner. +class ApiCreateGroupRequest extends NakamaAsyncResult: + + const _SCHEMA = { + "avatar_url": {"name": "_avatar_url", "type": TYPE_STRING, "required": false}, + "description": {"name": "_description", "type": TYPE_STRING, "required": false}, + "lang_tag": {"name": "_lang_tag", "type": TYPE_STRING, "required": false}, + "max_count": {"name": "_max_count", "type": TYPE_INT, "required": false}, + "name": {"name": "_name", "type": TYPE_STRING, "required": false}, + "open": {"name": "_open", "type": TYPE_BOOL, "required": false}, + } + + # A URL for an avatar image. + var _avatar_url + var avatar_url : String: + get: + return "" if not _avatar_url is String else String(_avatar_url) + + # A description for the group. + var _description + var description : String: + get: + return "" if not _description is String else String(_description) + + # The language expected to be a tag which follows the BCP-47 spec. + var _lang_tag + var lang_tag : String: + get: + return "" if not _lang_tag is String else String(_lang_tag) + + # Maximum number of group members. + var _max_count + var max_count : int: + get: + return 0 if not _max_count is int else int(_max_count) + + # A unique name for the group. + var _name + var name : String: + get: + return "" if not _name is String else String(_name) + + # Mark a group as open or not where only admins can accept members. + var _open + var open : bool: + get: + return false if not _open is bool else bool(_open) + + func _init(p_exception = null): + super(p_exception) + + static func create(p_ns : GDScript, p_dict : Dictionary) -> ApiCreateGroupRequest: + return _safe_ret(NakamaSerializer.deserialize(p_ns, "ApiCreateGroupRequest", p_dict), ApiCreateGroupRequest) as ApiCreateGroupRequest + + func serialize() -> Dictionary: + return NakamaSerializer.serialize(self) + + func _to_string() -> String: + if is_exception(): + return get_exception()._to_string() + var output : String = "" + output += "avatar_url: %s, " % _avatar_url + output += "description: %s, " % _description + output += "lang_tag: %s, " % _lang_tag + output += "max_count: %s, " % _max_count + output += "name: %s, " % _name + output += "open: %s, " % _open + return output + +# Storage objects to delete. +class ApiDeleteStorageObjectId extends NakamaAsyncResult: + + const _SCHEMA = { + "collection": {"name": "_collection", "type": TYPE_STRING, "required": false}, + "key": {"name": "_key", "type": TYPE_STRING, "required": false}, + "version": {"name": "_version", "type": TYPE_STRING, "required": false}, + } + + # The collection which stores the object. + var _collection + var collection : String: + get: + return "" if not _collection is String else String(_collection) + + # The key of the object within the collection. + var _key + var key : String: + get: + return "" if not _key is String else String(_key) + + # The version hash of the object. + var _version + var version : String: + get: + return "" if not _version is String else String(_version) + + func _init(p_exception = null): + super(p_exception) + + static func create(p_ns : GDScript, p_dict : Dictionary) -> ApiDeleteStorageObjectId: + return _safe_ret(NakamaSerializer.deserialize(p_ns, "ApiDeleteStorageObjectId", p_dict), ApiDeleteStorageObjectId) as ApiDeleteStorageObjectId + + func serialize() -> Dictionary: + return NakamaSerializer.serialize(self) + + func _to_string() -> String: + if is_exception(): + return get_exception()._to_string() + var output : String = "" + output += "collection: %s, " % _collection + output += "key: %s, " % _key + output += "version: %s, " % _version + return output + +# Batch delete storage objects. +class ApiDeleteStorageObjectsRequest extends NakamaAsyncResult: + + const _SCHEMA = { + "object_ids": {"name": "_object_ids", "type": TYPE_ARRAY, "required": false, "content": "ApiDeleteStorageObjectId"}, + } + + # Batch of storage objects. + var _object_ids + var object_ids : Array: + get: + return Array() if not _object_ids is Array else Array(_object_ids) + + func _init(p_exception = null): + super(p_exception) + + static func create(p_ns : GDScript, p_dict : Dictionary) -> ApiDeleteStorageObjectsRequest: + return _safe_ret(NakamaSerializer.deserialize(p_ns, "ApiDeleteStorageObjectsRequest", p_dict), ApiDeleteStorageObjectsRequest) as ApiDeleteStorageObjectsRequest + + func serialize() -> Dictionary: + return NakamaSerializer.serialize(self) + + func _to_string() -> String: + if is_exception(): + return get_exception()._to_string() + var output : String = "" + output += "object_ids: %s, " % [_object_ids] + return output + +# Represents an event to be passed through the server to registered event handlers. +class ApiEvent extends NakamaAsyncResult: + + const _SCHEMA = { + "external": {"name": "_external", "type": TYPE_BOOL, "required": false}, + "name": {"name": "_name", "type": TYPE_STRING, "required": false}, + "properties": {"name": "_properties", "type": TYPE_DICTIONARY, "required": false, "content": TYPE_STRING}, + "timestamp": {"name": "_timestamp", "type": TYPE_STRING, "required": false}, + } + + # True if the event came directly from a client call, false otherwise. + var _external + var external : bool: + get: + return false if not _external is bool else bool(_external) + + # An event name, type, category, or identifier. + var _name + var name : String: + get: + return "" if not _name is String else String(_name) + + # Arbitrary event property values. + var _properties + var properties : Dictionary: + get: + return Dictionary() if not _properties is Dictionary else _properties.duplicate() + + # The time when the event was triggered. + var _timestamp + var timestamp : String: + get: + return "" if not _timestamp is String else String(_timestamp) + + func _init(p_exception = null): + super(p_exception) + + static func create(p_ns : GDScript, p_dict : Dictionary) -> ApiEvent: + return _safe_ret(NakamaSerializer.deserialize(p_ns, "ApiEvent", p_dict), ApiEvent) as ApiEvent + + func serialize() -> Dictionary: + return NakamaSerializer.serialize(self) + + func _to_string() -> String: + if is_exception(): + return get_exception()._to_string() + var output : String = "" + output += "external: %s, " % _external + output += "name: %s, " % _name + var map_string : String = "" + if typeof(_properties) == TYPE_DICTIONARY: + for k in _properties: + map_string += "{%s=%s}, " % [k, _properties[k]] + output += "properties: [%s], " % map_string + output += "timestamp: %s, " % _timestamp + return output + +# A friend of a user. +class ApiFriend extends NakamaAsyncResult: + + const _SCHEMA = { + "state": {"name": "_state", "type": TYPE_INT, "required": false}, + "update_time": {"name": "_update_time", "type": TYPE_STRING, "required": false}, + "user": {"name": "_user", "type": "ApiUser", "required": false}, + } + + # The friend status. + var _state + var state : int: + get: + return 0 if not _state is int else int(_state) + + # Time of the latest relationship update. + var _update_time + var update_time : String: + get: + return "" if not _update_time is String else String(_update_time) + + # The user object. + var _user + var user : ApiUser: + get: + return _user as ApiUser + + func _init(p_exception = null): + super(p_exception) + + static func create(p_ns : GDScript, p_dict : Dictionary) -> ApiFriend: + return _safe_ret(NakamaSerializer.deserialize(p_ns, "ApiFriend", p_dict), ApiFriend) as ApiFriend + + func serialize() -> Dictionary: + return NakamaSerializer.serialize(self) + + func _to_string() -> String: + if is_exception(): + return get_exception()._to_string() + var output : String = "" + output += "state: %s, " % _state + output += "update_time: %s, " % _update_time + output += "user: %s, " % _user + return output + +# A collection of zero or more friends of the user. +class ApiFriendList extends NakamaAsyncResult: + + const _SCHEMA = { + "cursor": {"name": "_cursor", "type": TYPE_STRING, "required": false}, + "friends": {"name": "_friends", "type": TYPE_ARRAY, "required": false, "content": "ApiFriend"}, + } + + # Cursor for the next page of results, if any. + var _cursor + var cursor : String: + get: + return "" if not _cursor is String else String(_cursor) + + # The Friend objects. + var _friends + var friends : Array: + get: + return Array() if not _friends is Array else Array(_friends) + + func _init(p_exception = null): + super(p_exception) + + static func create(p_ns : GDScript, p_dict : Dictionary) -> ApiFriendList: + return _safe_ret(NakamaSerializer.deserialize(p_ns, "ApiFriendList", p_dict), ApiFriendList) as ApiFriendList + + func serialize() -> Dictionary: + return NakamaSerializer.serialize(self) + + func _to_string() -> String: + if is_exception(): + return get_exception()._to_string() + var output : String = "" + output += "cursor: %s, " % _cursor + output += "friends: %s, " % [_friends] + return output + +# A group in the server. +class ApiGroup extends NakamaAsyncResult: + + const _SCHEMA = { + "avatar_url": {"name": "_avatar_url", "type": TYPE_STRING, "required": false}, + "create_time": {"name": "_create_time", "type": TYPE_STRING, "required": false}, + "creator_id": {"name": "_creator_id", "type": TYPE_STRING, "required": false}, + "description": {"name": "_description", "type": TYPE_STRING, "required": false}, + "edge_count": {"name": "_edge_count", "type": TYPE_INT, "required": false}, + "id": {"name": "_id", "type": TYPE_STRING, "required": false}, + "lang_tag": {"name": "_lang_tag", "type": TYPE_STRING, "required": false}, + "max_count": {"name": "_max_count", "type": TYPE_INT, "required": false}, + "metadata": {"name": "_metadata", "type": TYPE_STRING, "required": false}, + "name": {"name": "_name", "type": TYPE_STRING, "required": false}, + "open": {"name": "_open", "type": TYPE_BOOL, "required": false}, + "update_time": {"name": "_update_time", "type": TYPE_STRING, "required": false}, + } + + # A URL for an avatar image. + var _avatar_url + var avatar_url : String: + get: + return "" if not _avatar_url is String else String(_avatar_url) + + # The UNIX time when the group was created. + var _create_time + var create_time : String: + get: + return "" if not _create_time is String else String(_create_time) + + # The id of the user who created the group. + var _creator_id + var creator_id : String: + get: + return "" if not _creator_id is String else String(_creator_id) + + # A description for the group. + var _description + var description : String: + get: + return "" if not _description is String else String(_description) + + # The current count of all members in the group. + var _edge_count + var edge_count : int: + get: + return 0 if not _edge_count is int else int(_edge_count) + + # The id of a group. + var _id + var id : String: + get: + return "" if not _id is String else String(_id) + + # The language expected to be a tag which follows the BCP-47 spec. + var _lang_tag + var lang_tag : String: + get: + return "" if not _lang_tag is String else String(_lang_tag) + + # The maximum number of members allowed. + var _max_count + var max_count : int: + get: + return 0 if not _max_count is int else int(_max_count) + + # Additional information stored as a JSON object. + var _metadata + var metadata : String: + get: + return "" if not _metadata is String else String(_metadata) + + # The unique name of the group. + var _name + var name : String: + get: + return "" if not _name is String else String(_name) + + # Anyone can join open groups, otherwise only admins can accept members. + var _open + var open : bool: + get: + return false if not _open is bool else bool(_open) + + # The UNIX time when the group was last updated. + var _update_time + var update_time : String: + get: + return "" if not _update_time is String else String(_update_time) + + func _init(p_exception = null): + super(p_exception) + + static func create(p_ns : GDScript, p_dict : Dictionary) -> ApiGroup: + return _safe_ret(NakamaSerializer.deserialize(p_ns, "ApiGroup", p_dict), ApiGroup) as ApiGroup + + func serialize() -> Dictionary: + return NakamaSerializer.serialize(self) + + func _to_string() -> String: + if is_exception(): + return get_exception()._to_string() + var output : String = "" + output += "avatar_url: %s, " % _avatar_url + output += "create_time: %s, " % _create_time + output += "creator_id: %s, " % _creator_id + output += "description: %s, " % _description + output += "edge_count: %s, " % _edge_count + output += "id: %s, " % _id + output += "lang_tag: %s, " % _lang_tag + output += "max_count: %s, " % _max_count + output += "metadata: %s, " % _metadata + output += "name: %s, " % _name + output += "open: %s, " % _open + output += "update_time: %s, " % _update_time + return output + +# One or more groups returned from a listing operation. +class ApiGroupList extends NakamaAsyncResult: + + const _SCHEMA = { + "cursor": {"name": "_cursor", "type": TYPE_STRING, "required": false}, + "groups": {"name": "_groups", "type": TYPE_ARRAY, "required": false, "content": "ApiGroup"}, + } + + # A cursor used to get the next page. + var _cursor + var cursor : String: + get: + return "" if not _cursor is String else String(_cursor) + + # One or more groups. + var _groups + var groups : Array: + get: + return Array() if not _groups is Array else Array(_groups) + + func _init(p_exception = null): + super(p_exception) + + static func create(p_ns : GDScript, p_dict : Dictionary) -> ApiGroupList: + return _safe_ret(NakamaSerializer.deserialize(p_ns, "ApiGroupList", p_dict), ApiGroupList) as ApiGroupList + + func serialize() -> Dictionary: + return NakamaSerializer.serialize(self) + + func _to_string() -> String: + if is_exception(): + return get_exception()._to_string() + var output : String = "" + output += "cursor: %s, " % _cursor + output += "groups: %s, " % [_groups] + return output + +# A list of users belonging to a group, along with their role. +class ApiGroupUserList extends NakamaAsyncResult: + + const _SCHEMA = { + "cursor": {"name": "_cursor", "type": TYPE_STRING, "required": false}, + "group_users": {"name": "_group_users", "type": TYPE_ARRAY, "required": false, "content": "GroupUserListGroupUser"}, + } + + # Cursor for the next page of results, if any. + var _cursor + var cursor : String: + get: + return "" if not _cursor is String else String(_cursor) + + # User-role pairs for a group. + var _group_users + var group_users : Array: + get: + return Array() if not _group_users is Array else Array(_group_users) + + func _init(p_exception = null): + super(p_exception) + + static func create(p_ns : GDScript, p_dict : Dictionary) -> ApiGroupUserList: + return _safe_ret(NakamaSerializer.deserialize(p_ns, "ApiGroupUserList", p_dict), ApiGroupUserList) as ApiGroupUserList + + func serialize() -> Dictionary: + return NakamaSerializer.serialize(self) + + func _to_string() -> String: + if is_exception(): + return get_exception()._to_string() + var output : String = "" + output += "cursor: %s, " % _cursor + output += "group_users: %s, " % [_group_users] + return output + +# Represents a complete leaderboard record with all scores and associated metadata. +class ApiLeaderboardRecord extends NakamaAsyncResult: + + const _SCHEMA = { + "create_time": {"name": "_create_time", "type": TYPE_STRING, "required": false}, + "expiry_time": {"name": "_expiry_time", "type": TYPE_STRING, "required": false}, + "leaderboard_id": {"name": "_leaderboard_id", "type": TYPE_STRING, "required": false}, + "max_num_score": {"name": "_max_num_score", "type": TYPE_INT, "required": false}, + "metadata": {"name": "_metadata", "type": TYPE_STRING, "required": false}, + "num_score": {"name": "_num_score", "type": TYPE_INT, "required": false}, + "owner_id": {"name": "_owner_id", "type": TYPE_STRING, "required": false}, + "rank": {"name": "_rank", "type": TYPE_STRING, "required": false}, + "score": {"name": "_score", "type": TYPE_STRING, "required": false}, + "subscore": {"name": "_subscore", "type": TYPE_STRING, "required": false}, + "update_time": {"name": "_update_time", "type": TYPE_STRING, "required": false}, + "username": {"name": "_username", "type": TYPE_STRING, "required": false}, + } + + # The UNIX time when the leaderboard record was created. + var _create_time + var create_time : String: + get: + return "" if not _create_time is String else String(_create_time) + + # The UNIX time when the leaderboard record expires. + var _expiry_time + var expiry_time : String: + get: + return "" if not _expiry_time is String else String(_expiry_time) + + # The ID of the leaderboard this score belongs to. + var _leaderboard_id + var leaderboard_id : String: + get: + return "" if not _leaderboard_id is String else String(_leaderboard_id) + + # The maximum number of score updates allowed by the owner. + var _max_num_score + var max_num_score : int: + get: + return 0 if not _max_num_score is int else int(_max_num_score) + + # Metadata. + var _metadata + var metadata : String: + get: + return "" if not _metadata is String else String(_metadata) + + # The number of submissions to this score record. + var _num_score + var num_score : int: + get: + return 0 if not _num_score is int else int(_num_score) + + # The ID of the score owner, usually a user or group. + var _owner_id + var owner_id : String: + get: + return "" if not _owner_id is String else String(_owner_id) + + # The rank of this record. + var _rank + var rank : String: + get: + return "" if not _rank is String else String(_rank) + + # The score value. + var _score + var score : String: + get: + return "" if not _score is String else String(_score) + + # An optional subscore value. + var _subscore + var subscore : String: + get: + return "" if not _subscore is String else String(_subscore) + + # The UNIX time when the leaderboard record was updated. + var _update_time + var update_time : String: + get: + return "" if not _update_time is String else String(_update_time) + + # The username of the score owner, if the owner is a user. + var _username + var username : String: + get: + return "" if not _username is String else String(_username) + + func _init(p_exception = null): + super(p_exception) + + static func create(p_ns : GDScript, p_dict : Dictionary) -> ApiLeaderboardRecord: + return _safe_ret(NakamaSerializer.deserialize(p_ns, "ApiLeaderboardRecord", p_dict), ApiLeaderboardRecord) as ApiLeaderboardRecord + + func serialize() -> Dictionary: + return NakamaSerializer.serialize(self) + + func _to_string() -> String: + if is_exception(): + return get_exception()._to_string() + var output : String = "" + output += "create_time: %s, " % _create_time + output += "expiry_time: %s, " % _expiry_time + output += "leaderboard_id: %s, " % _leaderboard_id + output += "max_num_score: %s, " % _max_num_score + output += "metadata: %s, " % _metadata + output += "num_score: %s, " % _num_score + output += "owner_id: %s, " % _owner_id + output += "rank: %s, " % _rank + output += "score: %s, " % _score + output += "subscore: %s, " % _subscore + output += "update_time: %s, " % _update_time + output += "username: %s, " % _username + return output + +# A set of leaderboard records, may be part of a leaderboard records page or a batch of individual records. +class ApiLeaderboardRecordList extends NakamaAsyncResult: + + const _SCHEMA = { + "next_cursor": {"name": "_next_cursor", "type": TYPE_STRING, "required": false}, + "owner_records": {"name": "_owner_records", "type": TYPE_ARRAY, "required": false, "content": "ApiLeaderboardRecord"}, + "prev_cursor": {"name": "_prev_cursor", "type": TYPE_STRING, "required": false}, + "records": {"name": "_records", "type": TYPE_ARRAY, "required": false, "content": "ApiLeaderboardRecord"}, + } + + # The cursor to send when retrieving the next page, if any. + var _next_cursor + var next_cursor : String: + get: + return "" if not _next_cursor is String else String(_next_cursor) + + # A batched set of leaderboard records belonging to specified owners. + var _owner_records + var owner_records : Array: + get: + return Array() if not _owner_records is Array else Array(_owner_records) + + # The cursor to send when retrieving the previous page, if any. + var _prev_cursor + var prev_cursor : String: + get: + return "" if not _prev_cursor is String else String(_prev_cursor) + + # A list of leaderboard records. + var _records + var records : Array: + get: + return Array() if not _records is Array else Array(_records) + + func _init(p_exception = null): + super(p_exception) + + static func create(p_ns : GDScript, p_dict : Dictionary) -> ApiLeaderboardRecordList: + return _safe_ret(NakamaSerializer.deserialize(p_ns, "ApiLeaderboardRecordList", p_dict), ApiLeaderboardRecordList) as ApiLeaderboardRecordList + + func serialize() -> Dictionary: + return NakamaSerializer.serialize(self) + + func _to_string() -> String: + if is_exception(): + return get_exception()._to_string() + var output : String = "" + output += "next_cursor: %s, " % _next_cursor + output += "owner_records: %s, " % [_owner_records] + output += "prev_cursor: %s, " % _prev_cursor + output += "records: %s, " % [_records] + return output + +# Link Steam to the current user's account. +class ApiLinkSteamRequest extends NakamaAsyncResult: + + const _SCHEMA = { + "account": {"name": "_account", "type": "ApiAccountSteam", "required": false}, + "sync": {"name": "_sync", "type": TYPE_BOOL, "required": false}, + } + + # The Facebook account details. + var _account + var account : ApiAccountSteam: + get: + return _account as ApiAccountSteam + + # Import Steam friends for the user. + var _sync + var sync : bool: + get: + return false if not _sync is bool else bool(_sync) + + func _init(p_exception = null): + super(p_exception) + + static func create(p_ns : GDScript, p_dict : Dictionary) -> ApiLinkSteamRequest: + return _safe_ret(NakamaSerializer.deserialize(p_ns, "ApiLinkSteamRequest", p_dict), ApiLinkSteamRequest) as ApiLinkSteamRequest + + func serialize() -> Dictionary: + return NakamaSerializer.serialize(self) + + func _to_string() -> String: + if is_exception(): + return get_exception()._to_string() + var output : String = "" + output += "account: %s, " % _account + output += "sync: %s, " % _sync + return output + +# List user subscriptions. +class ApiListSubscriptionsRequest extends NakamaAsyncResult: + + const _SCHEMA = { + "cursor": {"name": "_cursor", "type": TYPE_STRING, "required": false}, + "limit": {"name": "_limit", "type": TYPE_INT, "required": false}, + } + + # + var _cursor + var cursor : String: + get: + return "" if not _cursor is String else String(_cursor) + + # + var _limit + var limit : int: + get: + return 0 if not _limit is int else int(_limit) + + func _init(p_exception = null): + super(p_exception) + + static func create(p_ns : GDScript, p_dict : Dictionary) -> ApiListSubscriptionsRequest: + return _safe_ret(NakamaSerializer.deserialize(p_ns, "ApiListSubscriptionsRequest", p_dict), ApiListSubscriptionsRequest) as ApiListSubscriptionsRequest + + func serialize() -> Dictionary: + return NakamaSerializer.serialize(self) + + func _to_string() -> String: + if is_exception(): + return get_exception()._to_string() + var output : String = "" + output += "cursor: %s, " % _cursor + output += "limit: %s, " % _limit + return output + +# Represents a realtime match. +class ApiMatch extends NakamaAsyncResult: + + const _SCHEMA = { + "authoritative": {"name": "_authoritative", "type": TYPE_BOOL, "required": false}, + "handler_name": {"name": "_handler_name", "type": TYPE_STRING, "required": false}, + "label": {"name": "_label", "type": TYPE_STRING, "required": false}, + "match_id": {"name": "_match_id", "type": TYPE_STRING, "required": false}, + "size": {"name": "_size", "type": TYPE_INT, "required": false}, + "tick_rate": {"name": "_tick_rate", "type": TYPE_INT, "required": false}, + } + + # True if it's an server-managed authoritative match, false otherwise. + var _authoritative + var authoritative : bool: + get: + return false if not _authoritative is bool else bool(_authoritative) + + # + var _handler_name + var handler_name : String: + get: + return "" if not _handler_name is String else String(_handler_name) + + # Match label, if any. + var _label + var label : String: + get: + return "" if not _label is String else String(_label) + + # The ID of the match, can be used to join. + var _match_id + var match_id : String: + get: + return "" if not _match_id is String else String(_match_id) + + # Current number of users in the match. + var _size + var size : int: + get: + return 0 if not _size is int else int(_size) + + # + var _tick_rate + var tick_rate : int: + get: + return 0 if not _tick_rate is int else int(_tick_rate) + + func _init(p_exception = null): + super(p_exception) + + static func create(p_ns : GDScript, p_dict : Dictionary) -> ApiMatch: + return _safe_ret(NakamaSerializer.deserialize(p_ns, "ApiMatch", p_dict), ApiMatch) as ApiMatch + + func serialize() -> Dictionary: + return NakamaSerializer.serialize(self) + + func _to_string() -> String: + if is_exception(): + return get_exception()._to_string() + var output : String = "" + output += "authoritative: %s, " % _authoritative + output += "handler_name: %s, " % _handler_name + output += "label: %s, " % _label + output += "match_id: %s, " % _match_id + output += "size: %s, " % _size + output += "tick_rate: %s, " % _tick_rate + return output + +# A list of realtime matches. +class ApiMatchList extends NakamaAsyncResult: + + const _SCHEMA = { + "matches": {"name": "_matches", "type": TYPE_ARRAY, "required": false, "content": "ApiMatch"}, + } + + # A number of matches corresponding to a list operation. + var _matches + var matches : Array: + get: + return Array() if not _matches is Array else Array(_matches) + + func _init(p_exception = null): + super(p_exception) + + static func create(p_ns : GDScript, p_dict : Dictionary) -> ApiMatchList: + return _safe_ret(NakamaSerializer.deserialize(p_ns, "ApiMatchList", p_dict), ApiMatchList) as ApiMatchList + + func serialize() -> Dictionary: + return NakamaSerializer.serialize(self) + + func _to_string() -> String: + if is_exception(): + return get_exception()._to_string() + var output : String = "" + output += "matches: %s, " % [_matches] + return output + +# A notification in the server. +class ApiNotification extends NakamaAsyncResult: + + const _SCHEMA = { + "code": {"name": "_code", "type": TYPE_INT, "required": false}, + "content": {"name": "_content", "type": TYPE_STRING, "required": false}, + "create_time": {"name": "_create_time", "type": TYPE_STRING, "required": false}, + "id": {"name": "_id", "type": TYPE_STRING, "required": false}, + "persistent": {"name": "_persistent", "type": TYPE_BOOL, "required": false}, + "sender_id": {"name": "_sender_id", "type": TYPE_STRING, "required": false}, + "subject": {"name": "_subject", "type": TYPE_STRING, "required": false}, + } + + # Category code for this notification. + var _code + var code : int: + get: + return 0 if not _code is int else int(_code) + + # Content of the notification in JSON. + var _content + var content : String: + get: + return "" if not _content is String else String(_content) + + # The UNIX time when the notification was created. + var _create_time + var create_time : String: + get: + return "" if not _create_time is String else String(_create_time) + + # ID of the Notification. + var _id + var id : String: + get: + return "" if not _id is String else String(_id) + + # True if this notification was persisted to the database. + var _persistent + var persistent : bool: + get: + return false if not _persistent is bool else bool(_persistent) + + # ID of the sender, if a user. Otherwise 'null'. + var _sender_id + var sender_id : String: + get: + return "" if not _sender_id is String else String(_sender_id) + + # Subject of the notification. + var _subject + var subject : String: + get: + return "" if not _subject is String else String(_subject) + + func _init(p_exception = null): + super(p_exception) + + static func create(p_ns : GDScript, p_dict : Dictionary) -> ApiNotification: + return _safe_ret(NakamaSerializer.deserialize(p_ns, "ApiNotification", p_dict), ApiNotification) as ApiNotification + + func serialize() -> Dictionary: + return NakamaSerializer.serialize(self) + + func _to_string() -> String: + if is_exception(): + return get_exception()._to_string() + var output : String = "" + output += "code: %s, " % _code + output += "content: %s, " % _content + output += "create_time: %s, " % _create_time + output += "id: %s, " % _id + output += "persistent: %s, " % _persistent + output += "sender_id: %s, " % _sender_id + output += "subject: %s, " % _subject + return output + +# A collection of zero or more notifications. +class ApiNotificationList extends NakamaAsyncResult: + + const _SCHEMA = { + "cacheable_cursor": {"name": "_cacheable_cursor", "type": TYPE_STRING, "required": false}, + "notifications": {"name": "_notifications", "type": TYPE_ARRAY, "required": false, "content": "ApiNotification"}, + } + + # Use this cursor to paginate notifications. Cache this to catch up to new notifications. + var _cacheable_cursor + var cacheable_cursor : String: + get: + return "" if not _cacheable_cursor is String else String(_cacheable_cursor) + + # Collection of notifications. + var _notifications + var notifications : Array: + get: + return Array() if not _notifications is Array else Array(_notifications) + + func _init(p_exception = null): + super(p_exception) + + static func create(p_ns : GDScript, p_dict : Dictionary) -> ApiNotificationList: + return _safe_ret(NakamaSerializer.deserialize(p_ns, "ApiNotificationList", p_dict), ApiNotificationList) as ApiNotificationList + + func serialize() -> Dictionary: + return NakamaSerializer.serialize(self) + + func _to_string() -> String: + if is_exception(): + return get_exception()._to_string() + var output : String = "" + output += "cacheable_cursor: %s, " % _cacheable_cursor + output += "notifications: %s, " % [_notifications] + return output + +# Operator that can be used to override the one set in the leaderboard. +# - NO_OVERRIDE: Do not override the leaderboard operator. +# - BEST: Override the leaderboard operator with BEST. +# - SET: Override the leaderboard operator with SET. +# - INCREMENT: Override the leaderboard operator with INCREMENT. +# - DECREMENT: Override the leaderboard operator with DECREMENT.# [ - NO_OVERRIDE: Do not override the leaderboard operator. - BEST: Override the leaderboard operator with BEST. - SET: Override the leaderboard operator with SET. - INCREMENT: Override the leaderboard operator with INCREMENT. - DECREMENT: Override the leaderboard operator with DECREMENT.] +enum ApiOperator {NO_OVERRIDE = 0,BEST = 1,SET = 2,INCREMENT = 3,DECREMENT = 4,} + +# Storage objects to get. +class ApiReadStorageObjectId extends NakamaAsyncResult: + + const _SCHEMA = { + "collection": {"name": "_collection", "type": TYPE_STRING, "required": false}, + "key": {"name": "_key", "type": TYPE_STRING, "required": false}, + "user_id": {"name": "_user_id", "type": TYPE_STRING, "required": false}, + } + + # The collection which stores the object. + var _collection + var collection : String: + get: + return "" if not _collection is String else String(_collection) + + # The key of the object within the collection. + var _key + var key : String: + get: + return "" if not _key is String else String(_key) + + # The user owner of the object. + var _user_id + var user_id : String: + get: + return "" if not _user_id is String else String(_user_id) + + func _init(p_exception = null): + super(p_exception) + + static func create(p_ns : GDScript, p_dict : Dictionary) -> ApiReadStorageObjectId: + return _safe_ret(NakamaSerializer.deserialize(p_ns, "ApiReadStorageObjectId", p_dict), ApiReadStorageObjectId) as ApiReadStorageObjectId + + func serialize() -> Dictionary: + return NakamaSerializer.serialize(self) + + func _to_string() -> String: + if is_exception(): + return get_exception()._to_string() + var output : String = "" + output += "collection: %s, " % _collection + output += "key: %s, " % _key + output += "user_id: %s, " % _user_id + return output + +# Batch get storage objects. +class ApiReadStorageObjectsRequest extends NakamaAsyncResult: + + const _SCHEMA = { + "object_ids": {"name": "_object_ids", "type": TYPE_ARRAY, "required": false, "content": "ApiReadStorageObjectId"}, + } + + # Batch of storage objects. + var _object_ids + var object_ids : Array: + get: + return Array() if not _object_ids is Array else Array(_object_ids) + + func _init(p_exception = null): + super(p_exception) + + static func create(p_ns : GDScript, p_dict : Dictionary) -> ApiReadStorageObjectsRequest: + return _safe_ret(NakamaSerializer.deserialize(p_ns, "ApiReadStorageObjectsRequest", p_dict), ApiReadStorageObjectsRequest) as ApiReadStorageObjectsRequest + + func serialize() -> Dictionary: + return NakamaSerializer.serialize(self) + + func _to_string() -> String: + if is_exception(): + return get_exception()._to_string() + var output : String = "" + output += "object_ids: %s, " % [_object_ids] + return output + +# Execute an Lua function on the server. +class ApiRpc extends NakamaAsyncResult: + + const _SCHEMA = { + "http_key": {"name": "_http_key", "type": TYPE_STRING, "required": false}, + "id": {"name": "_id", "type": TYPE_STRING, "required": false}, + "payload": {"name": "_payload", "type": TYPE_STRING, "required": false}, + } + + # The authentication key used when executed as a non-client HTTP request. + var _http_key + var http_key : String: + get: + return "" if not _http_key is String else String(_http_key) + + # The identifier of the function. + var _id + var id : String: + get: + return "" if not _id is String else String(_id) + + # The payload of the function which must be a JSON object. + var _payload + var payload : String: + get: + return "" if not _payload is String else String(_payload) + + func _init(p_exception = null): + super(p_exception) + + static func create(p_ns : GDScript, p_dict : Dictionary) -> ApiRpc: + return _safe_ret(NakamaSerializer.deserialize(p_ns, "ApiRpc", p_dict), ApiRpc) as ApiRpc + + func serialize() -> Dictionary: + return NakamaSerializer.serialize(self) + + func _to_string() -> String: + if is_exception(): + return get_exception()._to_string() + var output : String = "" + output += "http_key: %s, " % _http_key + output += "id: %s, " % _id + output += "payload: %s, " % _payload + return output + +# A user's session used to authenticate messages. +class ApiSession extends NakamaAsyncResult: + + const _SCHEMA = { + "created": {"name": "_created", "type": TYPE_BOOL, "required": false}, + "refresh_token": {"name": "_refresh_token", "type": TYPE_STRING, "required": false}, + "token": {"name": "_token", "type": TYPE_STRING, "required": false}, + } + + # True if the corresponding account was just created, false otherwise. + var _created + var created : bool: + get: + return false if not _created is bool else bool(_created) + + # Refresh token that can be used for session token renewal. + var _refresh_token + var refresh_token : String: + get: + return "" if not _refresh_token is String else String(_refresh_token) + + # Authentication credentials. + var _token + var token : String: + get: + return "" if not _token is String else String(_token) + + func _init(p_exception = null): + super(p_exception) + + static func create(p_ns : GDScript, p_dict : Dictionary) -> ApiSession: + return _safe_ret(NakamaSerializer.deserialize(p_ns, "ApiSession", p_dict), ApiSession) as ApiSession + + func serialize() -> Dictionary: + return NakamaSerializer.serialize(self) + + func _to_string() -> String: + if is_exception(): + return get_exception()._to_string() + var output : String = "" + output += "created: %s, " % _created + output += "refresh_token: %s, " % _refresh_token + output += "token: %s, " % _token + return output + +# Log out a session, invalidate a refresh token, or log out all sessions/refresh tokens for a user. +class ApiSessionLogoutRequest extends NakamaAsyncResult: + + const _SCHEMA = { + "refresh_token": {"name": "_refresh_token", "type": TYPE_STRING, "required": false}, + "token": {"name": "_token", "type": TYPE_STRING, "required": false}, + } + + # Refresh token to invalidate. + var _refresh_token + var refresh_token : String: + get: + return "" if not _refresh_token is String else String(_refresh_token) + + # Session token to log out. + var _token + var token : String: + get: + return "" if not _token is String else String(_token) + + func _init(p_exception = null): + super(p_exception) + + static func create(p_ns : GDScript, p_dict : Dictionary) -> ApiSessionLogoutRequest: + return _safe_ret(NakamaSerializer.deserialize(p_ns, "ApiSessionLogoutRequest", p_dict), ApiSessionLogoutRequest) as ApiSessionLogoutRequest + + func serialize() -> Dictionary: + return NakamaSerializer.serialize(self) + + func _to_string() -> String: + if is_exception(): + return get_exception()._to_string() + var output : String = "" + output += "refresh_token: %s, " % _refresh_token + output += "token: %s, " % _token + return output + +# Authenticate against the server with a refresh token. +class ApiSessionRefreshRequest extends NakamaAsyncResult: + + const _SCHEMA = { + "token": {"name": "_token", "type": TYPE_STRING, "required": false}, + "vars": {"name": "_vars", "type": TYPE_DICTIONARY, "required": false, "content": TYPE_STRING}, + } + + # Refresh token. + var _token + var token : String: + get: + return "" if not _token is String else String(_token) + + # Extra information that will be bundled in the session token. + var _vars + var vars : Dictionary: + get: + return Dictionary() if not _vars is Dictionary else _vars.duplicate() + + func _init(p_exception = null): + super(p_exception) + + static func create(p_ns : GDScript, p_dict : Dictionary) -> ApiSessionRefreshRequest: + return _safe_ret(NakamaSerializer.deserialize(p_ns, "ApiSessionRefreshRequest", p_dict), ApiSessionRefreshRequest) as ApiSessionRefreshRequest + + func serialize() -> Dictionary: + return NakamaSerializer.serialize(self) + + func _to_string() -> String: + if is_exception(): + return get_exception()._to_string() + var output : String = "" + output += "token: %s, " % _token + var map_string : String = "" + if typeof(_vars) == TYPE_DICTIONARY: + for k in _vars: + map_string += "{%s=%s}, " % [k, _vars[k]] + output += "vars: [%s], " % map_string + return output + +# An object within the storage engine. +class ApiStorageObject extends NakamaAsyncResult: + + const _SCHEMA = { + "collection": {"name": "_collection", "type": TYPE_STRING, "required": false}, + "create_time": {"name": "_create_time", "type": TYPE_STRING, "required": false}, + "key": {"name": "_key", "type": TYPE_STRING, "required": false}, + "permission_read": {"name": "_permission_read", "type": TYPE_INT, "required": false}, + "permission_write": {"name": "_permission_write", "type": TYPE_INT, "required": false}, + "update_time": {"name": "_update_time", "type": TYPE_STRING, "required": false}, + "user_id": {"name": "_user_id", "type": TYPE_STRING, "required": false}, + "value": {"name": "_value", "type": TYPE_STRING, "required": false}, + "version": {"name": "_version", "type": TYPE_STRING, "required": false}, + } + + # The collection which stores the object. + var _collection + var collection : String: + get: + return "" if not _collection is String else String(_collection) + + # The UNIX time when the object was created. + var _create_time + var create_time : String: + get: + return "" if not _create_time is String else String(_create_time) + + # The key of the object within the collection. + var _key + var key : String: + get: + return "" if not _key is String else String(_key) + + # The read access permissions for the object. + var _permission_read + var permission_read : int: + get: + return 0 if not _permission_read is int else int(_permission_read) + + # The write access permissions for the object. + var _permission_write + var permission_write : int: + get: + return 0 if not _permission_write is int else int(_permission_write) + + # The UNIX time when the object was last updated. + var _update_time + var update_time : String: + get: + return "" if not _update_time is String else String(_update_time) + + # The user owner of the object. + var _user_id + var user_id : String: + get: + return "" if not _user_id is String else String(_user_id) + + # The value of the object. + var _value + var value : String: + get: + return "" if not _value is String else String(_value) + + # The version hash of the object. + var _version + var version : String: + get: + return "" if not _version is String else String(_version) + + func _init(p_exception = null): + super(p_exception) + + static func create(p_ns : GDScript, p_dict : Dictionary) -> ApiStorageObject: + return _safe_ret(NakamaSerializer.deserialize(p_ns, "ApiStorageObject", p_dict), ApiStorageObject) as ApiStorageObject + + func serialize() -> Dictionary: + return NakamaSerializer.serialize(self) + + func _to_string() -> String: + if is_exception(): + return get_exception()._to_string() + var output : String = "" + output += "collection: %s, " % _collection + output += "create_time: %s, " % _create_time + output += "key: %s, " % _key + output += "permission_read: %s, " % _permission_read + output += "permission_write: %s, " % _permission_write + output += "update_time: %s, " % _update_time + output += "user_id: %s, " % _user_id + output += "value: %s, " % _value + output += "version: %s, " % _version + return output + +# A storage acknowledgement. +class ApiStorageObjectAck extends NakamaAsyncResult: + + const _SCHEMA = { + "collection": {"name": "_collection", "type": TYPE_STRING, "required": false}, + "key": {"name": "_key", "type": TYPE_STRING, "required": false}, + "user_id": {"name": "_user_id", "type": TYPE_STRING, "required": false}, + "version": {"name": "_version", "type": TYPE_STRING, "required": false}, + } + + # The collection which stores the object. + var _collection + var collection : String: + get: + return "" if not _collection is String else String(_collection) + + # The key of the object within the collection. + var _key + var key : String: + get: + return "" if not _key is String else String(_key) + + # The owner of the object. + var _user_id + var user_id : String: + get: + return "" if not _user_id is String else String(_user_id) + + # The version hash of the object. + var _version + var version : String: + get: + return "" if not _version is String else String(_version) + + func _init(p_exception = null): + super(p_exception) + + static func create(p_ns : GDScript, p_dict : Dictionary) -> ApiStorageObjectAck: + return _safe_ret(NakamaSerializer.deserialize(p_ns, "ApiStorageObjectAck", p_dict), ApiStorageObjectAck) as ApiStorageObjectAck + + func serialize() -> Dictionary: + return NakamaSerializer.serialize(self) + + func _to_string() -> String: + if is_exception(): + return get_exception()._to_string() + var output : String = "" + output += "collection: %s, " % _collection + output += "key: %s, " % _key + output += "user_id: %s, " % _user_id + output += "version: %s, " % _version + return output + +# Batch of acknowledgements for the storage object write. +class ApiStorageObjectAcks extends NakamaAsyncResult: + + const _SCHEMA = { + "acks": {"name": "_acks", "type": TYPE_ARRAY, "required": false, "content": "ApiStorageObjectAck"}, + } + + # Batch of storage write acknowledgements. + var _acks + var acks : Array: + get: + return Array() if not _acks is Array else Array(_acks) + + func _init(p_exception = null): + super(p_exception) + + static func create(p_ns : GDScript, p_dict : Dictionary) -> ApiStorageObjectAcks: + return _safe_ret(NakamaSerializer.deserialize(p_ns, "ApiStorageObjectAcks", p_dict), ApiStorageObjectAcks) as ApiStorageObjectAcks + + func serialize() -> Dictionary: + return NakamaSerializer.serialize(self) + + func _to_string() -> String: + if is_exception(): + return get_exception()._to_string() + var output : String = "" + output += "acks: %s, " % [_acks] + return output + +# List of storage objects. +class ApiStorageObjectList extends NakamaAsyncResult: + + const _SCHEMA = { + "cursor": {"name": "_cursor", "type": TYPE_STRING, "required": false}, + "objects": {"name": "_objects", "type": TYPE_ARRAY, "required": false, "content": "ApiStorageObject"}, + } + + # The cursor for the next page of results, if any. + var _cursor + var cursor : String: + get: + return "" if not _cursor is String else String(_cursor) + + # The list of storage objects. + var _objects + var objects : Array: + get: + return Array() if not _objects is Array else Array(_objects) + + func _init(p_exception = null): + super(p_exception) + + static func create(p_ns : GDScript, p_dict : Dictionary) -> ApiStorageObjectList: + return _safe_ret(NakamaSerializer.deserialize(p_ns, "ApiStorageObjectList", p_dict), ApiStorageObjectList) as ApiStorageObjectList + + func serialize() -> Dictionary: + return NakamaSerializer.serialize(self) + + func _to_string() -> String: + if is_exception(): + return get_exception()._to_string() + var output : String = "" + output += "cursor: %s, " % _cursor + output += "objects: %s, " % [_objects] + return output + +# Batch of storage objects. +class ApiStorageObjects extends NakamaAsyncResult: + + const _SCHEMA = { + "objects": {"name": "_objects", "type": TYPE_ARRAY, "required": false, "content": "ApiStorageObject"}, + } + + # The batch of storage objects. + var _objects + var objects : Array: + get: + return Array() if not _objects is Array else Array(_objects) + + func _init(p_exception = null): + super(p_exception) + + static func create(p_ns : GDScript, p_dict : Dictionary) -> ApiStorageObjects: + return _safe_ret(NakamaSerializer.deserialize(p_ns, "ApiStorageObjects", p_dict), ApiStorageObjects) as ApiStorageObjects + + func serialize() -> Dictionary: + return NakamaSerializer.serialize(self) + + func _to_string() -> String: + if is_exception(): + return get_exception()._to_string() + var output : String = "" + output += "objects: %s, " % [_objects] + return output + +# Environment where a purchase/subscription took place, +# - UNKNOWN: Unknown environment. +# - SANDBOX: Sandbox/test environment. +# - PRODUCTION: Production environment.# [- UNKNOWN: Unknown environment. - SANDBOX: Sandbox/test environment. - PRODUCTION: Production environment.] +enum ApiStoreEnvironment {UNKNOWN = 0,SANDBOX = 1,PRODUCTION = 2,} + +# Validation Provider, +# - APPLE_APP_STORE: Apple App Store +# - GOOGLE_PLAY_STORE: Google Play Store +# - HUAWEI_APP_GALLERY: Huawei App Gallery# [- APPLE_APP_STORE: Apple App Store - GOOGLE_PLAY_STORE: Google Play Store - HUAWEI_APP_GALLERY: Huawei App Gallery] +enum ApiStoreProvider {APPLE_APP_STORE = 0,GOOGLE_PLAY_STORE = 1,HUAWEI_APP_GALLERY = 2,} + +# A list of validated subscriptions stored by Nakama. +class ApiSubscriptionList extends NakamaAsyncResult: + + const _SCHEMA = { + "cursor": {"name": "_cursor", "type": TYPE_STRING, "required": false}, + "prev_cursor": {"name": "_prev_cursor", "type": TYPE_STRING, "required": false}, + "validated_subscriptions": {"name": "_validated_subscriptions", "type": TYPE_ARRAY, "required": false, "content": "ApiValidatedSubscription"}, + } + + # The cursor to send when retrieving the next page, if any. + var _cursor + var cursor : String: + get: + return "" if not _cursor is String else String(_cursor) + + # The cursor to send when retrieving the previous page, if any. + var _prev_cursor + var prev_cursor : String: + get: + return "" if not _prev_cursor is String else String(_prev_cursor) + + # Stored validated subscriptions. + var _validated_subscriptions + var validated_subscriptions : Array: + get: + return Array() if not _validated_subscriptions is Array else Array(_validated_subscriptions) + + func _init(p_exception = null): + super(p_exception) + + static func create(p_ns : GDScript, p_dict : Dictionary) -> ApiSubscriptionList: + return _safe_ret(NakamaSerializer.deserialize(p_ns, "ApiSubscriptionList", p_dict), ApiSubscriptionList) as ApiSubscriptionList + + func serialize() -> Dictionary: + return NakamaSerializer.serialize(self) + + func _to_string() -> String: + if is_exception(): + return get_exception()._to_string() + var output : String = "" + output += "cursor: %s, " % _cursor + output += "prev_cursor: %s, " % _prev_cursor + output += "validated_subscriptions: %s, " % [_validated_subscriptions] + return output + +# A tournament on the server. +class ApiTournament extends NakamaAsyncResult: + + const _SCHEMA = { + "authoritative": {"name": "_authoritative", "type": TYPE_BOOL, "required": false}, + "can_enter": {"name": "_can_enter", "type": TYPE_BOOL, "required": false}, + "category": {"name": "_category", "type": TYPE_INT, "required": false}, + "create_time": {"name": "_create_time", "type": TYPE_STRING, "required": false}, + "description": {"name": "_description", "type": TYPE_STRING, "required": false}, + "duration": {"name": "_duration", "type": TYPE_INT, "required": false}, + "end_active": {"name": "_end_active", "type": TYPE_INT, "required": false}, + "end_time": {"name": "_end_time", "type": TYPE_STRING, "required": false}, + "id": {"name": "_id", "type": TYPE_STRING, "required": false}, + "max_num_score": {"name": "_max_num_score", "type": TYPE_INT, "required": false}, + "max_size": {"name": "_max_size", "type": TYPE_INT, "required": false}, + "metadata": {"name": "_metadata", "type": TYPE_STRING, "required": false}, + "next_reset": {"name": "_next_reset", "type": TYPE_INT, "required": false}, + "operator": {"name": "_operator", "type": TYPE_INT, "required": false}, + "prev_reset": {"name": "_prev_reset", "type": TYPE_INT, "required": false}, + "size": {"name": "_size", "type": TYPE_INT, "required": false}, + "sort_order": {"name": "_sort_order", "type": TYPE_INT, "required": false}, + "start_active": {"name": "_start_active", "type": TYPE_INT, "required": false}, + "start_time": {"name": "_start_time", "type": TYPE_STRING, "required": false}, + "title": {"name": "_title", "type": TYPE_STRING, "required": false}, + } + + # Whether the leaderboard was created authoritatively or not. + var _authoritative + var authoritative : bool: + get: + return false if not _authoritative is bool else bool(_authoritative) + + # True if the tournament is active and can enter. A computed value. + var _can_enter + var can_enter : bool: + get: + return false if not _can_enter is bool else bool(_can_enter) + + # The category of the tournament. e.g. "vip" could be category 1. + var _category + var category : int: + get: + return 0 if not _category is int else int(_category) + + # The UNIX time when the tournament was created. + var _create_time + var create_time : String: + get: + return "" if not _create_time is String else String(_create_time) + + # The description of the tournament. May be blank. + var _description + var description : String: + get: + return "" if not _description is String else String(_description) + + # Duration of the tournament in seconds. + var _duration + var duration : int: + get: + return 0 if not _duration is int else int(_duration) + + # The UNIX time when the tournament stops being active until next reset. A computed value. + var _end_active + var end_active : int: + get: + return 0 if not _end_active is int else int(_end_active) + + # The UNIX time when the tournament will be stopped. + var _end_time + var end_time : String: + get: + return "" if not _end_time is String else String(_end_time) + + # The ID of the tournament. + var _id + var id : String: + get: + return "" if not _id is String else String(_id) + + # The maximum score updates allowed per player for the current tournament. + var _max_num_score + var max_num_score : int: + get: + return 0 if not _max_num_score is int else int(_max_num_score) + + # The maximum number of players for the tournament. + var _max_size + var max_size : int: + get: + return 0 if not _max_size is int else int(_max_size) + + # Additional information stored as a JSON object. + var _metadata + var metadata : String: + get: + return "" if not _metadata is String else String(_metadata) + + # The UNIX time when the tournament is next playable. A computed value. + var _next_reset + var next_reset : int: + get: + return 0 if not _next_reset is int else int(_next_reset) + + # Operator. + var _operator + var operator : int: + get: + return ApiOperator.values()[0] if not ApiOperator.values().has(_operator) else _operator + + # The UNIX time when the tournament was last reset. A computed value. + var _prev_reset + var prev_reset : int: + get: + return 0 if not _prev_reset is int else int(_prev_reset) + + # The current number of players in the tournament. + var _size + var size : int: + get: + return 0 if not _size is int else int(_size) + + # ASC (0) or DESC (1) sort mode of scores in the tournament. + var _sort_order + var sort_order : int: + get: + return 0 if not _sort_order is int else int(_sort_order) + + # The UNIX time when the tournament start being active. A computed value. + var _start_active + var start_active : int: + get: + return 0 if not _start_active is int else int(_start_active) + + # The UNIX time when the tournament will start. + var _start_time + var start_time : String: + get: + return "" if not _start_time is String else String(_start_time) + + # The title for the tournament. + var _title + var title : String: + get: + return "" if not _title is String else String(_title) + + func _init(p_exception = null): + super(p_exception) + + static func create(p_ns : GDScript, p_dict : Dictionary) -> ApiTournament: + return _safe_ret(NakamaSerializer.deserialize(p_ns, "ApiTournament", p_dict), ApiTournament) as ApiTournament + + func serialize() -> Dictionary: + return NakamaSerializer.serialize(self) + + func _to_string() -> String: + if is_exception(): + return get_exception()._to_string() + var output : String = "" + output += "authoritative: %s, " % _authoritative + output += "can_enter: %s, " % _can_enter + output += "category: %s, " % _category + output += "create_time: %s, " % _create_time + output += "description: %s, " % _description + output += "duration: %s, " % _duration + output += "end_active: %s, " % _end_active + output += "end_time: %s, " % _end_time + output += "id: %s, " % _id + output += "max_num_score: %s, " % _max_num_score + output += "max_size: %s, " % _max_size + output += "metadata: %s, " % _metadata + output += "next_reset: %s, " % _next_reset + output += "operator: %s, " % _operator + output += "prev_reset: %s, " % _prev_reset + output += "size: %s, " % _size + output += "sort_order: %s, " % _sort_order + output += "start_active: %s, " % _start_active + output += "start_time: %s, " % _start_time + output += "title: %s, " % _title + return output + +# A list of tournaments. +class ApiTournamentList extends NakamaAsyncResult: + + const _SCHEMA = { + "cursor": {"name": "_cursor", "type": TYPE_STRING, "required": false}, + "tournaments": {"name": "_tournaments", "type": TYPE_ARRAY, "required": false, "content": "ApiTournament"}, + } + + # A pagination cursor (optional). + var _cursor + var cursor : String: + get: + return "" if not _cursor is String else String(_cursor) + + # The list of tournaments returned. + var _tournaments + var tournaments : Array: + get: + return Array() if not _tournaments is Array else Array(_tournaments) + + func _init(p_exception = null): + super(p_exception) + + static func create(p_ns : GDScript, p_dict : Dictionary) -> ApiTournamentList: + return _safe_ret(NakamaSerializer.deserialize(p_ns, "ApiTournamentList", p_dict), ApiTournamentList) as ApiTournamentList + + func serialize() -> Dictionary: + return NakamaSerializer.serialize(self) + + func _to_string() -> String: + if is_exception(): + return get_exception()._to_string() + var output : String = "" + output += "cursor: %s, " % _cursor + output += "tournaments: %s, " % [_tournaments] + return output + +# A set of tournament records which may be part of a tournament records page or a batch of individual records. +class ApiTournamentRecordList extends NakamaAsyncResult: + + const _SCHEMA = { + "next_cursor": {"name": "_next_cursor", "type": TYPE_STRING, "required": false}, + "owner_records": {"name": "_owner_records", "type": TYPE_ARRAY, "required": false, "content": "ApiLeaderboardRecord"}, + "prev_cursor": {"name": "_prev_cursor", "type": TYPE_STRING, "required": false}, + "records": {"name": "_records", "type": TYPE_ARRAY, "required": false, "content": "ApiLeaderboardRecord"}, + } + + # The cursor to send when retireving the next page (optional). + var _next_cursor + var next_cursor : String: + get: + return "" if not _next_cursor is String else String(_next_cursor) + + # A batched set of tournament records belonging to specified owners. + var _owner_records + var owner_records : Array: + get: + return Array() if not _owner_records is Array else Array(_owner_records) + + # The cursor to send when retrieving the previous page (optional). + var _prev_cursor + var prev_cursor : String: + get: + return "" if not _prev_cursor is String else String(_prev_cursor) + + # A list of tournament records. + var _records + var records : Array: + get: + return Array() if not _records is Array else Array(_records) + + func _init(p_exception = null): + super(p_exception) + + static func create(p_ns : GDScript, p_dict : Dictionary) -> ApiTournamentRecordList: + return _safe_ret(NakamaSerializer.deserialize(p_ns, "ApiTournamentRecordList", p_dict), ApiTournamentRecordList) as ApiTournamentRecordList + + func serialize() -> Dictionary: + return NakamaSerializer.serialize(self) + + func _to_string() -> String: + if is_exception(): + return get_exception()._to_string() + var output : String = "" + output += "next_cursor: %s, " % _next_cursor + output += "owner_records: %s, " % [_owner_records] + output += "prev_cursor: %s, " % _prev_cursor + output += "records: %s, " % [_records] + return output + +# Update a user's account details. +class ApiUpdateAccountRequest extends NakamaAsyncResult: + + const _SCHEMA = { + "avatar_url": {"name": "_avatar_url", "type": TYPE_STRING, "required": false}, + "display_name": {"name": "_display_name", "type": TYPE_STRING, "required": false}, + "lang_tag": {"name": "_lang_tag", "type": TYPE_STRING, "required": false}, + "location": {"name": "_location", "type": TYPE_STRING, "required": false}, + "timezone": {"name": "_timezone", "type": TYPE_STRING, "required": false}, + "username": {"name": "_username", "type": TYPE_STRING, "required": false}, + } + + # A URL for an avatar image. + var _avatar_url + var avatar_url : String: + get: + return "" if not _avatar_url is String else String(_avatar_url) + + # The display name of the user. + var _display_name + var display_name : String: + get: + return "" if not _display_name is String else String(_display_name) + + # The language expected to be a tag which follows the BCP-47 spec. + var _lang_tag + var lang_tag : String: + get: + return "" if not _lang_tag is String else String(_lang_tag) + + # The location set by the user. + var _location + var location : String: + get: + return "" if not _location is String else String(_location) + + # The timezone set by the user. + var _timezone + var timezone : String: + get: + return "" if not _timezone is String else String(_timezone) + + # The username of the user's account. + var _username + var username : String: + get: + return "" if not _username is String else String(_username) + + func _init(p_exception = null): + super(p_exception) + + static func create(p_ns : GDScript, p_dict : Dictionary) -> ApiUpdateAccountRequest: + return _safe_ret(NakamaSerializer.deserialize(p_ns, "ApiUpdateAccountRequest", p_dict), ApiUpdateAccountRequest) as ApiUpdateAccountRequest + + func serialize() -> Dictionary: + return NakamaSerializer.serialize(self) + + func _to_string() -> String: + if is_exception(): + return get_exception()._to_string() + var output : String = "" + output += "avatar_url: %s, " % _avatar_url + output += "display_name: %s, " % _display_name + output += "lang_tag: %s, " % _lang_tag + output += "location: %s, " % _location + output += "timezone: %s, " % _timezone + output += "username: %s, " % _username + return output + +# Update fields in a given group. +class ApiUpdateGroupRequest extends NakamaAsyncResult: + + const _SCHEMA = { + "avatar_url": {"name": "_avatar_url", "type": TYPE_STRING, "required": false}, + "description": {"name": "_description", "type": TYPE_STRING, "required": false}, + "group_id": {"name": "_group_id", "type": TYPE_STRING, "required": false}, + "lang_tag": {"name": "_lang_tag", "type": TYPE_STRING, "required": false}, + "name": {"name": "_name", "type": TYPE_STRING, "required": false}, + "open": {"name": "_open", "type": TYPE_BOOL, "required": false}, + } + + # Avatar URL. + var _avatar_url + var avatar_url : String: + get: + return "" if not _avatar_url is String else String(_avatar_url) + + # Description string. + var _description + var description : String: + get: + return "" if not _description is String else String(_description) + + # The ID of the group to update. + var _group_id + var group_id : String: + get: + return "" if not _group_id is String else String(_group_id) + + # Lang tag. + var _lang_tag + var lang_tag : String: + get: + return "" if not _lang_tag is String else String(_lang_tag) + + # Name. + var _name + var name : String: + get: + return "" if not _name is String else String(_name) + + # Open is true if anyone should be allowed to join, or false if joins must be approved by a group admin. + var _open + var open : bool: + get: + return false if not _open is bool else bool(_open) + + func _init(p_exception = null): + super(p_exception) + + static func create(p_ns : GDScript, p_dict : Dictionary) -> ApiUpdateGroupRequest: + return _safe_ret(NakamaSerializer.deserialize(p_ns, "ApiUpdateGroupRequest", p_dict), ApiUpdateGroupRequest) as ApiUpdateGroupRequest + + func serialize() -> Dictionary: + return NakamaSerializer.serialize(self) + + func _to_string() -> String: + if is_exception(): + return get_exception()._to_string() + var output : String = "" + output += "avatar_url: %s, " % _avatar_url + output += "description: %s, " % _description + output += "group_id: %s, " % _group_id + output += "lang_tag: %s, " % _lang_tag + output += "name: %s, " % _name + output += "open: %s, " % _open + return output + +# A user in the server. +class ApiUser extends NakamaAsyncResult: + + const _SCHEMA = { + "apple_id": {"name": "_apple_id", "type": TYPE_STRING, "required": false}, + "avatar_url": {"name": "_avatar_url", "type": TYPE_STRING, "required": false}, + "create_time": {"name": "_create_time", "type": TYPE_STRING, "required": false}, + "display_name": {"name": "_display_name", "type": TYPE_STRING, "required": false}, + "edge_count": {"name": "_edge_count", "type": TYPE_INT, "required": false}, + "facebook_id": {"name": "_facebook_id", "type": TYPE_STRING, "required": false}, + "facebook_instant_game_id": {"name": "_facebook_instant_game_id", "type": TYPE_STRING, "required": false}, + "gamecenter_id": {"name": "_gamecenter_id", "type": TYPE_STRING, "required": false}, + "google_id": {"name": "_google_id", "type": TYPE_STRING, "required": false}, + "id": {"name": "_id", "type": TYPE_STRING, "required": false}, + "lang_tag": {"name": "_lang_tag", "type": TYPE_STRING, "required": false}, + "location": {"name": "_location", "type": TYPE_STRING, "required": false}, + "metadata": {"name": "_metadata", "type": TYPE_STRING, "required": false}, + "online": {"name": "_online", "type": TYPE_BOOL, "required": false}, + "steam_id": {"name": "_steam_id", "type": TYPE_STRING, "required": false}, + "timezone": {"name": "_timezone", "type": TYPE_STRING, "required": false}, + "update_time": {"name": "_update_time", "type": TYPE_STRING, "required": false}, + "username": {"name": "_username", "type": TYPE_STRING, "required": false}, + } + + # The Apple Sign In ID in the user's account. + var _apple_id + var apple_id : String: + get: + return "" if not _apple_id is String else String(_apple_id) + + # A URL for an avatar image. + var _avatar_url + var avatar_url : String: + get: + return "" if not _avatar_url is String else String(_avatar_url) + + # The UNIX time when the user was created. + var _create_time + var create_time : String: + get: + return "" if not _create_time is String else String(_create_time) + + # The display name of the user. + var _display_name + var display_name : String: + get: + return "" if not _display_name is String else String(_display_name) + + # Number of related edges to this user. + var _edge_count + var edge_count : int: + get: + return 0 if not _edge_count is int else int(_edge_count) + + # The Facebook id in the user's account. + var _facebook_id + var facebook_id : String: + get: + return "" if not _facebook_id is String else String(_facebook_id) + + # The Facebook Instant Game ID in the user's account. + var _facebook_instant_game_id + var facebook_instant_game_id : String: + get: + return "" if not _facebook_instant_game_id is String else String(_facebook_instant_game_id) + + # The Apple Game Center in of the user's account. + var _gamecenter_id + var gamecenter_id : String: + get: + return "" if not _gamecenter_id is String else String(_gamecenter_id) + + # The Google id in the user's account. + var _google_id + var google_id : String: + get: + return "" if not _google_id is String else String(_google_id) + + # The id of the user's account. + var _id + var id : String: + get: + return "" if not _id is String else String(_id) + + # The language expected to be a tag which follows the BCP-47 spec. + var _lang_tag + var lang_tag : String: + get: + return "" if not _lang_tag is String else String(_lang_tag) + + # The location set by the user. + var _location + var location : String: + get: + return "" if not _location is String else String(_location) + + # Additional information stored as a JSON object. + var _metadata + var metadata : String: + get: + return "" if not _metadata is String else String(_metadata) + + # Indicates whether the user is currently online. + var _online + var online : bool: + get: + return false if not _online is bool else bool(_online) + + # The Steam id in the user's account. + var _steam_id + var steam_id : String: + get: + return "" if not _steam_id is String else String(_steam_id) + + # The timezone set by the user. + var _timezone + var timezone : String: + get: + return "" if not _timezone is String else String(_timezone) + + # The UNIX time when the user was last updated. + var _update_time + var update_time : String: + get: + return "" if not _update_time is String else String(_update_time) + + # The username of the user's account. + var _username + var username : String: + get: + return "" if not _username is String else String(_username) + + func _init(p_exception = null): + super(p_exception) + + static func create(p_ns : GDScript, p_dict : Dictionary) -> ApiUser: + return _safe_ret(NakamaSerializer.deserialize(p_ns, "ApiUser", p_dict), ApiUser) as ApiUser + + func serialize() -> Dictionary: + return NakamaSerializer.serialize(self) + + func _to_string() -> String: + if is_exception(): + return get_exception()._to_string() + var output : String = "" + output += "apple_id: %s, " % _apple_id + output += "avatar_url: %s, " % _avatar_url + output += "create_time: %s, " % _create_time + output += "display_name: %s, " % _display_name + output += "edge_count: %s, " % _edge_count + output += "facebook_id: %s, " % _facebook_id + output += "facebook_instant_game_id: %s, " % _facebook_instant_game_id + output += "gamecenter_id: %s, " % _gamecenter_id + output += "google_id: %s, " % _google_id + output += "id: %s, " % _id + output += "lang_tag: %s, " % _lang_tag + output += "location: %s, " % _location + output += "metadata: %s, " % _metadata + output += "online: %s, " % _online + output += "steam_id: %s, " % _steam_id + output += "timezone: %s, " % _timezone + output += "update_time: %s, " % _update_time + output += "username: %s, " % _username + return output + +# A list of groups belonging to a user, along with the user's role in each group. +class ApiUserGroupList extends NakamaAsyncResult: + + const _SCHEMA = { + "cursor": {"name": "_cursor", "type": TYPE_STRING, "required": false}, + "user_groups": {"name": "_user_groups", "type": TYPE_ARRAY, "required": false, "content": "UserGroupListUserGroup"}, + } + + # Cursor for the next page of results, if any. + var _cursor + var cursor : String: + get: + return "" if not _cursor is String else String(_cursor) + + # Group-role pairs for a user. + var _user_groups + var user_groups : Array: + get: + return Array() if not _user_groups is Array else Array(_user_groups) + + func _init(p_exception = null): + super(p_exception) + + static func create(p_ns : GDScript, p_dict : Dictionary) -> ApiUserGroupList: + return _safe_ret(NakamaSerializer.deserialize(p_ns, "ApiUserGroupList", p_dict), ApiUserGroupList) as ApiUserGroupList + + func serialize() -> Dictionary: + return NakamaSerializer.serialize(self) + + func _to_string() -> String: + if is_exception(): + return get_exception()._to_string() + var output : String = "" + output += "cursor: %s, " % _cursor + output += "user_groups: %s, " % [_user_groups] + return output + +# A collection of zero or more users. +class ApiUsers extends NakamaAsyncResult: + + const _SCHEMA = { + "users": {"name": "_users", "type": TYPE_ARRAY, "required": false, "content": "ApiUser"}, + } + + # The User objects. + var _users + var users : Array: + get: + return Array() if not _users is Array else Array(_users) + + func _init(p_exception = null): + super(p_exception) + + static func create(p_ns : GDScript, p_dict : Dictionary) -> ApiUsers: + return _safe_ret(NakamaSerializer.deserialize(p_ns, "ApiUsers", p_dict), ApiUsers) as ApiUsers + + func serialize() -> Dictionary: + return NakamaSerializer.serialize(self) + + func _to_string() -> String: + if is_exception(): + return get_exception()._to_string() + var output : String = "" + output += "users: %s, " % [_users] + return output + +# +class ApiValidatePurchaseAppleRequest extends NakamaAsyncResult: + + const _SCHEMA = { + "persist": {"name": "_persist", "type": TYPE_BOOL, "required": false}, + "receipt": {"name": "_receipt", "type": TYPE_STRING, "required": false}, + } + + # + var _persist + var persist : bool: + get: + return false if not _persist is bool else bool(_persist) + + # Base64 encoded Apple receipt data payload. + var _receipt + var receipt : String: + get: + return "" if not _receipt is String else String(_receipt) + + func _init(p_exception = null): + super(p_exception) + + static func create(p_ns : GDScript, p_dict : Dictionary) -> ApiValidatePurchaseAppleRequest: + return _safe_ret(NakamaSerializer.deserialize(p_ns, "ApiValidatePurchaseAppleRequest", p_dict), ApiValidatePurchaseAppleRequest) as ApiValidatePurchaseAppleRequest + + func serialize() -> Dictionary: + return NakamaSerializer.serialize(self) + + func _to_string() -> String: + if is_exception(): + return get_exception()._to_string() + var output : String = "" + output += "persist: %s, " % _persist + output += "receipt: %s, " % _receipt + return output + +# +class ApiValidatePurchaseGoogleRequest extends NakamaAsyncResult: + + const _SCHEMA = { + "persist": {"name": "_persist", "type": TYPE_BOOL, "required": false}, + "purchase": {"name": "_purchase", "type": TYPE_STRING, "required": false}, + } + + # + var _persist + var persist : bool: + get: + return false if not _persist is bool else bool(_persist) + + # JSON encoded Google purchase payload. + var _purchase + var purchase : String: + get: + return "" if not _purchase is String else String(_purchase) + + func _init(p_exception = null): + super(p_exception) + + static func create(p_ns : GDScript, p_dict : Dictionary) -> ApiValidatePurchaseGoogleRequest: + return _safe_ret(NakamaSerializer.deserialize(p_ns, "ApiValidatePurchaseGoogleRequest", p_dict), ApiValidatePurchaseGoogleRequest) as ApiValidatePurchaseGoogleRequest + + func serialize() -> Dictionary: + return NakamaSerializer.serialize(self) + + func _to_string() -> String: + if is_exception(): + return get_exception()._to_string() + var output : String = "" + output += "persist: %s, " % _persist + output += "purchase: %s, " % _purchase + return output + +# +class ApiValidatePurchaseHuaweiRequest extends NakamaAsyncResult: + + const _SCHEMA = { + "persist": {"name": "_persist", "type": TYPE_BOOL, "required": false}, + "purchase": {"name": "_purchase", "type": TYPE_STRING, "required": false}, + "signature": {"name": "_signature", "type": TYPE_STRING, "required": false}, + } + + # + var _persist + var persist : bool: + get: + return false if not _persist is bool else bool(_persist) + + # JSON encoded Huawei InAppPurchaseData. + var _purchase + var purchase : String: + get: + return "" if not _purchase is String else String(_purchase) + + # InAppPurchaseData signature. + var _signature + var signature : String: + get: + return "" if not _signature is String else String(_signature) + + func _init(p_exception = null): + super(p_exception) + + static func create(p_ns : GDScript, p_dict : Dictionary) -> ApiValidatePurchaseHuaweiRequest: + return _safe_ret(NakamaSerializer.deserialize(p_ns, "ApiValidatePurchaseHuaweiRequest", p_dict), ApiValidatePurchaseHuaweiRequest) as ApiValidatePurchaseHuaweiRequest + + func serialize() -> Dictionary: + return NakamaSerializer.serialize(self) + + func _to_string() -> String: + if is_exception(): + return get_exception()._to_string() + var output : String = "" + output += "persist: %s, " % _persist + output += "purchase: %s, " % _purchase + output += "signature: %s, " % _signature + return output + +# Validate IAP response. +class ApiValidatePurchaseResponse extends NakamaAsyncResult: + + const _SCHEMA = { + "validated_purchases": {"name": "_validated_purchases", "type": TYPE_ARRAY, "required": false, "content": "ApiValidatedPurchase"}, + } + + # Newly seen validated purchases. + var _validated_purchases + var validated_purchases : Array: + get: + return Array() if not _validated_purchases is Array else Array(_validated_purchases) + + func _init(p_exception = null): + super(p_exception) + + static func create(p_ns : GDScript, p_dict : Dictionary) -> ApiValidatePurchaseResponse: + return _safe_ret(NakamaSerializer.deserialize(p_ns, "ApiValidatePurchaseResponse", p_dict), ApiValidatePurchaseResponse) as ApiValidatePurchaseResponse + + func serialize() -> Dictionary: + return NakamaSerializer.serialize(self) + + func _to_string() -> String: + if is_exception(): + return get_exception()._to_string() + var output : String = "" + output += "validated_purchases: %s, " % [_validated_purchases] + return output + +# +class ApiValidateSubscriptionAppleRequest extends NakamaAsyncResult: + + const _SCHEMA = { + "persist": {"name": "_persist", "type": TYPE_BOOL, "required": false}, + "receipt": {"name": "_receipt", "type": TYPE_STRING, "required": false}, + } + + # Persist the subscription. + var _persist + var persist : bool: + get: + return false if not _persist is bool else bool(_persist) + + # Base64 encoded Apple receipt data payload. + var _receipt + var receipt : String: + get: + return "" if not _receipt is String else String(_receipt) + + func _init(p_exception = null): + super(p_exception) + + static func create(p_ns : GDScript, p_dict : Dictionary) -> ApiValidateSubscriptionAppleRequest: + return _safe_ret(NakamaSerializer.deserialize(p_ns, "ApiValidateSubscriptionAppleRequest", p_dict), ApiValidateSubscriptionAppleRequest) as ApiValidateSubscriptionAppleRequest + + func serialize() -> Dictionary: + return NakamaSerializer.serialize(self) + + func _to_string() -> String: + if is_exception(): + return get_exception()._to_string() + var output : String = "" + output += "persist: %s, " % _persist + output += "receipt: %s, " % _receipt + return output + +# +class ApiValidateSubscriptionGoogleRequest extends NakamaAsyncResult: + + const _SCHEMA = { + "persist": {"name": "_persist", "type": TYPE_BOOL, "required": false}, + "receipt": {"name": "_receipt", "type": TYPE_STRING, "required": false}, + } + + # Persist the subscription. + var _persist + var persist : bool: + get: + return false if not _persist is bool else bool(_persist) + + # JSON encoded Google purchase payload. + var _receipt + var receipt : String: + get: + return "" if not _receipt is String else String(_receipt) + + func _init(p_exception = null): + super(p_exception) + + static func create(p_ns : GDScript, p_dict : Dictionary) -> ApiValidateSubscriptionGoogleRequest: + return _safe_ret(NakamaSerializer.deserialize(p_ns, "ApiValidateSubscriptionGoogleRequest", p_dict), ApiValidateSubscriptionGoogleRequest) as ApiValidateSubscriptionGoogleRequest + + func serialize() -> Dictionary: + return NakamaSerializer.serialize(self) + + func _to_string() -> String: + if is_exception(): + return get_exception()._to_string() + var output : String = "" + output += "persist: %s, " % _persist + output += "receipt: %s, " % _receipt + return output + +# Validate Subscription response. +class ApiValidateSubscriptionResponse extends NakamaAsyncResult: + + const _SCHEMA = { + "validated_subscription": {"name": "_validated_subscription", "type": "ApiValidatedSubscription", "required": false}, + } + + # + var _validated_subscription + var validated_subscription : ApiValidatedSubscription: + get: + return _validated_subscription as ApiValidatedSubscription + + func _init(p_exception = null): + super(p_exception) + + static func create(p_ns : GDScript, p_dict : Dictionary) -> ApiValidateSubscriptionResponse: + return _safe_ret(NakamaSerializer.deserialize(p_ns, "ApiValidateSubscriptionResponse", p_dict), ApiValidateSubscriptionResponse) as ApiValidateSubscriptionResponse + + func serialize() -> Dictionary: + return NakamaSerializer.serialize(self) + + func _to_string() -> String: + if is_exception(): + return get_exception()._to_string() + var output : String = "" + output += "validated_subscription: %s, " % _validated_subscription + return output + +# Validated Purchase stored by Nakama. +class ApiValidatedPurchase extends NakamaAsyncResult: + + const _SCHEMA = { + "create_time": {"name": "_create_time", "type": TYPE_STRING, "required": false}, + "environment": {"name": "_environment", "type": TYPE_INT, "required": false}, + "product_id": {"name": "_product_id", "type": TYPE_STRING, "required": false}, + "provider_response": {"name": "_provider_response", "type": TYPE_STRING, "required": false}, + "purchase_time": {"name": "_purchase_time", "type": TYPE_STRING, "required": false}, + "refund_time": {"name": "_refund_time", "type": TYPE_STRING, "required": false}, + "seen_before": {"name": "_seen_before", "type": TYPE_BOOL, "required": false}, + "store": {"name": "_store", "type": TYPE_INT, "required": false}, + "transaction_id": {"name": "_transaction_id", "type": TYPE_STRING, "required": false}, + "update_time": {"name": "_update_time", "type": TYPE_STRING, "required": false}, + "user_id": {"name": "_user_id", "type": TYPE_STRING, "required": false}, + } + + # Timestamp when the receipt validation was stored in DB. + var _create_time + var create_time : String: + get: + return "" if not _create_time is String else String(_create_time) + + # Whether the purchase was done in production or sandbox environment. + var _environment + var environment : int: + get: + return ApiStoreEnvironment.values()[0] if not ApiStoreEnvironment.values().has(_environment) else _environment + + # Purchase Product ID. + var _product_id + var product_id : String: + get: + return "" if not _product_id is String else String(_product_id) + + # Raw provider validation response. + var _provider_response + var provider_response : String: + get: + return "" if not _provider_response is String else String(_provider_response) + + # Timestamp when the purchase was done. + var _purchase_time + var purchase_time : String: + get: + return "" if not _purchase_time is String else String(_purchase_time) + + # + var _refund_time + var refund_time : String: + get: + return "" if not _refund_time is String else String(_refund_time) + + # Whether the purchase had already been validated by Nakama before. + var _seen_before + var seen_before : bool: + get: + return false if not _seen_before is bool else bool(_seen_before) + + # + var _store + var store : int: + get: + return ApiStoreProvider.values()[0] if not ApiStoreProvider.values().has(_store) else _store + + # Purchase Transaction ID. + var _transaction_id + var transaction_id : String: + get: + return "" if not _transaction_id is String else String(_transaction_id) + + # Timestamp when the receipt validation was updated in DB. + var _update_time + var update_time : String: + get: + return "" if not _update_time is String else String(_update_time) + + # Purchase User ID. + var _user_id + var user_id : String: + get: + return "" if not _user_id is String else String(_user_id) + + func _init(p_exception = null): + super(p_exception) + + static func create(p_ns : GDScript, p_dict : Dictionary) -> ApiValidatedPurchase: + return _safe_ret(NakamaSerializer.deserialize(p_ns, "ApiValidatedPurchase", p_dict), ApiValidatedPurchase) as ApiValidatedPurchase + + func serialize() -> Dictionary: + return NakamaSerializer.serialize(self) + + func _to_string() -> String: + if is_exception(): + return get_exception()._to_string() + var output : String = "" + output += "create_time: %s, " % _create_time + output += "environment: %s, " % _environment + output += "product_id: %s, " % _product_id + output += "provider_response: %s, " % _provider_response + output += "purchase_time: %s, " % _purchase_time + output += "refund_time: %s, " % _refund_time + output += "seen_before: %s, " % _seen_before + output += "store: %s, " % _store + output += "transaction_id: %s, " % _transaction_id + output += "update_time: %s, " % _update_time + output += "user_id: %s, " % _user_id + return output + +# +class ApiValidatedSubscription extends NakamaAsyncResult: + + const _SCHEMA = { + "active": {"name": "_active", "type": TYPE_BOOL, "required": false}, + "create_time": {"name": "_create_time", "type": TYPE_STRING, "required": false}, + "environment": {"name": "_environment", "type": TYPE_INT, "required": false}, + "expiry_time": {"name": "_expiry_time", "type": TYPE_STRING, "required": false}, + "original_transaction_id": {"name": "_original_transaction_id", "type": TYPE_STRING, "required": false}, + "product_id": {"name": "_product_id", "type": TYPE_STRING, "required": false}, + "provider_notification": {"name": "_provider_notification", "type": TYPE_STRING, "required": false}, + "provider_response": {"name": "_provider_response", "type": TYPE_STRING, "required": false}, + "purchase_time": {"name": "_purchase_time", "type": TYPE_STRING, "required": false}, + "refund_time": {"name": "_refund_time", "type": TYPE_STRING, "required": false}, + "store": {"name": "_store", "type": TYPE_INT, "required": false}, + "update_time": {"name": "_update_time", "type": TYPE_STRING, "required": false}, + "user_id": {"name": "_user_id", "type": TYPE_STRING, "required": false}, + } + + # Whether the subscription is currently active or not. + var _active + var active : bool: + get: + return false if not _active is bool else bool(_active) + + # UNIX Timestamp when the receipt validation was stored in DB. + var _create_time + var create_time : String: + get: + return "" if not _create_time is String else String(_create_time) + + # Whether the purchase was done in production or sandbox environment. + var _environment + var environment : int: + get: + return ApiStoreEnvironment.values()[0] if not ApiStoreEnvironment.values().has(_environment) else _environment + + # Subscription expiration time. The subscription can still be auto-renewed to extend the expiration time further. + var _expiry_time + var expiry_time : String: + get: + return "" if not _expiry_time is String else String(_expiry_time) + + # Purchase Original transaction ID (we only keep track of the original subscription, not subsequent renewals). + var _original_transaction_id + var original_transaction_id : String: + get: + return "" if not _original_transaction_id is String else String(_original_transaction_id) + + # Purchase Product ID. + var _product_id + var product_id : String: + get: + return "" if not _product_id is String else String(_product_id) + + # Raw provider notification body. + var _provider_notification + var provider_notification : String: + get: + return "" if not _provider_notification is String else String(_provider_notification) + + # Raw provider validation response body. + var _provider_response + var provider_response : String: + get: + return "" if not _provider_response is String else String(_provider_response) + + # UNIX Timestamp when the purchase was done. + var _purchase_time + var purchase_time : String: + get: + return "" if not _purchase_time is String else String(_purchase_time) + + # Subscription refund time. If this time is set, the subscription was refunded. + var _refund_time + var refund_time : String: + get: + return "" if not _refund_time is String else String(_refund_time) + + # + var _store + var store : int: + get: + return ApiStoreProvider.values()[0] if not ApiStoreProvider.values().has(_store) else _store + + # UNIX Timestamp when the receipt validation was updated in DB. + var _update_time + var update_time : String: + get: + return "" if not _update_time is String else String(_update_time) + + # Subscription User ID. + var _user_id + var user_id : String: + get: + return "" if not _user_id is String else String(_user_id) + + func _init(p_exception = null): + super(p_exception) + + static func create(p_ns : GDScript, p_dict : Dictionary) -> ApiValidatedSubscription: + return _safe_ret(NakamaSerializer.deserialize(p_ns, "ApiValidatedSubscription", p_dict), ApiValidatedSubscription) as ApiValidatedSubscription + + func serialize() -> Dictionary: + return NakamaSerializer.serialize(self) + + func _to_string() -> String: + if is_exception(): + return get_exception()._to_string() + var output : String = "" + output += "active: %s, " % _active + output += "create_time: %s, " % _create_time + output += "environment: %s, " % _environment + output += "expiry_time: %s, " % _expiry_time + output += "original_transaction_id: %s, " % _original_transaction_id + output += "product_id: %s, " % _product_id + output += "provider_notification: %s, " % _provider_notification + output += "provider_response: %s, " % _provider_response + output += "purchase_time: %s, " % _purchase_time + output += "refund_time: %s, " % _refund_time + output += "store: %s, " % _store + output += "update_time: %s, " % _update_time + output += "user_id: %s, " % _user_id + return output + +# The object to store. +class ApiWriteStorageObject extends NakamaAsyncResult: + + const _SCHEMA = { + "collection": {"name": "_collection", "type": TYPE_STRING, "required": false}, + "key": {"name": "_key", "type": TYPE_STRING, "required": false}, + "permission_read": {"name": "_permission_read", "type": TYPE_INT, "required": false}, + "permission_write": {"name": "_permission_write", "type": TYPE_INT, "required": false}, + "value": {"name": "_value", "type": TYPE_STRING, "required": false}, + "version": {"name": "_version", "type": TYPE_STRING, "required": false}, + } + + # The collection to store the object. + var _collection + var collection : String: + get: + return "" if not _collection is String else String(_collection) + + # The key for the object within the collection. + var _key + var key : String: + get: + return "" if not _key is String else String(_key) + + # The read access permissions for the object. + var _permission_read + var permission_read : int: + get: + return 0 if not _permission_read is int else int(_permission_read) + + # The write access permissions for the object. + var _permission_write + var permission_write : int: + get: + return 0 if not _permission_write is int else int(_permission_write) + + # The value of the object. + var _value + var value : String: + get: + return "" if not _value is String else String(_value) + + # The version hash of the object to check. Possible values are: ["", "*", "#hash#"]. + var _version + var version : String: + get: + return "" if not _version is String else String(_version) + + func _init(p_exception = null): + super(p_exception) + + static func create(p_ns : GDScript, p_dict : Dictionary) -> ApiWriteStorageObject: + return _safe_ret(NakamaSerializer.deserialize(p_ns, "ApiWriteStorageObject", p_dict), ApiWriteStorageObject) as ApiWriteStorageObject + + func serialize() -> Dictionary: + return NakamaSerializer.serialize(self) + + func _to_string() -> String: + if is_exception(): + return get_exception()._to_string() + var output : String = "" + output += "collection: %s, " % _collection + output += "key: %s, " % _key + output += "permission_read: %s, " % _permission_read + output += "permission_write: %s, " % _permission_write + output += "value: %s, " % _value + output += "version: %s, " % _version + return output + +# Write objects to the storage engine. +class ApiWriteStorageObjectsRequest extends NakamaAsyncResult: + + const _SCHEMA = { + "objects": {"name": "_objects", "type": TYPE_ARRAY, "required": false, "content": "ApiWriteStorageObject"}, + } + + # The objects to store on the server. + var _objects + var objects : Array: + get: + return Array() if not _objects is Array else Array(_objects) + + func _init(p_exception = null): + super(p_exception) + + static func create(p_ns : GDScript, p_dict : Dictionary) -> ApiWriteStorageObjectsRequest: + return _safe_ret(NakamaSerializer.deserialize(p_ns, "ApiWriteStorageObjectsRequest", p_dict), ApiWriteStorageObjectsRequest) as ApiWriteStorageObjectsRequest + + func serialize() -> Dictionary: + return NakamaSerializer.serialize(self) + + func _to_string() -> String: + if is_exception(): + return get_exception()._to_string() + var output : String = "" + output += "objects: %s, " % [_objects] + return output + +# +class ProtobufAny extends NakamaAsyncResult: + + const _SCHEMA = { + "type_url": {"name": "_type_url", "type": TYPE_STRING, "required": false}, + "value": {"name": "_value", "type": TYPE_STRING, "required": false}, + } + + # + var _type_url + var type_url : String: + get: + return "" if not _type_url is String else String(_type_url) + + # + var _value + var value : String: + get: + return "" if not _value is String else String(_value) + + func _init(p_exception = null): + super(p_exception) + + static func create(p_ns : GDScript, p_dict : Dictionary) -> ProtobufAny: + return _safe_ret(NakamaSerializer.deserialize(p_ns, "ProtobufAny", p_dict), ProtobufAny) as ProtobufAny + + func serialize() -> Dictionary: + return NakamaSerializer.serialize(self) + + func _to_string() -> String: + if is_exception(): + return get_exception()._to_string() + var output : String = "" + output += "type_url: %s, " % _type_url + output += "value: %s, " % _value + return output + +# +class RpcStatus extends NakamaAsyncResult: + + const _SCHEMA = { + "code": {"name": "_code", "type": TYPE_INT, "required": false}, + "details": {"name": "_details", "type": TYPE_ARRAY, "required": false, "content": "ProtobufAny"}, + "message": {"name": "_message", "type": TYPE_STRING, "required": false}, + } + + # + var _code + var code : int: + get: + return 0 if not _code is int else int(_code) + + # + var _details + var details : Array: + get: + return Array() if not _details is Array else Array(_details) + + # + var _message + var message : String: + get: + return "" if not _message is String else String(_message) + + func _init(p_exception = null): + super(p_exception) + + static func create(p_ns : GDScript, p_dict : Dictionary) -> RpcStatus: + return _safe_ret(NakamaSerializer.deserialize(p_ns, "RpcStatus", p_dict), RpcStatus) as RpcStatus + + func serialize() -> Dictionary: + return NakamaSerializer.serialize(self) + + func _to_string() -> String: + if is_exception(): + return get_exception()._to_string() + var output : String = "" + output += "code: %s, " % _code + output += "details: %s, " % [_details] + output += "message: %s, " % _message + return output + +# The low level client for the Nakama API. +class ApiClient extends RefCounted: + + var _base_uri : String + + var _http_adapter + var _namespace : GDScript + var _server_key : String + var auto_refresh := true + var auto_refresh_time := 300 + + var auto_retry : bool: + set(p_value): + _http_adapter.auto_retry = p_value + get: + return _http_adapter.auto_retry + + var auto_retry_count : int: + set(p_value): + _http_adapter.auto_retry_count = p_value + get: + return _http_adapter.auto_retry_count + + var auto_retry_backoff_base : int: + set(p_value): + _http_adapter.auto_retry_backoff_base = p_value + get: + return _http_adapter.auto_retry_backoff_base + + var last_cancel_token: + get: + return _http_adapter.get_last_token() + + func _init(p_base_uri : String, p_http_adapter, p_namespace : GDScript, p_server_key : String, p_timeout : int = 10): + _base_uri = p_base_uri + _http_adapter = p_http_adapter + _http_adapter.timeout = p_timeout + _namespace = p_namespace + _server_key = p_server_key + + func _refresh_session(p_session : NakamaSession): + if auto_refresh and p_session.is_valid() and p_session.refresh_token and not p_session.is_refresh_expired() and p_session.would_expire_in(auto_refresh_time): + var request = ApiSessionRefreshRequest.new() + request._token = p_session.refresh_token + return await session_refresh_async(_server_key, "", request) + return null + + func cancel_request(p_token): + if p_token: + _http_adapter.cancel_request(p_token) + + # A healthcheck which load balancers can use to check the service. + func healthcheck_async( + p_session : NakamaSession + ) -> NakamaAsyncResult: + var try_refresh = await _refresh_session(p_session) + if try_refresh != null: + if try_refresh.is_exception(): + return NakamaAsyncResult.new(try_refresh.get_exception()) + await p_session.refresh(try_refresh) + var urlpath : String = "/healthcheck" + var query_params = "" + var uri = "%s%s%s" % [_base_uri, urlpath, "?" + query_params if query_params else ""] + var method = "GET" + var headers = {} + var header = "Bearer %s" % p_session.token + headers["Authorization"] = header + + var content : PackedByteArray + + var result = await _http_adapter.send_async(method, uri, headers, content) + if result is NakamaException: + return NakamaAsyncResult.new(result) + return NakamaAsyncResult.new() + + # Delete the current user's account. + func delete_account_async( + p_session : NakamaSession + ) -> NakamaAsyncResult: + var try_refresh = await _refresh_session(p_session) + if try_refresh != null: + if try_refresh.is_exception(): + return NakamaAsyncResult.new(try_refresh.get_exception()) + await p_session.refresh(try_refresh) + var urlpath : String = "/v2/account" + var query_params = "" + var uri = "%s%s%s" % [_base_uri, urlpath, "?" + query_params if query_params else ""] + var method = "DELETE" + var headers = {} + var header = "Bearer %s" % p_session.token + headers["Authorization"] = header + + var content : PackedByteArray + + var result = await _http_adapter.send_async(method, uri, headers, content) + if result is NakamaException: + return NakamaAsyncResult.new(result) + return NakamaAsyncResult.new() + + # Fetch the current user's account. + func get_account_async( + p_session : NakamaSession + ) -> ApiAccount: + var try_refresh = await _refresh_session(p_session) + if try_refresh != null: + if try_refresh.is_exception(): + return ApiAccount.new(try_refresh.get_exception()) + await p_session.refresh(try_refresh) + var urlpath : String = "/v2/account" + var query_params = "" + var uri = "%s%s%s" % [_base_uri, urlpath, "?" + query_params if query_params else ""] + var method = "GET" + var headers = {} + var header = "Bearer %s" % p_session.token + headers["Authorization"] = header + + var content : PackedByteArray + + var result = await _http_adapter.send_async(method, uri, headers, content) + if result is NakamaException: + return ApiAccount.new(result) + var out : ApiAccount = NakamaSerializer.deserialize(_namespace, "ApiAccount", result) + return out + + # Update fields in the current user's account. + func update_account_async( + p_session : NakamaSession + , p_body : ApiUpdateAccountRequest + ) -> NakamaAsyncResult: + var try_refresh = await _refresh_session(p_session) + if try_refresh != null: + if try_refresh.is_exception(): + return NakamaAsyncResult.new(try_refresh.get_exception()) + await p_session.refresh(try_refresh) + var urlpath : String = "/v2/account" + var query_params = "" + var uri = "%s%s%s" % [_base_uri, urlpath, "?" + query_params if query_params else ""] + var method = "PUT" + var headers = {} + var header = "Bearer %s" % p_session.token + headers["Authorization"] = header + + var content : PackedByteArray + content = JSON.stringify(p_body.serialize()).to_utf8_buffer() + + var result = await _http_adapter.send_async(method, uri, headers, content) + if result is NakamaException: + return NakamaAsyncResult.new(result) + return NakamaAsyncResult.new() + + # Authenticate a user with an Apple ID against the server. + func authenticate_apple_async( + p_basic_auth_username : String + , p_basic_auth_password : String + , p_account : ApiAccountApple + , p_create = null # : boolean + , p_username = null # : string + ) -> ApiSession: + var urlpath : String = "/v2/account/authenticate/apple" + var query_params = "" + if p_create != null: + query_params += "create=%s&" % str(bool(p_create)).to_lower() + if p_username != null: + query_params += "username=%s&" % NakamaSerializer.escape_http(p_username) + var uri = "%s%s%s" % [_base_uri, urlpath, "?" + query_params if query_params else ""] + var method = "POST" + var headers = {} + var credentials = Marshalls.utf8_to_base64(p_basic_auth_username + ":" + p_basic_auth_password) + var header = "Basic %s" % credentials + headers["Authorization"] = header + + var content : PackedByteArray + content = JSON.stringify(p_account.serialize()).to_utf8_buffer() + + var result = await _http_adapter.send_async(method, uri, headers, content) + if result is NakamaException: + return ApiSession.new(result) + var out : ApiSession = NakamaSerializer.deserialize(_namespace, "ApiSession", result) + return out + + # Authenticate a user with a custom id against the server. + func authenticate_custom_async( + p_basic_auth_username : String + , p_basic_auth_password : String + , p_account : ApiAccountCustom + , p_create = null # : boolean + , p_username = null # : string + ) -> ApiSession: + var urlpath : String = "/v2/account/authenticate/custom" + var query_params = "" + if p_create != null: + query_params += "create=%s&" % str(bool(p_create)).to_lower() + if p_username != null: + query_params += "username=%s&" % NakamaSerializer.escape_http(p_username) + var uri = "%s%s%s" % [_base_uri, urlpath, "?" + query_params if query_params else ""] + var method = "POST" + var headers = {} + var credentials = Marshalls.utf8_to_base64(p_basic_auth_username + ":" + p_basic_auth_password) + var header = "Basic %s" % credentials + headers["Authorization"] = header + + var content : PackedByteArray + content = JSON.stringify(p_account.serialize()).to_utf8_buffer() + + var result = await _http_adapter.send_async(method, uri, headers, content) + if result is NakamaException: + return ApiSession.new(result) + var out : ApiSession = NakamaSerializer.deserialize(_namespace, "ApiSession", result) + return out + + # Authenticate a user with a device id against the server. + func authenticate_device_async( + p_basic_auth_username : String + , p_basic_auth_password : String + , p_account : ApiAccountDevice + , p_create = null # : boolean + , p_username = null # : string + ) -> ApiSession: + var urlpath : String = "/v2/account/authenticate/device" + var query_params = "" + if p_create != null: + query_params += "create=%s&" % str(bool(p_create)).to_lower() + if p_username != null: + query_params += "username=%s&" % NakamaSerializer.escape_http(p_username) + var uri = "%s%s%s" % [_base_uri, urlpath, "?" + query_params if query_params else ""] + var method = "POST" + var headers = {} + var credentials = Marshalls.utf8_to_base64(p_basic_auth_username + ":" + p_basic_auth_password) + var header = "Basic %s" % credentials + headers["Authorization"] = header + + var content : PackedByteArray + content = JSON.stringify(p_account.serialize()).to_utf8_buffer() + + var result = await _http_adapter.send_async(method, uri, headers, content) + if result is NakamaException: + return ApiSession.new(result) + var out : ApiSession = NakamaSerializer.deserialize(_namespace, "ApiSession", result) + return out + + # Authenticate a user with an email+password against the server. + func authenticate_email_async( + p_basic_auth_username : String + , p_basic_auth_password : String + , p_account : ApiAccountEmail + , p_create = null # : boolean + , p_username = null # : string + ) -> ApiSession: + var urlpath : String = "/v2/account/authenticate/email" + var query_params = "" + if p_create != null: + query_params += "create=%s&" % str(bool(p_create)).to_lower() + if p_username != null: + query_params += "username=%s&" % NakamaSerializer.escape_http(p_username) + var uri = "%s%s%s" % [_base_uri, urlpath, "?" + query_params if query_params else ""] + var method = "POST" + var headers = {} + var credentials = Marshalls.utf8_to_base64(p_basic_auth_username + ":" + p_basic_auth_password) + var header = "Basic %s" % credentials + headers["Authorization"] = header + + var content : PackedByteArray + content = JSON.stringify(p_account.serialize()).to_utf8_buffer() + + var result = await _http_adapter.send_async(method, uri, headers, content) + if result is NakamaException: + return ApiSession.new(result) + var out : ApiSession = NakamaSerializer.deserialize(_namespace, "ApiSession", result) + return out + + # Authenticate a user with a Facebook OAuth token against the server. + func authenticate_facebook_async( + p_basic_auth_username : String + , p_basic_auth_password : String + , p_account : ApiAccountFacebook + , p_create = null # : boolean + , p_username = null # : string + , p_sync = null # : boolean + ) -> ApiSession: + var urlpath : String = "/v2/account/authenticate/facebook" + var query_params = "" + if p_create != null: + query_params += "create=%s&" % str(bool(p_create)).to_lower() + if p_username != null: + query_params += "username=%s&" % NakamaSerializer.escape_http(p_username) + if p_sync != null: + query_params += "sync=%s&" % str(bool(p_sync)).to_lower() + var uri = "%s%s%s" % [_base_uri, urlpath, "?" + query_params if query_params else ""] + var method = "POST" + var headers = {} + var credentials = Marshalls.utf8_to_base64(p_basic_auth_username + ":" + p_basic_auth_password) + var header = "Basic %s" % credentials + headers["Authorization"] = header + + var content : PackedByteArray + content = JSON.stringify(p_account.serialize()).to_utf8_buffer() + + var result = await _http_adapter.send_async(method, uri, headers, content) + if result is NakamaException: + return ApiSession.new(result) + var out : ApiSession = NakamaSerializer.deserialize(_namespace, "ApiSession", result) + return out + + # Authenticate a user with a Facebook Instant Game token against the server. + func authenticate_facebook_instant_game_async( + p_basic_auth_username : String + , p_basic_auth_password : String + , p_account : ApiAccountFacebookInstantGame + , p_create = null # : boolean + , p_username = null # : string + ) -> ApiSession: + var urlpath : String = "/v2/account/authenticate/facebookinstantgame" + var query_params = "" + if p_create != null: + query_params += "create=%s&" % str(bool(p_create)).to_lower() + if p_username != null: + query_params += "username=%s&" % NakamaSerializer.escape_http(p_username) + var uri = "%s%s%s" % [_base_uri, urlpath, "?" + query_params if query_params else ""] + var method = "POST" + var headers = {} + var credentials = Marshalls.utf8_to_base64(p_basic_auth_username + ":" + p_basic_auth_password) + var header = "Basic %s" % credentials + headers["Authorization"] = header + + var content : PackedByteArray + content = JSON.stringify(p_account.serialize()).to_utf8_buffer() + + var result = await _http_adapter.send_async(method, uri, headers, content) + if result is NakamaException: + return ApiSession.new(result) + var out : ApiSession = NakamaSerializer.deserialize(_namespace, "ApiSession", result) + return out + + # Authenticate a user with Apple's GameCenter against the server. + func authenticate_game_center_async( + p_basic_auth_username : String + , p_basic_auth_password : String + , p_account : ApiAccountGameCenter + , p_create = null # : boolean + , p_username = null # : string + ) -> ApiSession: + var urlpath : String = "/v2/account/authenticate/gamecenter" + var query_params = "" + if p_create != null: + query_params += "create=%s&" % str(bool(p_create)).to_lower() + if p_username != null: + query_params += "username=%s&" % NakamaSerializer.escape_http(p_username) + var uri = "%s%s%s" % [_base_uri, urlpath, "?" + query_params if query_params else ""] + var method = "POST" + var headers = {} + var credentials = Marshalls.utf8_to_base64(p_basic_auth_username + ":" + p_basic_auth_password) + var header = "Basic %s" % credentials + headers["Authorization"] = header + + var content : PackedByteArray + content = JSON.stringify(p_account.serialize()).to_utf8_buffer() + + var result = await _http_adapter.send_async(method, uri, headers, content) + if result is NakamaException: + return ApiSession.new(result) + var out : ApiSession = NakamaSerializer.deserialize(_namespace, "ApiSession", result) + return out + + # Authenticate a user with Google against the server. + func authenticate_google_async( + p_basic_auth_username : String + , p_basic_auth_password : String + , p_account : ApiAccountGoogle + , p_create = null # : boolean + , p_username = null # : string + ) -> ApiSession: + var urlpath : String = "/v2/account/authenticate/google" + var query_params = "" + if p_create != null: + query_params += "create=%s&" % str(bool(p_create)).to_lower() + if p_username != null: + query_params += "username=%s&" % NakamaSerializer.escape_http(p_username) + var uri = "%s%s%s" % [_base_uri, urlpath, "?" + query_params if query_params else ""] + var method = "POST" + var headers = {} + var credentials = Marshalls.utf8_to_base64(p_basic_auth_username + ":" + p_basic_auth_password) + var header = "Basic %s" % credentials + headers["Authorization"] = header + + var content : PackedByteArray + content = JSON.stringify(p_account.serialize()).to_utf8_buffer() + + var result = await _http_adapter.send_async(method, uri, headers, content) + if result is NakamaException: + return ApiSession.new(result) + var out : ApiSession = NakamaSerializer.deserialize(_namespace, "ApiSession", result) + return out + + # Authenticate a user with Steam against the server. + func authenticate_steam_async( + p_basic_auth_username : String + , p_basic_auth_password : String + , p_account : ApiAccountSteam + , p_create = null # : boolean + , p_username = null # : string + , p_sync = null # : boolean + ) -> ApiSession: + var urlpath : String = "/v2/account/authenticate/steam" + var query_params = "" + if p_create != null: + query_params += "create=%s&" % str(bool(p_create)).to_lower() + if p_username != null: + query_params += "username=%s&" % NakamaSerializer.escape_http(p_username) + if p_sync != null: + query_params += "sync=%s&" % str(bool(p_sync)).to_lower() + var uri = "%s%s%s" % [_base_uri, urlpath, "?" + query_params if query_params else ""] + var method = "POST" + var headers = {} + var credentials = Marshalls.utf8_to_base64(p_basic_auth_username + ":" + p_basic_auth_password) + var header = "Basic %s" % credentials + headers["Authorization"] = header + + var content : PackedByteArray + content = JSON.stringify(p_account.serialize()).to_utf8_buffer() + + var result = await _http_adapter.send_async(method, uri, headers, content) + if result is NakamaException: + return ApiSession.new(result) + var out : ApiSession = NakamaSerializer.deserialize(_namespace, "ApiSession", result) + return out + + # Add an Apple ID to the social profiles on the current user's account. + func link_apple_async( + p_session : NakamaSession + , p_body : ApiAccountApple + ) -> NakamaAsyncResult: + var try_refresh = await _refresh_session(p_session) + if try_refresh != null: + if try_refresh.is_exception(): + return NakamaAsyncResult.new(try_refresh.get_exception()) + await p_session.refresh(try_refresh) + var urlpath : String = "/v2/account/link/apple" + var query_params = "" + var uri = "%s%s%s" % [_base_uri, urlpath, "?" + query_params if query_params else ""] + var method = "POST" + var headers = {} + var header = "Bearer %s" % p_session.token + headers["Authorization"] = header + + var content : PackedByteArray + content = JSON.stringify(p_body.serialize()).to_utf8_buffer() + + var result = await _http_adapter.send_async(method, uri, headers, content) + if result is NakamaException: + return NakamaAsyncResult.new(result) + return NakamaAsyncResult.new() + + # Add a custom ID to the social profiles on the current user's account. + func link_custom_async( + p_session : NakamaSession + , p_body : ApiAccountCustom + ) -> NakamaAsyncResult: + var try_refresh = await _refresh_session(p_session) + if try_refresh != null: + if try_refresh.is_exception(): + return NakamaAsyncResult.new(try_refresh.get_exception()) + await p_session.refresh(try_refresh) + var urlpath : String = "/v2/account/link/custom" + var query_params = "" + var uri = "%s%s%s" % [_base_uri, urlpath, "?" + query_params if query_params else ""] + var method = "POST" + var headers = {} + var header = "Bearer %s" % p_session.token + headers["Authorization"] = header + + var content : PackedByteArray + content = JSON.stringify(p_body.serialize()).to_utf8_buffer() + + var result = await _http_adapter.send_async(method, uri, headers, content) + if result is NakamaException: + return NakamaAsyncResult.new(result) + return NakamaAsyncResult.new() + + # Add a device ID to the social profiles on the current user's account. + func link_device_async( + p_session : NakamaSession + , p_body : ApiAccountDevice + ) -> NakamaAsyncResult: + var try_refresh = await _refresh_session(p_session) + if try_refresh != null: + if try_refresh.is_exception(): + return NakamaAsyncResult.new(try_refresh.get_exception()) + await p_session.refresh(try_refresh) + var urlpath : String = "/v2/account/link/device" + var query_params = "" + var uri = "%s%s%s" % [_base_uri, urlpath, "?" + query_params if query_params else ""] + var method = "POST" + var headers = {} + var header = "Bearer %s" % p_session.token + headers["Authorization"] = header + + var content : PackedByteArray + content = JSON.stringify(p_body.serialize()).to_utf8_buffer() + + var result = await _http_adapter.send_async(method, uri, headers, content) + if result is NakamaException: + return NakamaAsyncResult.new(result) + return NakamaAsyncResult.new() + + # Add an email+password to the social profiles on the current user's account. + func link_email_async( + p_session : NakamaSession + , p_body : ApiAccountEmail + ) -> NakamaAsyncResult: + var try_refresh = await _refresh_session(p_session) + if try_refresh != null: + if try_refresh.is_exception(): + return NakamaAsyncResult.new(try_refresh.get_exception()) + await p_session.refresh(try_refresh) + var urlpath : String = "/v2/account/link/email" + var query_params = "" + var uri = "%s%s%s" % [_base_uri, urlpath, "?" + query_params if query_params else ""] + var method = "POST" + var headers = {} + var header = "Bearer %s" % p_session.token + headers["Authorization"] = header + + var content : PackedByteArray + content = JSON.stringify(p_body.serialize()).to_utf8_buffer() + + var result = await _http_adapter.send_async(method, uri, headers, content) + if result is NakamaException: + return NakamaAsyncResult.new(result) + return NakamaAsyncResult.new() + + # Add Facebook to the social profiles on the current user's account. + func link_facebook_async( + p_session : NakamaSession + , p_account : ApiAccountFacebook + , p_sync = null # : boolean + ) -> NakamaAsyncResult: + var try_refresh = await _refresh_session(p_session) + if try_refresh != null: + if try_refresh.is_exception(): + return NakamaAsyncResult.new(try_refresh.get_exception()) + await p_session.refresh(try_refresh) + var urlpath : String = "/v2/account/link/facebook" + var query_params = "" + if p_sync != null: + query_params += "sync=%s&" % str(bool(p_sync)).to_lower() + var uri = "%s%s%s" % [_base_uri, urlpath, "?" + query_params if query_params else ""] + var method = "POST" + var headers = {} + var header = "Bearer %s" % p_session.token + headers["Authorization"] = header + + var content : PackedByteArray + content = JSON.stringify(p_account.serialize()).to_utf8_buffer() + + var result = await _http_adapter.send_async(method, uri, headers, content) + if result is NakamaException: + return NakamaAsyncResult.new(result) + return NakamaAsyncResult.new() + + # Add Facebook Instant Game to the social profiles on the current user's account. + func link_facebook_instant_game_async( + p_session : NakamaSession + , p_body : ApiAccountFacebookInstantGame + ) -> NakamaAsyncResult: + var try_refresh = await _refresh_session(p_session) + if try_refresh != null: + if try_refresh.is_exception(): + return NakamaAsyncResult.new(try_refresh.get_exception()) + await p_session.refresh(try_refresh) + var urlpath : String = "/v2/account/link/facebookinstantgame" + var query_params = "" + var uri = "%s%s%s" % [_base_uri, urlpath, "?" + query_params if query_params else ""] + var method = "POST" + var headers = {} + var header = "Bearer %s" % p_session.token + headers["Authorization"] = header + + var content : PackedByteArray + content = JSON.stringify(p_body.serialize()).to_utf8_buffer() + + var result = await _http_adapter.send_async(method, uri, headers, content) + if result is NakamaException: + return NakamaAsyncResult.new(result) + return NakamaAsyncResult.new() + + # Add Apple's GameCenter to the social profiles on the current user's account. + func link_game_center_async( + p_session : NakamaSession + , p_body : ApiAccountGameCenter + ) -> NakamaAsyncResult: + var try_refresh = await _refresh_session(p_session) + if try_refresh != null: + if try_refresh.is_exception(): + return NakamaAsyncResult.new(try_refresh.get_exception()) + await p_session.refresh(try_refresh) + var urlpath : String = "/v2/account/link/gamecenter" + var query_params = "" + var uri = "%s%s%s" % [_base_uri, urlpath, "?" + query_params if query_params else ""] + var method = "POST" + var headers = {} + var header = "Bearer %s" % p_session.token + headers["Authorization"] = header + + var content : PackedByteArray + content = JSON.stringify(p_body.serialize()).to_utf8_buffer() + + var result = await _http_adapter.send_async(method, uri, headers, content) + if result is NakamaException: + return NakamaAsyncResult.new(result) + return NakamaAsyncResult.new() + + # Add Google to the social profiles on the current user's account. + func link_google_async( + p_session : NakamaSession + , p_body : ApiAccountGoogle + ) -> NakamaAsyncResult: + var try_refresh = await _refresh_session(p_session) + if try_refresh != null: + if try_refresh.is_exception(): + return NakamaAsyncResult.new(try_refresh.get_exception()) + await p_session.refresh(try_refresh) + var urlpath : String = "/v2/account/link/google" + var query_params = "" + var uri = "%s%s%s" % [_base_uri, urlpath, "?" + query_params if query_params else ""] + var method = "POST" + var headers = {} + var header = "Bearer %s" % p_session.token + headers["Authorization"] = header + + var content : PackedByteArray + content = JSON.stringify(p_body.serialize()).to_utf8_buffer() + + var result = await _http_adapter.send_async(method, uri, headers, content) + if result is NakamaException: + return NakamaAsyncResult.new(result) + return NakamaAsyncResult.new() + + # Add Steam to the social profiles on the current user's account. + func link_steam_async( + p_session : NakamaSession + , p_body : ApiLinkSteamRequest + ) -> NakamaAsyncResult: + var try_refresh = await _refresh_session(p_session) + if try_refresh != null: + if try_refresh.is_exception(): + return NakamaAsyncResult.new(try_refresh.get_exception()) + await p_session.refresh(try_refresh) + var urlpath : String = "/v2/account/link/steam" + var query_params = "" + var uri = "%s%s%s" % [_base_uri, urlpath, "?" + query_params if query_params else ""] + var method = "POST" + var headers = {} + var header = "Bearer %s" % p_session.token + headers["Authorization"] = header + + var content : PackedByteArray + content = JSON.stringify(p_body.serialize()).to_utf8_buffer() + + var result = await _http_adapter.send_async(method, uri, headers, content) + if result is NakamaException: + return NakamaAsyncResult.new(result) + return NakamaAsyncResult.new() + + # Refresh a user's session using a refresh token retrieved from a previous authentication request. + func session_refresh_async( + p_basic_auth_username : String + , p_basic_auth_password : String + , p_body : ApiSessionRefreshRequest + ) -> ApiSession: + var urlpath : String = "/v2/account/session/refresh" + var query_params = "" + var uri = "%s%s%s" % [_base_uri, urlpath, "?" + query_params if query_params else ""] + var method = "POST" + var headers = {} + var credentials = Marshalls.utf8_to_base64(p_basic_auth_username + ":" + p_basic_auth_password) + var header = "Basic %s" % credentials + headers["Authorization"] = header + + var content : PackedByteArray + content = JSON.stringify(p_body.serialize()).to_utf8_buffer() + + var result = await _http_adapter.send_async(method, uri, headers, content) + if result is NakamaException: + return ApiSession.new(result) + var out : ApiSession = NakamaSerializer.deserialize(_namespace, "ApiSession", result) + return out + + # Remove the Apple ID from the social profiles on the current user's account. + func unlink_apple_async( + p_session : NakamaSession + , p_body : ApiAccountApple + ) -> NakamaAsyncResult: + var try_refresh = await _refresh_session(p_session) + if try_refresh != null: + if try_refresh.is_exception(): + return NakamaAsyncResult.new(try_refresh.get_exception()) + await p_session.refresh(try_refresh) + var urlpath : String = "/v2/account/unlink/apple" + var query_params = "" + var uri = "%s%s%s" % [_base_uri, urlpath, "?" + query_params if query_params else ""] + var method = "POST" + var headers = {} + var header = "Bearer %s" % p_session.token + headers["Authorization"] = header + + var content : PackedByteArray + content = JSON.stringify(p_body.serialize()).to_utf8_buffer() + + var result = await _http_adapter.send_async(method, uri, headers, content) + if result is NakamaException: + return NakamaAsyncResult.new(result) + return NakamaAsyncResult.new() + + # Remove the custom ID from the social profiles on the current user's account. + func unlink_custom_async( + p_session : NakamaSession + , p_body : ApiAccountCustom + ) -> NakamaAsyncResult: + var try_refresh = await _refresh_session(p_session) + if try_refresh != null: + if try_refresh.is_exception(): + return NakamaAsyncResult.new(try_refresh.get_exception()) + await p_session.refresh(try_refresh) + var urlpath : String = "/v2/account/unlink/custom" + var query_params = "" + var uri = "%s%s%s" % [_base_uri, urlpath, "?" + query_params if query_params else ""] + var method = "POST" + var headers = {} + var header = "Bearer %s" % p_session.token + headers["Authorization"] = header + + var content : PackedByteArray + content = JSON.stringify(p_body.serialize()).to_utf8_buffer() + + var result = await _http_adapter.send_async(method, uri, headers, content) + if result is NakamaException: + return NakamaAsyncResult.new(result) + return NakamaAsyncResult.new() + + # Remove the device ID from the social profiles on the current user's account. + func unlink_device_async( + p_session : NakamaSession + , p_body : ApiAccountDevice + ) -> NakamaAsyncResult: + var try_refresh = await _refresh_session(p_session) + if try_refresh != null: + if try_refresh.is_exception(): + return NakamaAsyncResult.new(try_refresh.get_exception()) + await p_session.refresh(try_refresh) + var urlpath : String = "/v2/account/unlink/device" + var query_params = "" + var uri = "%s%s%s" % [_base_uri, urlpath, "?" + query_params if query_params else ""] + var method = "POST" + var headers = {} + var header = "Bearer %s" % p_session.token + headers["Authorization"] = header + + var content : PackedByteArray + content = JSON.stringify(p_body.serialize()).to_utf8_buffer() + + var result = await _http_adapter.send_async(method, uri, headers, content) + if result is NakamaException: + return NakamaAsyncResult.new(result) + return NakamaAsyncResult.new() + + # Remove the email+password from the social profiles on the current user's account. + func unlink_email_async( + p_session : NakamaSession + , p_body : ApiAccountEmail + ) -> NakamaAsyncResult: + var try_refresh = await _refresh_session(p_session) + if try_refresh != null: + if try_refresh.is_exception(): + return NakamaAsyncResult.new(try_refresh.get_exception()) + await p_session.refresh(try_refresh) + var urlpath : String = "/v2/account/unlink/email" + var query_params = "" + var uri = "%s%s%s" % [_base_uri, urlpath, "?" + query_params if query_params else ""] + var method = "POST" + var headers = {} + var header = "Bearer %s" % p_session.token + headers["Authorization"] = header + + var content : PackedByteArray + content = JSON.stringify(p_body.serialize()).to_utf8_buffer() + + var result = await _http_adapter.send_async(method, uri, headers, content) + if result is NakamaException: + return NakamaAsyncResult.new(result) + return NakamaAsyncResult.new() + + # Remove Facebook from the social profiles on the current user's account. + func unlink_facebook_async( + p_session : NakamaSession + , p_body : ApiAccountFacebook + ) -> NakamaAsyncResult: + var try_refresh = await _refresh_session(p_session) + if try_refresh != null: + if try_refresh.is_exception(): + return NakamaAsyncResult.new(try_refresh.get_exception()) + await p_session.refresh(try_refresh) + var urlpath : String = "/v2/account/unlink/facebook" + var query_params = "" + var uri = "%s%s%s" % [_base_uri, urlpath, "?" + query_params if query_params else ""] + var method = "POST" + var headers = {} + var header = "Bearer %s" % p_session.token + headers["Authorization"] = header + + var content : PackedByteArray + content = JSON.stringify(p_body.serialize()).to_utf8_buffer() + + var result = await _http_adapter.send_async(method, uri, headers, content) + if result is NakamaException: + return NakamaAsyncResult.new(result) + return NakamaAsyncResult.new() + + # Remove Facebook Instant Game profile from the social profiles on the current user's account. + func unlink_facebook_instant_game_async( + p_session : NakamaSession + , p_body : ApiAccountFacebookInstantGame + ) -> NakamaAsyncResult: + var try_refresh = await _refresh_session(p_session) + if try_refresh != null: + if try_refresh.is_exception(): + return NakamaAsyncResult.new(try_refresh.get_exception()) + await p_session.refresh(try_refresh) + var urlpath : String = "/v2/account/unlink/facebookinstantgame" + var query_params = "" + var uri = "%s%s%s" % [_base_uri, urlpath, "?" + query_params if query_params else ""] + var method = "POST" + var headers = {} + var header = "Bearer %s" % p_session.token + headers["Authorization"] = header + + var content : PackedByteArray + content = JSON.stringify(p_body.serialize()).to_utf8_buffer() + + var result = await _http_adapter.send_async(method, uri, headers, content) + if result is NakamaException: + return NakamaAsyncResult.new(result) + return NakamaAsyncResult.new() + + # Remove Apple's GameCenter from the social profiles on the current user's account. + func unlink_game_center_async( + p_session : NakamaSession + , p_body : ApiAccountGameCenter + ) -> NakamaAsyncResult: + var try_refresh = await _refresh_session(p_session) + if try_refresh != null: + if try_refresh.is_exception(): + return NakamaAsyncResult.new(try_refresh.get_exception()) + await p_session.refresh(try_refresh) + var urlpath : String = "/v2/account/unlink/gamecenter" + var query_params = "" + var uri = "%s%s%s" % [_base_uri, urlpath, "?" + query_params if query_params else ""] + var method = "POST" + var headers = {} + var header = "Bearer %s" % p_session.token + headers["Authorization"] = header + + var content : PackedByteArray + content = JSON.stringify(p_body.serialize()).to_utf8_buffer() + + var result = await _http_adapter.send_async(method, uri, headers, content) + if result is NakamaException: + return NakamaAsyncResult.new(result) + return NakamaAsyncResult.new() + + # Remove Google from the social profiles on the current user's account. + func unlink_google_async( + p_session : NakamaSession + , p_body : ApiAccountGoogle + ) -> NakamaAsyncResult: + var try_refresh = await _refresh_session(p_session) + if try_refresh != null: + if try_refresh.is_exception(): + return NakamaAsyncResult.new(try_refresh.get_exception()) + await p_session.refresh(try_refresh) + var urlpath : String = "/v2/account/unlink/google" + var query_params = "" + var uri = "%s%s%s" % [_base_uri, urlpath, "?" + query_params if query_params else ""] + var method = "POST" + var headers = {} + var header = "Bearer %s" % p_session.token + headers["Authorization"] = header + + var content : PackedByteArray + content = JSON.stringify(p_body.serialize()).to_utf8_buffer() + + var result = await _http_adapter.send_async(method, uri, headers, content) + if result is NakamaException: + return NakamaAsyncResult.new(result) + return NakamaAsyncResult.new() + + # Remove Steam from the social profiles on the current user's account. + func unlink_steam_async( + p_session : NakamaSession + , p_body : ApiAccountSteam + ) -> NakamaAsyncResult: + var try_refresh = await _refresh_session(p_session) + if try_refresh != null: + if try_refresh.is_exception(): + return NakamaAsyncResult.new(try_refresh.get_exception()) + await p_session.refresh(try_refresh) + var urlpath : String = "/v2/account/unlink/steam" + var query_params = "" + var uri = "%s%s%s" % [_base_uri, urlpath, "?" + query_params if query_params else ""] + var method = "POST" + var headers = {} + var header = "Bearer %s" % p_session.token + headers["Authorization"] = header + + var content : PackedByteArray + content = JSON.stringify(p_body.serialize()).to_utf8_buffer() + + var result = await _http_adapter.send_async(method, uri, headers, content) + if result is NakamaException: + return NakamaAsyncResult.new(result) + return NakamaAsyncResult.new() + + # List a channel's message history. + func list_channel_messages_async( + p_session : NakamaSession + , p_channel_id : String + , p_limit = null # : integer + , p_forward = null # : boolean + , p_cursor = null # : string + ) -> ApiChannelMessageList: + var try_refresh = await _refresh_session(p_session) + if try_refresh != null: + if try_refresh.is_exception(): + return ApiChannelMessageList.new(try_refresh.get_exception()) + await p_session.refresh(try_refresh) + var urlpath : String = "/v2/channel/{channelId}" + urlpath = urlpath.replace("{channelId}", NakamaSerializer.escape_http(p_channel_id)) + var query_params = "" + if p_limit != null: + query_params += "limit=%d&" % p_limit + if p_forward != null: + query_params += "forward=%s&" % str(bool(p_forward)).to_lower() + if p_cursor != null: + query_params += "cursor=%s&" % NakamaSerializer.escape_http(p_cursor) + var uri = "%s%s%s" % [_base_uri, urlpath, "?" + query_params if query_params else ""] + var method = "GET" + var headers = {} + var header = "Bearer %s" % p_session.token + headers["Authorization"] = header + + var content : PackedByteArray + + var result = await _http_adapter.send_async(method, uri, headers, content) + if result is NakamaException: + return ApiChannelMessageList.new(result) + var out : ApiChannelMessageList = NakamaSerializer.deserialize(_namespace, "ApiChannelMessageList", result) + return out + + # Submit an event for processing in the server's registered runtime custom events handler. + func event_async( + p_session : NakamaSession + , p_body : ApiEvent + ) -> NakamaAsyncResult: + var try_refresh = await _refresh_session(p_session) + if try_refresh != null: + if try_refresh.is_exception(): + return NakamaAsyncResult.new(try_refresh.get_exception()) + await p_session.refresh(try_refresh) + var urlpath : String = "/v2/event" + var query_params = "" + var uri = "%s%s%s" % [_base_uri, urlpath, "?" + query_params if query_params else ""] + var method = "POST" + var headers = {} + var header = "Bearer %s" % p_session.token + headers["Authorization"] = header + + var content : PackedByteArray + content = JSON.stringify(p_body.serialize()).to_utf8_buffer() + + var result = await _http_adapter.send_async(method, uri, headers, content) + if result is NakamaException: + return NakamaAsyncResult.new(result) + return NakamaAsyncResult.new() + + # Delete one or more users by ID or username. + func delete_friends_async( + p_session : NakamaSession + , p_ids = null # : array + , p_usernames = null # : array + ) -> NakamaAsyncResult: + var try_refresh = await _refresh_session(p_session) + if try_refresh != null: + if try_refresh.is_exception(): + return NakamaAsyncResult.new(try_refresh.get_exception()) + await p_session.refresh(try_refresh) + var urlpath : String = "/v2/friend" + var query_params = "" + if p_ids != null: + for elem in p_ids: + query_params += "ids=%s&" % elem + if p_usernames != null: + for elem in p_usernames: + query_params += "usernames=%s&" % elem + var uri = "%s%s%s" % [_base_uri, urlpath, "?" + query_params if query_params else ""] + var method = "DELETE" + var headers = {} + var header = "Bearer %s" % p_session.token + headers["Authorization"] = header + + var content : PackedByteArray + + var result = await _http_adapter.send_async(method, uri, headers, content) + if result is NakamaException: + return NakamaAsyncResult.new(result) + return NakamaAsyncResult.new() + + # List all friends for the current user. + func list_friends_async( + p_session : NakamaSession + , p_limit = null # : integer + , p_state = null # : integer + , p_cursor = null # : string + ) -> ApiFriendList: + var try_refresh = await _refresh_session(p_session) + if try_refresh != null: + if try_refresh.is_exception(): + return ApiFriendList.new(try_refresh.get_exception()) + await p_session.refresh(try_refresh) + var urlpath : String = "/v2/friend" + var query_params = "" + if p_limit != null: + query_params += "limit=%d&" % p_limit + if p_state != null: + query_params += "state=%d&" % p_state + if p_cursor != null: + query_params += "cursor=%s&" % NakamaSerializer.escape_http(p_cursor) + var uri = "%s%s%s" % [_base_uri, urlpath, "?" + query_params if query_params else ""] + var method = "GET" + var headers = {} + var header = "Bearer %s" % p_session.token + headers["Authorization"] = header + + var content : PackedByteArray + + var result = await _http_adapter.send_async(method, uri, headers, content) + if result is NakamaException: + return ApiFriendList.new(result) + var out : ApiFriendList = NakamaSerializer.deserialize(_namespace, "ApiFriendList", result) + return out + + # Add friends by ID or username to a user's account. + func add_friends_async( + p_session : NakamaSession + , p_ids = null # : array + , p_usernames = null # : array + ) -> NakamaAsyncResult: + var try_refresh = await _refresh_session(p_session) + if try_refresh != null: + if try_refresh.is_exception(): + return NakamaAsyncResult.new(try_refresh.get_exception()) + await p_session.refresh(try_refresh) + var urlpath : String = "/v2/friend" + var query_params = "" + if p_ids != null: + for elem in p_ids: + query_params += "ids=%s&" % elem + if p_usernames != null: + for elem in p_usernames: + query_params += "usernames=%s&" % elem + var uri = "%s%s%s" % [_base_uri, urlpath, "?" + query_params if query_params else ""] + var method = "POST" + var headers = {} + var header = "Bearer %s" % p_session.token + headers["Authorization"] = header + + var content : PackedByteArray + + var result = await _http_adapter.send_async(method, uri, headers, content) + if result is NakamaException: + return NakamaAsyncResult.new(result) + return NakamaAsyncResult.new() + + # Block one or more users by ID or username. + func block_friends_async( + p_session : NakamaSession + , p_ids = null # : array + , p_usernames = null # : array + ) -> NakamaAsyncResult: + var try_refresh = await _refresh_session(p_session) + if try_refresh != null: + if try_refresh.is_exception(): + return NakamaAsyncResult.new(try_refresh.get_exception()) + await p_session.refresh(try_refresh) + var urlpath : String = "/v2/friend/block" + var query_params = "" + if p_ids != null: + for elem in p_ids: + query_params += "ids=%s&" % elem + if p_usernames != null: + for elem in p_usernames: + query_params += "usernames=%s&" % elem + var uri = "%s%s%s" % [_base_uri, urlpath, "?" + query_params if query_params else ""] + var method = "POST" + var headers = {} + var header = "Bearer %s" % p_session.token + headers["Authorization"] = header + + var content : PackedByteArray + + var result = await _http_adapter.send_async(method, uri, headers, content) + if result is NakamaException: + return NakamaAsyncResult.new(result) + return NakamaAsyncResult.new() + + # Import Facebook friends and add them to a user's account. + func import_facebook_friends_async( + p_session : NakamaSession + , p_account : ApiAccountFacebook + , p_reset = null # : boolean + ) -> NakamaAsyncResult: + var try_refresh = await _refresh_session(p_session) + if try_refresh != null: + if try_refresh.is_exception(): + return NakamaAsyncResult.new(try_refresh.get_exception()) + await p_session.refresh(try_refresh) + var urlpath : String = "/v2/friend/facebook" + var query_params = "" + if p_reset != null: + query_params += "reset=%s&" % str(bool(p_reset)).to_lower() + var uri = "%s%s%s" % [_base_uri, urlpath, "?" + query_params if query_params else ""] + var method = "POST" + var headers = {} + var header = "Bearer %s" % p_session.token + headers["Authorization"] = header + + var content : PackedByteArray + content = JSON.stringify(p_account.serialize()).to_utf8_buffer() + + var result = await _http_adapter.send_async(method, uri, headers, content) + if result is NakamaException: + return NakamaAsyncResult.new(result) + return NakamaAsyncResult.new() + + # Import Steam friends and add them to a user's account. + func import_steam_friends_async( + p_session : NakamaSession + , p_account : ApiAccountSteam + , p_reset = null # : boolean + ) -> NakamaAsyncResult: + var try_refresh = await _refresh_session(p_session) + if try_refresh != null: + if try_refresh.is_exception(): + return NakamaAsyncResult.new(try_refresh.get_exception()) + await p_session.refresh(try_refresh) + var urlpath : String = "/v2/friend/steam" + var query_params = "" + if p_reset != null: + query_params += "reset=%s&" % str(bool(p_reset)).to_lower() + var uri = "%s%s%s" % [_base_uri, urlpath, "?" + query_params if query_params else ""] + var method = "POST" + var headers = {} + var header = "Bearer %s" % p_session.token + headers["Authorization"] = header + + var content : PackedByteArray + content = JSON.stringify(p_account.serialize()).to_utf8_buffer() + + var result = await _http_adapter.send_async(method, uri, headers, content) + if result is NakamaException: + return NakamaAsyncResult.new(result) + return NakamaAsyncResult.new() + + # List groups based on given filters. + func list_groups_async( + p_session : NakamaSession + , p_name = null # : string + , p_cursor = null # : string + , p_limit = null # : integer + , p_lang_tag = null # : string + , p_members = null # : integer + , p_open = null # : boolean + ) -> ApiGroupList: + var try_refresh = await _refresh_session(p_session) + if try_refresh != null: + if try_refresh.is_exception(): + return ApiGroupList.new(try_refresh.get_exception()) + await p_session.refresh(try_refresh) + var urlpath : String = "/v2/group" + var query_params = "" + if p_name != null: + query_params += "name=%s&" % NakamaSerializer.escape_http(p_name) + if p_cursor != null: + query_params += "cursor=%s&" % NakamaSerializer.escape_http(p_cursor) + if p_limit != null: + query_params += "limit=%d&" % p_limit + if p_lang_tag != null: + query_params += "lang_tag=%s&" % NakamaSerializer.escape_http(p_lang_tag) + if p_members != null: + query_params += "members=%d&" % p_members + if p_open != null: + query_params += "open=%s&" % str(bool(p_open)).to_lower() + var uri = "%s%s%s" % [_base_uri, urlpath, "?" + query_params if query_params else ""] + var method = "GET" + var headers = {} + var header = "Bearer %s" % p_session.token + headers["Authorization"] = header + + var content : PackedByteArray + + var result = await _http_adapter.send_async(method, uri, headers, content) + if result is NakamaException: + return ApiGroupList.new(result) + var out : ApiGroupList = NakamaSerializer.deserialize(_namespace, "ApiGroupList", result) + return out + + # Create a new group with the current user as the owner. + func create_group_async( + p_session : NakamaSession + , p_body : ApiCreateGroupRequest + ) -> ApiGroup: + var try_refresh = await _refresh_session(p_session) + if try_refresh != null: + if try_refresh.is_exception(): + return ApiGroup.new(try_refresh.get_exception()) + await p_session.refresh(try_refresh) + var urlpath : String = "/v2/group" + var query_params = "" + var uri = "%s%s%s" % [_base_uri, urlpath, "?" + query_params if query_params else ""] + var method = "POST" + var headers = {} + var header = "Bearer %s" % p_session.token + headers["Authorization"] = header + + var content : PackedByteArray + content = JSON.stringify(p_body.serialize()).to_utf8_buffer() + + var result = await _http_adapter.send_async(method, uri, headers, content) + if result is NakamaException: + return ApiGroup.new(result) + var out : ApiGroup = NakamaSerializer.deserialize(_namespace, "ApiGroup", result) + return out + + # Delete a group by ID. + func delete_group_async( + p_session : NakamaSession + , p_group_id : String + ) -> NakamaAsyncResult: + var try_refresh = await _refresh_session(p_session) + if try_refresh != null: + if try_refresh.is_exception(): + return NakamaAsyncResult.new(try_refresh.get_exception()) + await p_session.refresh(try_refresh) + var urlpath : String = "/v2/group/{groupId}" + urlpath = urlpath.replace("{groupId}", NakamaSerializer.escape_http(p_group_id)) + var query_params = "" + var uri = "%s%s%s" % [_base_uri, urlpath, "?" + query_params if query_params else ""] + var method = "DELETE" + var headers = {} + var header = "Bearer %s" % p_session.token + headers["Authorization"] = header + + var content : PackedByteArray + + var result = await _http_adapter.send_async(method, uri, headers, content) + if result is NakamaException: + return NakamaAsyncResult.new(result) + return NakamaAsyncResult.new() + + # Update fields in a given group. + func update_group_async( + p_session : NakamaSession + , p_group_id : String + , p_body : ApiUpdateGroupRequest + ) -> NakamaAsyncResult: + var try_refresh = await _refresh_session(p_session) + if try_refresh != null: + if try_refresh.is_exception(): + return NakamaAsyncResult.new(try_refresh.get_exception()) + await p_session.refresh(try_refresh) + var urlpath : String = "/v2/group/{groupId}" + urlpath = urlpath.replace("{groupId}", NakamaSerializer.escape_http(p_group_id)) + var query_params = "" + var uri = "%s%s%s" % [_base_uri, urlpath, "?" + query_params if query_params else ""] + var method = "PUT" + var headers = {} + var header = "Bearer %s" % p_session.token + headers["Authorization"] = header + + var content : PackedByteArray + content = JSON.stringify(p_body.serialize()).to_utf8_buffer() + + var result = await _http_adapter.send_async(method, uri, headers, content) + if result is NakamaException: + return NakamaAsyncResult.new(result) + return NakamaAsyncResult.new() + + # Add users to a group. + func add_group_users_async( + p_session : NakamaSession + , p_group_id : String + , p_user_ids = null # : array + ) -> NakamaAsyncResult: + var try_refresh = await _refresh_session(p_session) + if try_refresh != null: + if try_refresh.is_exception(): + return NakamaAsyncResult.new(try_refresh.get_exception()) + await p_session.refresh(try_refresh) + var urlpath : String = "/v2/group/{groupId}/add" + urlpath = urlpath.replace("{groupId}", NakamaSerializer.escape_http(p_group_id)) + var query_params = "" + if p_user_ids != null: + for elem in p_user_ids: + query_params += "user_ids=%s&" % elem + var uri = "%s%s%s" % [_base_uri, urlpath, "?" + query_params if query_params else ""] + var method = "POST" + var headers = {} + var header = "Bearer %s" % p_session.token + headers["Authorization"] = header + + var content : PackedByteArray + + var result = await _http_adapter.send_async(method, uri, headers, content) + if result is NakamaException: + return NakamaAsyncResult.new(result) + return NakamaAsyncResult.new() + + # Ban a set of users from a group. + func ban_group_users_async( + p_session : NakamaSession + , p_group_id : String + , p_user_ids = null # : array + ) -> NakamaAsyncResult: + var try_refresh = await _refresh_session(p_session) + if try_refresh != null: + if try_refresh.is_exception(): + return NakamaAsyncResult.new(try_refresh.get_exception()) + await p_session.refresh(try_refresh) + var urlpath : String = "/v2/group/{groupId}/ban" + urlpath = urlpath.replace("{groupId}", NakamaSerializer.escape_http(p_group_id)) + var query_params = "" + if p_user_ids != null: + for elem in p_user_ids: + query_params += "user_ids=%s&" % elem + var uri = "%s%s%s" % [_base_uri, urlpath, "?" + query_params if query_params else ""] + var method = "POST" + var headers = {} + var header = "Bearer %s" % p_session.token + headers["Authorization"] = header + + var content : PackedByteArray + + var result = await _http_adapter.send_async(method, uri, headers, content) + if result is NakamaException: + return NakamaAsyncResult.new(result) + return NakamaAsyncResult.new() + + # Demote a set of users in a group to the next role down. + func demote_group_users_async( + p_session : NakamaSession + , p_group_id : String + , p_user_ids = null # : array + ) -> NakamaAsyncResult: + var try_refresh = await _refresh_session(p_session) + if try_refresh != null: + if try_refresh.is_exception(): + return NakamaAsyncResult.new(try_refresh.get_exception()) + await p_session.refresh(try_refresh) + var urlpath : String = "/v2/group/{groupId}/demote" + urlpath = urlpath.replace("{groupId}", NakamaSerializer.escape_http(p_group_id)) + var query_params = "" + if p_user_ids != null: + for elem in p_user_ids: + query_params += "user_ids=%s&" % elem + var uri = "%s%s%s" % [_base_uri, urlpath, "?" + query_params if query_params else ""] + var method = "POST" + var headers = {} + var header = "Bearer %s" % p_session.token + headers["Authorization"] = header + + var content : PackedByteArray + + var result = await _http_adapter.send_async(method, uri, headers, content) + if result is NakamaException: + return NakamaAsyncResult.new(result) + return NakamaAsyncResult.new() + + # Immediately join an open group, or request to join a closed one. + func join_group_async( + p_session : NakamaSession + , p_group_id : String + ) -> NakamaAsyncResult: + var try_refresh = await _refresh_session(p_session) + if try_refresh != null: + if try_refresh.is_exception(): + return NakamaAsyncResult.new(try_refresh.get_exception()) + await p_session.refresh(try_refresh) + var urlpath : String = "/v2/group/{groupId}/join" + urlpath = urlpath.replace("{groupId}", NakamaSerializer.escape_http(p_group_id)) + var query_params = "" + var uri = "%s%s%s" % [_base_uri, urlpath, "?" + query_params if query_params else ""] + var method = "POST" + var headers = {} + var header = "Bearer %s" % p_session.token + headers["Authorization"] = header + + var content : PackedByteArray + + var result = await _http_adapter.send_async(method, uri, headers, content) + if result is NakamaException: + return NakamaAsyncResult.new(result) + return NakamaAsyncResult.new() + + # Kick a set of users from a group. + func kick_group_users_async( + p_session : NakamaSession + , p_group_id : String + , p_user_ids = null # : array + ) -> NakamaAsyncResult: + var try_refresh = await _refresh_session(p_session) + if try_refresh != null: + if try_refresh.is_exception(): + return NakamaAsyncResult.new(try_refresh.get_exception()) + await p_session.refresh(try_refresh) + var urlpath : String = "/v2/group/{groupId}/kick" + urlpath = urlpath.replace("{groupId}", NakamaSerializer.escape_http(p_group_id)) + var query_params = "" + if p_user_ids != null: + for elem in p_user_ids: + query_params += "user_ids=%s&" % elem + var uri = "%s%s%s" % [_base_uri, urlpath, "?" + query_params if query_params else ""] + var method = "POST" + var headers = {} + var header = "Bearer %s" % p_session.token + headers["Authorization"] = header + + var content : PackedByteArray + + var result = await _http_adapter.send_async(method, uri, headers, content) + if result is NakamaException: + return NakamaAsyncResult.new(result) + return NakamaAsyncResult.new() + + # Leave a group the user is a member of. + func leave_group_async( + p_session : NakamaSession + , p_group_id : String + ) -> NakamaAsyncResult: + var try_refresh = await _refresh_session(p_session) + if try_refresh != null: + if try_refresh.is_exception(): + return NakamaAsyncResult.new(try_refresh.get_exception()) + await p_session.refresh(try_refresh) + var urlpath : String = "/v2/group/{groupId}/leave" + urlpath = urlpath.replace("{groupId}", NakamaSerializer.escape_http(p_group_id)) + var query_params = "" + var uri = "%s%s%s" % [_base_uri, urlpath, "?" + query_params if query_params else ""] + var method = "POST" + var headers = {} + var header = "Bearer %s" % p_session.token + headers["Authorization"] = header + + var content : PackedByteArray + + var result = await _http_adapter.send_async(method, uri, headers, content) + if result is NakamaException: + return NakamaAsyncResult.new(result) + return NakamaAsyncResult.new() + + # Promote a set of users in a group to the next role up. + func promote_group_users_async( + p_session : NakamaSession + , p_group_id : String + , p_user_ids = null # : array + ) -> NakamaAsyncResult: + var try_refresh = await _refresh_session(p_session) + if try_refresh != null: + if try_refresh.is_exception(): + return NakamaAsyncResult.new(try_refresh.get_exception()) + await p_session.refresh(try_refresh) + var urlpath : String = "/v2/group/{groupId}/promote" + urlpath = urlpath.replace("{groupId}", NakamaSerializer.escape_http(p_group_id)) + var query_params = "" + if p_user_ids != null: + for elem in p_user_ids: + query_params += "user_ids=%s&" % elem + var uri = "%s%s%s" % [_base_uri, urlpath, "?" + query_params if query_params else ""] + var method = "POST" + var headers = {} + var header = "Bearer %s" % p_session.token + headers["Authorization"] = header + + var content : PackedByteArray + + var result = await _http_adapter.send_async(method, uri, headers, content) + if result is NakamaException: + return NakamaAsyncResult.new(result) + return NakamaAsyncResult.new() + + # List all users that are part of a group. + func list_group_users_async( + p_session : NakamaSession + , p_group_id : String + , p_limit = null # : integer + , p_state = null # : integer + , p_cursor = null # : string + ) -> ApiGroupUserList: + var try_refresh = await _refresh_session(p_session) + if try_refresh != null: + if try_refresh.is_exception(): + return ApiGroupUserList.new(try_refresh.get_exception()) + await p_session.refresh(try_refresh) + var urlpath : String = "/v2/group/{groupId}/user" + urlpath = urlpath.replace("{groupId}", NakamaSerializer.escape_http(p_group_id)) + var query_params = "" + if p_limit != null: + query_params += "limit=%d&" % p_limit + if p_state != null: + query_params += "state=%d&" % p_state + if p_cursor != null: + query_params += "cursor=%s&" % NakamaSerializer.escape_http(p_cursor) + var uri = "%s%s%s" % [_base_uri, urlpath, "?" + query_params if query_params else ""] + var method = "GET" + var headers = {} + var header = "Bearer %s" % p_session.token + headers["Authorization"] = header + + var content : PackedByteArray + + var result = await _http_adapter.send_async(method, uri, headers, content) + if result is NakamaException: + return ApiGroupUserList.new(result) + var out : ApiGroupUserList = NakamaSerializer.deserialize(_namespace, "ApiGroupUserList", result) + return out + + # Validate Apple IAP Receipt + func validate_purchase_apple_async( + p_session : NakamaSession + , p_body : ApiValidatePurchaseAppleRequest + ) -> ApiValidatePurchaseResponse: + var try_refresh = await _refresh_session(p_session) + if try_refresh != null: + if try_refresh.is_exception(): + return ApiValidatePurchaseResponse.new(try_refresh.get_exception()) + await p_session.refresh(try_refresh) + var urlpath : String = "/v2/iap/purchase/apple" + var query_params = "" + var uri = "%s%s%s" % [_base_uri, urlpath, "?" + query_params if query_params else ""] + var method = "POST" + var headers = {} + var header = "Bearer %s" % p_session.token + headers["Authorization"] = header + + var content : PackedByteArray + content = JSON.stringify(p_body.serialize()).to_utf8_buffer() + + var result = await _http_adapter.send_async(method, uri, headers, content) + if result is NakamaException: + return ApiValidatePurchaseResponse.new(result) + var out : ApiValidatePurchaseResponse = NakamaSerializer.deserialize(_namespace, "ApiValidatePurchaseResponse", result) + return out + + # Validate Google IAP Receipt + func validate_purchase_google_async( + p_session : NakamaSession + , p_body : ApiValidatePurchaseGoogleRequest + ) -> ApiValidatePurchaseResponse: + var try_refresh = await _refresh_session(p_session) + if try_refresh != null: + if try_refresh.is_exception(): + return ApiValidatePurchaseResponse.new(try_refresh.get_exception()) + await p_session.refresh(try_refresh) + var urlpath : String = "/v2/iap/purchase/google" + var query_params = "" + var uri = "%s%s%s" % [_base_uri, urlpath, "?" + query_params if query_params else ""] + var method = "POST" + var headers = {} + var header = "Bearer %s" % p_session.token + headers["Authorization"] = header + + var content : PackedByteArray + content = JSON.stringify(p_body.serialize()).to_utf8_buffer() + + var result = await _http_adapter.send_async(method, uri, headers, content) + if result is NakamaException: + return ApiValidatePurchaseResponse.new(result) + var out : ApiValidatePurchaseResponse = NakamaSerializer.deserialize(_namespace, "ApiValidatePurchaseResponse", result) + return out + + # Validate Huawei IAP Receipt + func validate_purchase_huawei_async( + p_session : NakamaSession + , p_body : ApiValidatePurchaseHuaweiRequest + ) -> ApiValidatePurchaseResponse: + var try_refresh = await _refresh_session(p_session) + if try_refresh != null: + if try_refresh.is_exception(): + return ApiValidatePurchaseResponse.new(try_refresh.get_exception()) + await p_session.refresh(try_refresh) + var urlpath : String = "/v2/iap/purchase/huawei" + var query_params = "" + var uri = "%s%s%s" % [_base_uri, urlpath, "?" + query_params if query_params else ""] + var method = "POST" + var headers = {} + var header = "Bearer %s" % p_session.token + headers["Authorization"] = header + + var content : PackedByteArray + content = JSON.stringify(p_body.serialize()).to_utf8_buffer() + + var result = await _http_adapter.send_async(method, uri, headers, content) + if result is NakamaException: + return ApiValidatePurchaseResponse.new(result) + var out : ApiValidatePurchaseResponse = NakamaSerializer.deserialize(_namespace, "ApiValidatePurchaseResponse", result) + return out + + # List user's subscriptions. + func list_subscriptions_async( + p_session : NakamaSession + , p_body : ApiListSubscriptionsRequest + ) -> ApiSubscriptionList: + var try_refresh = await _refresh_session(p_session) + if try_refresh != null: + if try_refresh.is_exception(): + return ApiSubscriptionList.new(try_refresh.get_exception()) + await p_session.refresh(try_refresh) + var urlpath : String = "/v2/iap/subscription" + var query_params = "" + var uri = "%s%s%s" % [_base_uri, urlpath, "?" + query_params if query_params else ""] + var method = "POST" + var headers = {} + var header = "Bearer %s" % p_session.token + headers["Authorization"] = header + + var content : PackedByteArray + content = JSON.stringify(p_body.serialize()).to_utf8_buffer() + + var result = await _http_adapter.send_async(method, uri, headers, content) + if result is NakamaException: + return ApiSubscriptionList.new(result) + var out : ApiSubscriptionList = NakamaSerializer.deserialize(_namespace, "ApiSubscriptionList", result) + return out + + # Validate Apple Subscription Receipt + func validate_subscription_apple_async( + p_session : NakamaSession + , p_body : ApiValidateSubscriptionAppleRequest + ) -> ApiValidateSubscriptionResponse: + var try_refresh = await _refresh_session(p_session) + if try_refresh != null: + if try_refresh.is_exception(): + return ApiValidateSubscriptionResponse.new(try_refresh.get_exception()) + await p_session.refresh(try_refresh) + var urlpath : String = "/v2/iap/subscription/apple" + var query_params = "" + var uri = "%s%s%s" % [_base_uri, urlpath, "?" + query_params if query_params else ""] + var method = "POST" + var headers = {} + var header = "Bearer %s" % p_session.token + headers["Authorization"] = header + + var content : PackedByteArray + content = JSON.stringify(p_body.serialize()).to_utf8_buffer() + + var result = await _http_adapter.send_async(method, uri, headers, content) + if result is NakamaException: + return ApiValidateSubscriptionResponse.new(result) + var out : ApiValidateSubscriptionResponse = NakamaSerializer.deserialize(_namespace, "ApiValidateSubscriptionResponse", result) + return out + + # Validate Google Subscription Receipt + func validate_subscription_google_async( + p_session : NakamaSession + , p_body : ApiValidateSubscriptionGoogleRequest + ) -> ApiValidateSubscriptionResponse: + var try_refresh = await _refresh_session(p_session) + if try_refresh != null: + if try_refresh.is_exception(): + return ApiValidateSubscriptionResponse.new(try_refresh.get_exception()) + await p_session.refresh(try_refresh) + var urlpath : String = "/v2/iap/subscription/google" + var query_params = "" + var uri = "%s%s%s" % [_base_uri, urlpath, "?" + query_params if query_params else ""] + var method = "POST" + var headers = {} + var header = "Bearer %s" % p_session.token + headers["Authorization"] = header + + var content : PackedByteArray + content = JSON.stringify(p_body.serialize()).to_utf8_buffer() + + var result = await _http_adapter.send_async(method, uri, headers, content) + if result is NakamaException: + return ApiValidateSubscriptionResponse.new(result) + var out : ApiValidateSubscriptionResponse = NakamaSerializer.deserialize(_namespace, "ApiValidateSubscriptionResponse", result) + return out + + # Get subscription by product id. + func get_subscription_async( + p_session : NakamaSession + , p_product_id : String + ) -> ApiValidatedSubscription: + var try_refresh = await _refresh_session(p_session) + if try_refresh != null: + if try_refresh.is_exception(): + return ApiValidatedSubscription.new(try_refresh.get_exception()) + await p_session.refresh(try_refresh) + var urlpath : String = "/v2/iap/subscription/{productId}" + urlpath = urlpath.replace("{productId}", NakamaSerializer.escape_http(p_product_id)) + var query_params = "" + var uri = "%s%s%s" % [_base_uri, urlpath, "?" + query_params if query_params else ""] + var method = "GET" + var headers = {} + var header = "Bearer %s" % p_session.token + headers["Authorization"] = header + + var content : PackedByteArray + + var result = await _http_adapter.send_async(method, uri, headers, content) + if result is NakamaException: + return ApiValidatedSubscription.new(result) + var out : ApiValidatedSubscription = NakamaSerializer.deserialize(_namespace, "ApiValidatedSubscription", result) + return out + + # Delete a leaderboard record. + func delete_leaderboard_record_async( + p_session : NakamaSession + , p_leaderboard_id : String + ) -> NakamaAsyncResult: + var try_refresh = await _refresh_session(p_session) + if try_refresh != null: + if try_refresh.is_exception(): + return NakamaAsyncResult.new(try_refresh.get_exception()) + await p_session.refresh(try_refresh) + var urlpath : String = "/v2/leaderboard/{leaderboardId}" + urlpath = urlpath.replace("{leaderboardId}", NakamaSerializer.escape_http(p_leaderboard_id)) + var query_params = "" + var uri = "%s%s%s" % [_base_uri, urlpath, "?" + query_params if query_params else ""] + var method = "DELETE" + var headers = {} + var header = "Bearer %s" % p_session.token + headers["Authorization"] = header + + var content : PackedByteArray + + var result = await _http_adapter.send_async(method, uri, headers, content) + if result is NakamaException: + return NakamaAsyncResult.new(result) + return NakamaAsyncResult.new() + + # List leaderboard records. + func list_leaderboard_records_async( + p_session : NakamaSession + , p_leaderboard_id : String + , p_owner_ids = null # : array + , p_limit = null # : integer + , p_cursor = null # : string + , p_expiry = null # : string + ) -> ApiLeaderboardRecordList: + var try_refresh = await _refresh_session(p_session) + if try_refresh != null: + if try_refresh.is_exception(): + return ApiLeaderboardRecordList.new(try_refresh.get_exception()) + await p_session.refresh(try_refresh) + var urlpath : String = "/v2/leaderboard/{leaderboardId}" + urlpath = urlpath.replace("{leaderboardId}", NakamaSerializer.escape_http(p_leaderboard_id)) + var query_params = "" + if p_owner_ids != null: + for elem in p_owner_ids: + query_params += "owner_ids=%s&" % elem + if p_limit != null: + query_params += "limit=%d&" % p_limit + if p_cursor != null: + query_params += "cursor=%s&" % NakamaSerializer.escape_http(p_cursor) + if p_expiry != null: + query_params += "expiry=%s&" % NakamaSerializer.escape_http(p_expiry) + var uri = "%s%s%s" % [_base_uri, urlpath, "?" + query_params if query_params else ""] + var method = "GET" + var headers = {} + var header = "Bearer %s" % p_session.token + headers["Authorization"] = header + + var content : PackedByteArray + + var result = await _http_adapter.send_async(method, uri, headers, content) + if result is NakamaException: + return ApiLeaderboardRecordList.new(result) + var out : ApiLeaderboardRecordList = NakamaSerializer.deserialize(_namespace, "ApiLeaderboardRecordList", result) + return out + + # Write a record to a leaderboard. + func write_leaderboard_record_async( + p_session : NakamaSession + , p_leaderboard_id : String + , p_record : WriteLeaderboardRecordRequestLeaderboardRecordWrite + ) -> ApiLeaderboardRecord: + var try_refresh = await _refresh_session(p_session) + if try_refresh != null: + if try_refresh.is_exception(): + return ApiLeaderboardRecord.new(try_refresh.get_exception()) + await p_session.refresh(try_refresh) + var urlpath : String = "/v2/leaderboard/{leaderboardId}" + urlpath = urlpath.replace("{leaderboardId}", NakamaSerializer.escape_http(p_leaderboard_id)) + var query_params = "" + var uri = "%s%s%s" % [_base_uri, urlpath, "?" + query_params if query_params else ""] + var method = "POST" + var headers = {} + var header = "Bearer %s" % p_session.token + headers["Authorization"] = header + + var content : PackedByteArray + content = JSON.stringify(p_record.serialize()).to_utf8_buffer() + + var result = await _http_adapter.send_async(method, uri, headers, content) + if result is NakamaException: + return ApiLeaderboardRecord.new(result) + var out : ApiLeaderboardRecord = NakamaSerializer.deserialize(_namespace, "ApiLeaderboardRecord", result) + return out + + # List leaderboard records that belong to a user. + func list_leaderboard_records_around_owner_async( + p_session : NakamaSession + , p_leaderboard_id : String + , p_owner_id : String + , p_limit = null # : integer + , p_expiry = null # : string + , p_cursor = null # : string + ) -> ApiLeaderboardRecordList: + var try_refresh = await _refresh_session(p_session) + if try_refresh != null: + if try_refresh.is_exception(): + return ApiLeaderboardRecordList.new(try_refresh.get_exception()) + await p_session.refresh(try_refresh) + var urlpath : String = "/v2/leaderboard/{leaderboardId}/owner/{ownerId}" + urlpath = urlpath.replace("{leaderboardId}", NakamaSerializer.escape_http(p_leaderboard_id)) + urlpath = urlpath.replace("{ownerId}", NakamaSerializer.escape_http(p_owner_id)) + var query_params = "" + if p_limit != null: + query_params += "limit=%d&" % p_limit + if p_expiry != null: + query_params += "expiry=%s&" % NakamaSerializer.escape_http(p_expiry) + if p_cursor != null: + query_params += "cursor=%s&" % NakamaSerializer.escape_http(p_cursor) + var uri = "%s%s%s" % [_base_uri, urlpath, "?" + query_params if query_params else ""] + var method = "GET" + var headers = {} + var header = "Bearer %s" % p_session.token + headers["Authorization"] = header + + var content : PackedByteArray + + var result = await _http_adapter.send_async(method, uri, headers, content) + if result is NakamaException: + return ApiLeaderboardRecordList.new(result) + var out : ApiLeaderboardRecordList = NakamaSerializer.deserialize(_namespace, "ApiLeaderboardRecordList", result) + return out + + # Fetch list of running matches. + func list_matches_async( + p_session : NakamaSession + , p_limit = null # : integer + , p_authoritative = null # : boolean + , p_label = null # : string + , p_min_size = null # : integer + , p_max_size = null # : integer + , p_query = null # : string + ) -> ApiMatchList: + var try_refresh = await _refresh_session(p_session) + if try_refresh != null: + if try_refresh.is_exception(): + return ApiMatchList.new(try_refresh.get_exception()) + await p_session.refresh(try_refresh) + var urlpath : String = "/v2/match" + var query_params = "" + if p_limit != null: + query_params += "limit=%d&" % p_limit + if p_authoritative != null: + query_params += "authoritative=%s&" % str(bool(p_authoritative)).to_lower() + if p_label != null: + query_params += "label=%s&" % NakamaSerializer.escape_http(p_label) + if p_min_size != null: + query_params += "min_size=%d&" % p_min_size + if p_max_size != null: + query_params += "max_size=%d&" % p_max_size + if p_query != null: + query_params += "query=%s&" % NakamaSerializer.escape_http(p_query) + var uri = "%s%s%s" % [_base_uri, urlpath, "?" + query_params if query_params else ""] + var method = "GET" + var headers = {} + var header = "Bearer %s" % p_session.token + headers["Authorization"] = header + + var content : PackedByteArray + + var result = await _http_adapter.send_async(method, uri, headers, content) + if result is NakamaException: + return ApiMatchList.new(result) + var out : ApiMatchList = NakamaSerializer.deserialize(_namespace, "ApiMatchList", result) + return out + + # Delete one or more notifications for the current user. + func delete_notifications_async( + p_session : NakamaSession + , p_ids = null # : array + ) -> NakamaAsyncResult: + var try_refresh = await _refresh_session(p_session) + if try_refresh != null: + if try_refresh.is_exception(): + return NakamaAsyncResult.new(try_refresh.get_exception()) + await p_session.refresh(try_refresh) + var urlpath : String = "/v2/notification" + var query_params = "" + if p_ids != null: + for elem in p_ids: + query_params += "ids=%s&" % elem + var uri = "%s%s%s" % [_base_uri, urlpath, "?" + query_params if query_params else ""] + var method = "DELETE" + var headers = {} + var header = "Bearer %s" % p_session.token + headers["Authorization"] = header + + var content : PackedByteArray + + var result = await _http_adapter.send_async(method, uri, headers, content) + if result is NakamaException: + return NakamaAsyncResult.new(result) + return NakamaAsyncResult.new() + + # Fetch list of notifications. + func list_notifications_async( + p_session : NakamaSession + , p_limit = null # : integer + , p_cacheable_cursor = null # : string + ) -> ApiNotificationList: + var try_refresh = await _refresh_session(p_session) + if try_refresh != null: + if try_refresh.is_exception(): + return ApiNotificationList.new(try_refresh.get_exception()) + await p_session.refresh(try_refresh) + var urlpath : String = "/v2/notification" + var query_params = "" + if p_limit != null: + query_params += "limit=%d&" % p_limit + if p_cacheable_cursor != null: + query_params += "cacheable_cursor=%s&" % NakamaSerializer.escape_http(p_cacheable_cursor) + var uri = "%s%s%s" % [_base_uri, urlpath, "?" + query_params if query_params else ""] + var method = "GET" + var headers = {} + var header = "Bearer %s" % p_session.token + headers["Authorization"] = header + + var content : PackedByteArray + + var result = await _http_adapter.send_async(method, uri, headers, content) + if result is NakamaException: + return ApiNotificationList.new(result) + var out : ApiNotificationList = NakamaSerializer.deserialize(_namespace, "ApiNotificationList", result) + return out + + # Execute a Lua function on the server. + func rpc_func2_async( + p_bearer_token : String + , p_id : String + , p_payload = null # : string + , p_http_key = null # : string + ) -> ApiRpc: + var urlpath : String = "/v2/rpc/{id}" + urlpath = urlpath.replace("{id}", NakamaSerializer.escape_http(p_id)) + var query_params = "" + if p_payload != null: + query_params += "payload=%s&" % NakamaSerializer.escape_http(p_payload) + if p_http_key != null: + query_params += "http_key=%s&" % NakamaSerializer.escape_http(p_http_key) + var uri = "%s%s%s" % [_base_uri, urlpath, "?" + query_params if query_params else ""] + var method = "GET" + var headers = {} + if (p_bearer_token): + var header = "Bearer %s" % p_bearer_token + headers["Authorization"] = header + + var content : PackedByteArray + + var result = await _http_adapter.send_async(method, uri, headers, content) + if result is NakamaException: + return ApiRpc.new(result) + var out : ApiRpc = NakamaSerializer.deserialize(_namespace, "ApiRpc", result) + return out + + # Execute a Lua function on the server. + func rpc_func_async( + p_bearer_token : String + , p_id : String + , p_payload : String + , p_http_key = null # : string + ) -> ApiRpc: + var urlpath : String = "/v2/rpc/{id}" + urlpath = urlpath.replace("{id}", NakamaSerializer.escape_http(p_id)) + var query_params = "" + if p_http_key != null: + query_params += "http_key=%s&" % NakamaSerializer.escape_http(p_http_key) + var uri = "%s%s%s" % [_base_uri, urlpath, "?" + query_params if query_params else ""] + var method = "POST" + var headers = {} + if (p_bearer_token): + var header = "Bearer %s" % p_bearer_token + headers["Authorization"] = header + + var content : PackedByteArray + content = JSON.stringify(p_payload).to_utf8_buffer() + + var result = await _http_adapter.send_async(method, uri, headers, content) + if result is NakamaException: + return ApiRpc.new(result) + var out : ApiRpc = NakamaSerializer.deserialize(_namespace, "ApiRpc", result) + return out + + # Log out a session, invalidate a refresh token, or log out all sessions/refresh tokens for a user. + func session_logout_async( + p_session : NakamaSession + , p_body : ApiSessionLogoutRequest + ) -> NakamaAsyncResult: + var try_refresh = await _refresh_session(p_session) + if try_refresh != null: + if try_refresh.is_exception(): + return NakamaAsyncResult.new(try_refresh.get_exception()) + await p_session.refresh(try_refresh) + var urlpath : String = "/v2/session/logout" + var query_params = "" + var uri = "%s%s%s" % [_base_uri, urlpath, "?" + query_params if query_params else ""] + var method = "POST" + var headers = {} + var header = "Bearer %s" % p_session.token + headers["Authorization"] = header + + var content : PackedByteArray + content = JSON.stringify(p_body.serialize()).to_utf8_buffer() + + var result = await _http_adapter.send_async(method, uri, headers, content) + if result is NakamaException: + return NakamaAsyncResult.new(result) + return NakamaAsyncResult.new() + + # Get storage objects. + func read_storage_objects_async( + p_session : NakamaSession + , p_body : ApiReadStorageObjectsRequest + ) -> ApiStorageObjects: + var try_refresh = await _refresh_session(p_session) + if try_refresh != null: + if try_refresh.is_exception(): + return ApiStorageObjects.new(try_refresh.get_exception()) + await p_session.refresh(try_refresh) + var urlpath : String = "/v2/storage" + var query_params = "" + var uri = "%s%s%s" % [_base_uri, urlpath, "?" + query_params if query_params else ""] + var method = "POST" + var headers = {} + var header = "Bearer %s" % p_session.token + headers["Authorization"] = header + + var content : PackedByteArray + content = JSON.stringify(p_body.serialize()).to_utf8_buffer() + + var result = await _http_adapter.send_async(method, uri, headers, content) + if result is NakamaException: + return ApiStorageObjects.new(result) + var out : ApiStorageObjects = NakamaSerializer.deserialize(_namespace, "ApiStorageObjects", result) + return out + + # Write objects into the storage engine. + func write_storage_objects_async( + p_session : NakamaSession + , p_body : ApiWriteStorageObjectsRequest + ) -> ApiStorageObjectAcks: + var try_refresh = await _refresh_session(p_session) + if try_refresh != null: + if try_refresh.is_exception(): + return ApiStorageObjectAcks.new(try_refresh.get_exception()) + await p_session.refresh(try_refresh) + var urlpath : String = "/v2/storage" + var query_params = "" + var uri = "%s%s%s" % [_base_uri, urlpath, "?" + query_params if query_params else ""] + var method = "PUT" + var headers = {} + var header = "Bearer %s" % p_session.token + headers["Authorization"] = header + + var content : PackedByteArray + content = JSON.stringify(p_body.serialize()).to_utf8_buffer() + + var result = await _http_adapter.send_async(method, uri, headers, content) + if result is NakamaException: + return ApiStorageObjectAcks.new(result) + var out : ApiStorageObjectAcks = NakamaSerializer.deserialize(_namespace, "ApiStorageObjectAcks", result) + return out + + # Delete one or more objects by ID or username. + func delete_storage_objects_async( + p_session : NakamaSession + , p_body : ApiDeleteStorageObjectsRequest + ) -> NakamaAsyncResult: + var try_refresh = await _refresh_session(p_session) + if try_refresh != null: + if try_refresh.is_exception(): + return NakamaAsyncResult.new(try_refresh.get_exception()) + await p_session.refresh(try_refresh) + var urlpath : String = "/v2/storage/delete" + var query_params = "" + var uri = "%s%s%s" % [_base_uri, urlpath, "?" + query_params if query_params else ""] + var method = "PUT" + var headers = {} + var header = "Bearer %s" % p_session.token + headers["Authorization"] = header + + var content : PackedByteArray + content = JSON.stringify(p_body.serialize()).to_utf8_buffer() + + var result = await _http_adapter.send_async(method, uri, headers, content) + if result is NakamaException: + return NakamaAsyncResult.new(result) + return NakamaAsyncResult.new() + + # List publicly readable storage objects in a given collection. + func list_storage_objects_async( + p_session : NakamaSession + , p_collection : String + , p_user_id = null # : string + , p_limit = null # : integer + , p_cursor = null # : string + ) -> ApiStorageObjectList: + var try_refresh = await _refresh_session(p_session) + if try_refresh != null: + if try_refresh.is_exception(): + return ApiStorageObjectList.new(try_refresh.get_exception()) + await p_session.refresh(try_refresh) + var urlpath : String = "/v2/storage/{collection}" + urlpath = urlpath.replace("{collection}", NakamaSerializer.escape_http(p_collection)) + var query_params = "" + if p_user_id != null: + query_params += "user_id=%s&" % NakamaSerializer.escape_http(p_user_id) + if p_limit != null: + query_params += "limit=%d&" % p_limit + if p_cursor != null: + query_params += "cursor=%s&" % NakamaSerializer.escape_http(p_cursor) + var uri = "%s%s%s" % [_base_uri, urlpath, "?" + query_params if query_params else ""] + var method = "GET" + var headers = {} + var header = "Bearer %s" % p_session.token + headers["Authorization"] = header + + var content : PackedByteArray + + var result = await _http_adapter.send_async(method, uri, headers, content) + if result is NakamaException: + return ApiStorageObjectList.new(result) + var out : ApiStorageObjectList = NakamaSerializer.deserialize(_namespace, "ApiStorageObjectList", result) + return out + + # List publicly readable storage objects in a given collection. + func list_storage_objects2_async( + p_session : NakamaSession + , p_collection : String + , p_user_id : String + , p_limit = null # : integer + , p_cursor = null # : string + ) -> ApiStorageObjectList: + var try_refresh = await _refresh_session(p_session) + if try_refresh != null: + if try_refresh.is_exception(): + return ApiStorageObjectList.new(try_refresh.get_exception()) + await p_session.refresh(try_refresh) + var urlpath : String = "/v2/storage/{collection}/{userId}" + urlpath = urlpath.replace("{collection}", NakamaSerializer.escape_http(p_collection)) + urlpath = urlpath.replace("{userId}", NakamaSerializer.escape_http(p_user_id)) + var query_params = "" + if p_limit != null: + query_params += "limit=%d&" % p_limit + if p_cursor != null: + query_params += "cursor=%s&" % NakamaSerializer.escape_http(p_cursor) + var uri = "%s%s%s" % [_base_uri, urlpath, "?" + query_params if query_params else ""] + var method = "GET" + var headers = {} + var header = "Bearer %s" % p_session.token + headers["Authorization"] = header + + var content : PackedByteArray + + var result = await _http_adapter.send_async(method, uri, headers, content) + if result is NakamaException: + return ApiStorageObjectList.new(result) + var out : ApiStorageObjectList = NakamaSerializer.deserialize(_namespace, "ApiStorageObjectList", result) + return out + + # List current or upcoming tournaments. + func list_tournaments_async( + p_session : NakamaSession + , p_category_start = null # : integer + , p_category_end = null # : integer + , p_start_time = null # : integer + , p_end_time = null # : integer + , p_limit = null # : integer + , p_cursor = null # : string + ) -> ApiTournamentList: + var try_refresh = await _refresh_session(p_session) + if try_refresh != null: + if try_refresh.is_exception(): + return ApiTournamentList.new(try_refresh.get_exception()) + await p_session.refresh(try_refresh) + var urlpath : String = "/v2/tournament" + var query_params = "" + if p_category_start != null: + query_params += "category_start=%d&" % p_category_start + if p_category_end != null: + query_params += "category_end=%d&" % p_category_end + if p_start_time != null: + query_params += "start_time=%d&" % p_start_time + if p_end_time != null: + query_params += "end_time=%d&" % p_end_time + if p_limit != null: + query_params += "limit=%d&" % p_limit + if p_cursor != null: + query_params += "cursor=%s&" % NakamaSerializer.escape_http(p_cursor) + var uri = "%s%s%s" % [_base_uri, urlpath, "?" + query_params if query_params else ""] + var method = "GET" + var headers = {} + var header = "Bearer %s" % p_session.token + headers["Authorization"] = header + + var content : PackedByteArray + + var result = await _http_adapter.send_async(method, uri, headers, content) + if result is NakamaException: + return ApiTournamentList.new(result) + var out : ApiTournamentList = NakamaSerializer.deserialize(_namespace, "ApiTournamentList", result) + return out + + # List tournament records. + func list_tournament_records_async( + p_session : NakamaSession + , p_tournament_id : String + , p_owner_ids = null # : array + , p_limit = null # : integer + , p_cursor = null # : string + , p_expiry = null # : string + ) -> ApiTournamentRecordList: + var try_refresh = await _refresh_session(p_session) + if try_refresh != null: + if try_refresh.is_exception(): + return ApiTournamentRecordList.new(try_refresh.get_exception()) + await p_session.refresh(try_refresh) + var urlpath : String = "/v2/tournament/{tournamentId}" + urlpath = urlpath.replace("{tournamentId}", NakamaSerializer.escape_http(p_tournament_id)) + var query_params = "" + if p_owner_ids != null: + for elem in p_owner_ids: + query_params += "owner_ids=%s&" % elem + if p_limit != null: + query_params += "limit=%d&" % p_limit + if p_cursor != null: + query_params += "cursor=%s&" % NakamaSerializer.escape_http(p_cursor) + if p_expiry != null: + query_params += "expiry=%s&" % NakamaSerializer.escape_http(p_expiry) + var uri = "%s%s%s" % [_base_uri, urlpath, "?" + query_params if query_params else ""] + var method = "GET" + var headers = {} + var header = "Bearer %s" % p_session.token + headers["Authorization"] = header + + var content : PackedByteArray + + var result = await _http_adapter.send_async(method, uri, headers, content) + if result is NakamaException: + return ApiTournamentRecordList.new(result) + var out : ApiTournamentRecordList = NakamaSerializer.deserialize(_namespace, "ApiTournamentRecordList", result) + return out + + # Write a record to a tournament. + func write_tournament_record2_async( + p_session : NakamaSession + , p_tournament_id : String + , p_record : WriteTournamentRecordRequestTournamentRecordWrite + ) -> ApiLeaderboardRecord: + var try_refresh = await _refresh_session(p_session) + if try_refresh != null: + if try_refresh.is_exception(): + return ApiLeaderboardRecord.new(try_refresh.get_exception()) + await p_session.refresh(try_refresh) + var urlpath : String = "/v2/tournament/{tournamentId}" + urlpath = urlpath.replace("{tournamentId}", NakamaSerializer.escape_http(p_tournament_id)) + var query_params = "" + var uri = "%s%s%s" % [_base_uri, urlpath, "?" + query_params if query_params else ""] + var method = "POST" + var headers = {} + var header = "Bearer %s" % p_session.token + headers["Authorization"] = header + + var content : PackedByteArray + content = JSON.stringify(p_record.serialize()).to_utf8_buffer() + + var result = await _http_adapter.send_async(method, uri, headers, content) + if result is NakamaException: + return ApiLeaderboardRecord.new(result) + var out : ApiLeaderboardRecord = NakamaSerializer.deserialize(_namespace, "ApiLeaderboardRecord", result) + return out + + # Write a record to a tournament. + func write_tournament_record_async( + p_session : NakamaSession + , p_tournament_id : String + , p_record : WriteTournamentRecordRequestTournamentRecordWrite + ) -> ApiLeaderboardRecord: + var try_refresh = await _refresh_session(p_session) + if try_refresh != null: + if try_refresh.is_exception(): + return ApiLeaderboardRecord.new(try_refresh.get_exception()) + await p_session.refresh(try_refresh) + var urlpath : String = "/v2/tournament/{tournamentId}" + urlpath = urlpath.replace("{tournamentId}", NakamaSerializer.escape_http(p_tournament_id)) + var query_params = "" + var uri = "%s%s%s" % [_base_uri, urlpath, "?" + query_params if query_params else ""] + var method = "PUT" + var headers = {} + var header = "Bearer %s" % p_session.token + headers["Authorization"] = header + + var content : PackedByteArray + content = JSON.stringify(p_record.serialize()).to_utf8_buffer() + + var result = await _http_adapter.send_async(method, uri, headers, content) + if result is NakamaException: + return ApiLeaderboardRecord.new(result) + var out : ApiLeaderboardRecord = NakamaSerializer.deserialize(_namespace, "ApiLeaderboardRecord", result) + return out + + # Attempt to join an open and running tournament. + func join_tournament_async( + p_session : NakamaSession + , p_tournament_id : String + ) -> NakamaAsyncResult: + var try_refresh = await _refresh_session(p_session) + if try_refresh != null: + if try_refresh.is_exception(): + return NakamaAsyncResult.new(try_refresh.get_exception()) + await p_session.refresh(try_refresh) + var urlpath : String = "/v2/tournament/{tournamentId}/join" + urlpath = urlpath.replace("{tournamentId}", NakamaSerializer.escape_http(p_tournament_id)) + var query_params = "" + var uri = "%s%s%s" % [_base_uri, urlpath, "?" + query_params if query_params else ""] + var method = "POST" + var headers = {} + var header = "Bearer %s" % p_session.token + headers["Authorization"] = header + + var content : PackedByteArray + + var result = await _http_adapter.send_async(method, uri, headers, content) + if result is NakamaException: + return NakamaAsyncResult.new(result) + return NakamaAsyncResult.new() + + # List tournament records for a given owner. + func list_tournament_records_around_owner_async( + p_session : NakamaSession + , p_tournament_id : String + , p_owner_id : String + , p_limit = null # : integer + , p_expiry = null # : string + , p_cursor = null # : string + ) -> ApiTournamentRecordList: + var try_refresh = await _refresh_session(p_session) + if try_refresh != null: + if try_refresh.is_exception(): + return ApiTournamentRecordList.new(try_refresh.get_exception()) + await p_session.refresh(try_refresh) + var urlpath : String = "/v2/tournament/{tournamentId}/owner/{ownerId}" + urlpath = urlpath.replace("{tournamentId}", NakamaSerializer.escape_http(p_tournament_id)) + urlpath = urlpath.replace("{ownerId}", NakamaSerializer.escape_http(p_owner_id)) + var query_params = "" + if p_limit != null: + query_params += "limit=%d&" % p_limit + if p_expiry != null: + query_params += "expiry=%s&" % NakamaSerializer.escape_http(p_expiry) + if p_cursor != null: + query_params += "cursor=%s&" % NakamaSerializer.escape_http(p_cursor) + var uri = "%s%s%s" % [_base_uri, urlpath, "?" + query_params if query_params else ""] + var method = "GET" + var headers = {} + var header = "Bearer %s" % p_session.token + headers["Authorization"] = header + + var content : PackedByteArray + + var result = await _http_adapter.send_async(method, uri, headers, content) + if result is NakamaException: + return ApiTournamentRecordList.new(result) + var out : ApiTournamentRecordList = NakamaSerializer.deserialize(_namespace, "ApiTournamentRecordList", result) + return out + + # Fetch zero or more users by ID and/or username. + func get_users_async( + p_session : NakamaSession + , p_ids = null # : array + , p_usernames = null # : array + , p_facebook_ids = null # : array + ) -> ApiUsers: + var try_refresh = await _refresh_session(p_session) + if try_refresh != null: + if try_refresh.is_exception(): + return ApiUsers.new(try_refresh.get_exception()) + await p_session.refresh(try_refresh) + var urlpath : String = "/v2/user" + var query_params = "" + if p_ids != null: + for elem in p_ids: + query_params += "ids=%s&" % elem + if p_usernames != null: + for elem in p_usernames: + query_params += "usernames=%s&" % elem + if p_facebook_ids != null: + for elem in p_facebook_ids: + query_params += "facebook_ids=%s&" % elem + var uri = "%s%s%s" % [_base_uri, urlpath, "?" + query_params if query_params else ""] + var method = "GET" + var headers = {} + var header = "Bearer %s" % p_session.token + headers["Authorization"] = header + + var content : PackedByteArray + + var result = await _http_adapter.send_async(method, uri, headers, content) + if result is NakamaException: + return ApiUsers.new(result) + var out : ApiUsers = NakamaSerializer.deserialize(_namespace, "ApiUsers", result) + return out + + # List groups the current user belongs to. + func list_user_groups_async( + p_session : NakamaSession + , p_user_id : String + , p_limit = null # : integer + , p_state = null # : integer + , p_cursor = null # : string + ) -> ApiUserGroupList: + var try_refresh = await _refresh_session(p_session) + if try_refresh != null: + if try_refresh.is_exception(): + return ApiUserGroupList.new(try_refresh.get_exception()) + await p_session.refresh(try_refresh) + var urlpath : String = "/v2/user/{userId}/group" + urlpath = urlpath.replace("{userId}", NakamaSerializer.escape_http(p_user_id)) + var query_params = "" + if p_limit != null: + query_params += "limit=%d&" % p_limit + if p_state != null: + query_params += "state=%d&" % p_state + if p_cursor != null: + query_params += "cursor=%s&" % NakamaSerializer.escape_http(p_cursor) + var uri = "%s%s%s" % [_base_uri, urlpath, "?" + query_params if query_params else ""] + var method = "GET" + var headers = {} + var header = "Bearer %s" % p_session.token + headers["Authorization"] = header + + var content : PackedByteArray + + var result = await _http_adapter.send_async(method, uri, headers, content) + if result is NakamaException: + return ApiUserGroupList.new(result) + var out : ApiUserGroupList = NakamaSerializer.deserialize(_namespace, "ApiUserGroupList", result) + return out diff --git a/addons/com.heroiclabs.nakama/api/NakamaAPI.gd.uid b/addons/com.heroiclabs.nakama/api/NakamaAPI.gd.uid new file mode 100644 index 0000000..6260120 --- /dev/null +++ b/addons/com.heroiclabs.nakama/api/NakamaAPI.gd.uid @@ -0,0 +1 @@ +uid://ubpf6wdqaitt diff --git a/addons/com.heroiclabs.nakama/api/NakamaRTAPI.gd b/addons/com.heroiclabs.nakama/api/NakamaRTAPI.gd new file mode 100644 index 0000000..05fc5ae --- /dev/null +++ b/addons/com.heroiclabs.nakama/api/NakamaRTAPI.gd @@ -0,0 +1,902 @@ +extends NakamaAsyncResult + +class_name NakamaRTAPI + +# A chat channel on the server. +class Channel extends NakamaAsyncResult: + + const _SCHEMA = { + "id": {"name": "id", "type": TYPE_STRING, "required": true}, + "presences": {"name": "presences", "type": TYPE_ARRAY, "required": false, "content": "UserPresence"}, + "self": {"name": "self_presence", "type": "UserPresence", "required": true}, + "room_name": {"name": "room_name", "type": TYPE_STRING, "required": false}, + "group_id": {"name": "group_id", "type": TYPE_STRING, "required": false}, + "user_id_one": {"name": "user_id_one", "type": TYPE_STRING, "required": false}, + "user_id_two": {"name": "user_id_two", "type": TYPE_STRING, "required": false} + } + + # The server-assigned channel ID. + var id : String + + # The presences visible on the chat channel. + var presences : Array # of objects NakamaUserPresence + + # The presence of the current user. i.e. Your self. + var self_presence : NakamaRTAPI.UserPresence + + # The name of the chat room, or an empty string if this message was not sent through a chat room. + var room_name : String + + # The ID of the group, or an empty string if this message was not sent through a group channel. + var group_id : String + + # The ID of the first DM user, or an empty string if this message was not sent through a DM chat. + var user_id_one : String + + # The ID of the second DM user, or an empty string if this message was not sent through a DM chat. + var user_id_two : String + + func _init(p_ex = null): + super(p_ex) + + func _to_string(): + if is_exception(): return get_exception()._to_string() + return "Channel" % [ + id, presences, self_presence, room_name, group_id, user_id_one, user_id_two + ] + + static func create(p_ns : GDScript, p_dict : Dictionary) -> Channel: + return _safe_ret(NakamaSerializer.deserialize(p_ns, "Channel", p_dict), Channel) as Channel + + static func get_result_key() -> String: + return "channel" + + +class ChannelMessageAck extends NakamaAsyncResult: + + const _SCHEMA = { + "channel_id": {"name": "channel_id", "type": TYPE_STRING, "required": true}, + "code": {"name": "code", "type": TYPE_INT, "required": true}, + "create_time": {"name": "create_time", "type": TYPE_STRING, "required": false}, + "message_id": {"name": "message_id", "type": TYPE_STRING, "required": true}, + "persistent": {"name": "persistent", "type": TYPE_BOOL, "required": false}, + "update_time": {"name": "update_time", "type": TYPE_STRING, "required": false}, + "username": {"name": "username", "type": TYPE_STRING, "required": false}, + "room_name": {"name": "room_name", "type": TYPE_STRING, "required": false}, + "group_id": {"name": "group_id", "type": TYPE_STRING, "required": false}, + "user_id_one": {"name": "user_id_one", "type": TYPE_STRING, "required": false}, + "user_id_two": {"name": "user_id_two", "type": TYPE_STRING, "required": false} + } + + # The server-assigned channel ID. + var channel_id : String + + # A user-defined code for the chat message. + var code : int + + # The UNIX time when the message was created. + var create_time : String + + # A unique ID for the chat message. + var message_id : String + + # True if the chat message has been stored in history. + var persistent : bool + + # The UNIX time when the message was updated. + var update_time : String + + # The username of the sender of the message. + var username : String + + # The name of the chat room, or an empty string if this message was not sent through a chat room. + var room_name : String + + # The ID of the group, or an empty string if this message was not sent through a group channel. + var group_id : String + + # The ID of the first DM user, or an empty string if this message was not sent through a DM chat. + var user_id_one : String + + # The ID of the second DM user, or an empty string if this message was not sent through a DM chat. + var user_id_two : String + + func _init(p_ex = null): + super(p_ex) + + func _to_string(): + if is_exception(): return get_exception()._to_string() + return "ChannelMessageAck" % [ + channel_id, code, create_time, message_id, persistent, update_time, username, room_name, group_id, user_id_one, user_id_two + ] + + static func create(p_ns : GDScript, p_dict : Dictionary) -> ChannelMessageAck: + return _safe_ret(NakamaSerializer.deserialize(p_ns, "ChannelMessageAck", p_dict), ChannelMessageAck) as ChannelMessageAck + + static func get_result_key() -> String: + return "channel_message_ack" + + +# A batch of join and leave presences on a chat channel. +class ChannelPresenceEvent extends NakamaAsyncResult: + + const _SCHEMA = { + "channel_id": {"name": "channel_id", "type": TYPE_STRING, "required": true}, + "joins": {"name": "joins", "type": TYPE_ARRAY, "required": false, "content" : "UserPresence"}, + "leaves": {"name": "leaves", "type": TYPE_ARRAY, "required": false, "content" : "UserPresence"}, + "room_name": {"name": "room_name", "type": TYPE_STRING, "required": false}, + "group_id": {"name": "group_id", "type": TYPE_STRING, "required": false}, + "user_id_one": {"name": "user_id_one", "type": TYPE_STRING, "required": false}, + "user_id_two": {"name": "user_id_two", "type": TYPE_STRING, "required": false} + } + + # The unique identifier of the chat channel. + var channel_id : String + + # Presences of the users who joined the channel. + var joins : Array # UserPresence + + # Presences of users who left the channel. + var leaves : Array # UserPresence + + # The name of the chat room, or an empty string if this message was not sent through a chat room. + var room_name : String + + # The ID of the group, or an empty string if this message was not sent through a group channel. + var group_id : String + + # The ID of the first DM user, or an empty string if this message was not sent through a DM chat. + var user_id_one : String + + # The ID of the second DM user, or an empty string if this message was not sent through a DM chat. + var user_id_two : String + + func _init(p_ex = null): + super(p_ex) + + func _to_string(): + if is_exception(): return get_exception()._to_string() + return "ChannelPresenceEvent" % [ + channel_id, joins, leaves, room_name, group_id, user_id_one, user_id_two + ] + + static func create(p_ns : GDScript, p_dict : Dictionary) -> ChannelPresenceEvent: + return _safe_ret(NakamaSerializer.deserialize(p_ns, "ChannelPresenceEvent", p_dict), ChannelPresenceEvent) as ChannelPresenceEvent + + static func get_result_key() -> String: + return "channel_presence_event" + + +# Describes an error which occurred on the server. +class Error extends NakamaAsyncResult: + + const _SCHEMA = { + "code": {"name": "code", "type": TYPE_INT, "required": true}, + "message": {"name": "message", "type": TYPE_STRING, "required": true}, + "context": {"name": "context", "type": TYPE_DICTIONARY, "required": false, "content": TYPE_STRING}, + } + + # The selection of possible error codes. + enum Code { + # An unexpected result from the server. + RUNTIME_EXCEPTION = 0, + # The server received a message which is not recognised. + UNRECOGNIZED_PAYLOAD = 1, + # A message was expected but contains no content. + MISSING_PAYLOAD = 2, + # Fields in the message have an invalid format. + BAD_INPUT = 3, + # The match id was not found. + MATCH_NOT_FOUND = 4, + # The match join was rejected. + MATCH_JOIN_REJECTED = 5, + # The runtime function does not exist on the server. + RUNTIME_FUNCTION_NOT_FOUND = 6, + #The runtime function executed with an error. + RUNTIME_FUNCTION_EXCEPTION = 7, + } + + # The error code which should be one of "Error.Code" enums. + var code : int + + # A message in English to help developers debug the response. + var message : String + + # Additional error details which may be different for each response. + var context : Dictionary + + func _init(p_ex = null): + super(p_ex) + + func _to_string(): + if is_exception(): return get_exception()._to_string() + return "Error" % [code, message, context] + + static func create(p_ns : GDScript, p_dict : Dictionary) -> Error: + return _safe_ret(NakamaSerializer.deserialize(p_ns, "Error", p_dict), Error) as Error + + static func get_result_key() -> String: + return "error" + + +# A multiplayer match. +class Match extends NakamaAsyncResult: + + const _SCHEMA = { + "authoritative": {"name": "authoritative", "type": TYPE_BOOL, "required": false}, + "match_id": {"name": "match_id", "type": TYPE_STRING, "required": true}, + "label": {"name": "label", "type": TYPE_STRING, "required": false}, + "presences": {"name": "presences", "type": TYPE_ARRAY, "required": false, "content": "UserPresence"}, + "size": {"name": "size", "type": TYPE_INT, "required": false}, + "self": {"name": "self_user", "type": "UserPresence", "required": true} + } + + # If this match has an authoritative handler on the server. + var authoritative : bool + + # The unique match identifier. + var match_id : String + + # A label for the match which can be filtered on. + var label : String + + # The presences already in the match. + var presences : Array # UserPresence + + # The number of users currently in the match. + var size : int + + # The current user in this match. i.e. Yourself. + var self_user : UserPresence + + func _init(p_ex = null): + super(p_ex) + + static func create(p_ns : GDScript, p_dict : Dictionary): + return _safe_ret(NakamaSerializer.deserialize(p_ns, "Match", p_dict), Match) as Match + + func _to_string(): + if is_exception(): return get_exception()._to_string() + return "Match" % [authoritative, match_id, label, presences, size, self_user] + + static func get_result_key() -> String: + return "match" + + +# Some game state update in a match. +class MatchData extends NakamaAsyncResult: + const _SCHEMA = { + "match_id": {"name": "match_id", "type": TYPE_STRING, "required": true}, + "presence": {"name": "presence", "type": "UserPresence", "required": false}, + "op_code": {"name": "op_code", "type": TYPE_INT, "required": false}, + "data": {"name": "data", "type": TYPE_STRING, "required": false} + } + + # The unique match identifier. + var match_id : String + + # The operation code for the state change. + # This value can be used to mark the type of the contents of the state. + var op_code : int = 0 + + # The user that sent this game state update. + var presence : UserPresence + + # The raw base64-encoded contents of the state change. + var base64_data : String + + # The contents of the state change decoded as a UTF-8 string. + var _data + var data : String: + get: + if _data == null and base64_data != '': + _data = Marshalls.base64_to_utf8(base64_data) + return _data if _data != null else '' + set(v): + _data = v + + # The contents of the state change decoded as binary data. + var _binary_data + var binary_data : PackedByteArray: + get: + if _binary_data == null and base64_data != '': + _binary_data = Marshalls.base64_to_raw(base64_data) + return _binary_data + + func _init(p_ex = null): + super(p_ex) + + func _to_string(): + if is_exception(): return get_exception()._to_string() + return "MatchData" % [match_id, op_code, presence, data] + + static func create(p_ns : GDScript, p_dict : Dictionary) -> MatchData: + var out = _safe_ret(NakamaSerializer.deserialize(p_ns, "MatchData", p_dict), MatchData) as MatchData + # Store the base64 data, ready to be decoded when the developer requests it. + if out._data != null: + out.base64_data = out._data + out._data = null + return out + + static func get_result_key() -> String: + return "match_data" + + +# A batch of join and leave presences for a match. +class MatchPresenceEvent extends NakamaAsyncResult: + const _SCHEMA = { + "match_id": {"name": "match_id", "type": TYPE_STRING, "required": true}, + "joins": {"name": "joins", "type": TYPE_ARRAY, "required": false, "content" : "UserPresence"}, + "leaves": {"name": "leaves", "type": TYPE_ARRAY, "required": false, "content" : "UserPresence"}, + } + + # Presences of users who joined the match. + var joins : Array + + # Presences of users who left the match. + var leaves : Array + + # The unique match identifier. + var match_id : String + + func _init(p_ex = null): + super(p_ex) + + func _to_string(): + if is_exception(): return get_exception()._to_string() + return "MatchPresenceEvent" % [match_id, joins, leaves] + + static func create(p_ns : GDScript, p_dict : Dictionary) -> MatchPresenceEvent: + return _safe_ret(NakamaSerializer.deserialize(p_ns, "MatchPresenceEvent", p_dict), MatchPresenceEvent) as MatchPresenceEvent + + static func get_result_key() -> String: + return "match_presence_event" + + +# The result of a successful matchmaker operation sent to the server. +class MatchmakerMatched extends NakamaAsyncResult: + + const _SCHEMA = { + "match_id": {"name": "match_id", "type": TYPE_STRING, "required": false}, + "ticket": {"name": "ticket", "type": TYPE_STRING, "required": true}, + "token": {"name": "token", "type": TYPE_STRING, "required": false}, + "users": {"name": "users", "type": TYPE_ARRAY, "required": false, "content": "MatchmakerUser"}, + "self": {"name": "self_user", "type": "MatchmakerUser", "required": true} + } + + # The id used to join the match. + # A match ID used to join the match. + var match_id : String + + # The ticket sent by the server when the user requested to matchmake for other players. + var ticket : String + + # The token used to join a match. + var token : String + + # The other users matched with this user and the parameters they sent. + var users : Array # MatchmakerUser + + # The current user who matched with opponents. + var self_user : MatchmakerUser + + func _init(p_ex = null): + super(p_ex) + + func _to_string(): + if is_exception(): return get_exception()._to_string() + return "" % [ + match_id, ticket, token, users, self_user + ] + + static func create(p_ns : GDScript, p_dict : Dictionary) -> MatchmakerMatched: + return _safe_ret(NakamaSerializer.deserialize(p_ns, "MatchmakerMatched", p_dict), MatchmakerMatched) as MatchmakerMatched + + static func get_result_key() -> String: + return "matchmaker_matched" + + +# The matchmaker ticket received from the server. +class MatchmakerTicket extends NakamaAsyncResult: + + const _SCHEMA = { + "ticket": {"name": "ticket", "type": TYPE_STRING, "required": true} + } + + # The ticket generated by the matchmaker. + var ticket : String + + func _init(p_ex = null): + super(p_ex) + + static func create(p_ns : GDScript, p_dict : Dictionary) -> MatchmakerTicket: + return _safe_ret(NakamaSerializer.deserialize(p_ns, "MatchmakerTicket", p_dict), MatchmakerTicket) as MatchmakerTicket + + func _to_string(): + if is_exception(): return get_exception()._to_string() + return "" % ticket + + static func get_result_key() -> String: + return "matchmaker_ticket" + + +# The user with the parameters they sent to the server when asking for opponents. +class MatchmakerUser extends NakamaAsyncResult: + + const _SCHEMA = { + "presence": {"name": "presence", "type": "UserPresence", "required": true}, + "party_id": {"name": "party_id", "type": TYPE_STRING, "required": false}, + "string_properties": {"name": "string_properties", "type": TYPE_DICTIONARY, "required": false, "content": TYPE_STRING}, + "numeric_properties": {"name": "numeric_properties", "type": TYPE_DICTIONARY, "required": false, "content": TYPE_FLOAT}, + } + + # The presence of the user. + var presence : UserPresence + + # Party identifier, if this user was matched as a party member. + var party_id : String + + # The numeric properties which this user asked to matchmake with. + var numeric_properties : Dictionary + + # The string properties which this user asked to matchmake with. + var string_properties : Dictionary + + func _init(p_ex = null): + super(p_ex) + + func _to_string(): + if is_exception(): return get_exception()._to_string() + return "" % [ + presence, numeric_properties, string_properties] + + static func create(p_ns : GDScript, p_dict : Dictionary) -> MatchmakerUser: + return _safe_ret(NakamaSerializer.deserialize(p_ns, "MatchmakerUser", p_dict), MatchmakerUser) as MatchmakerUser + + static func get_result_key() -> String: + return "matchmaker_user" + + +# Receive status updates for users. +class Status extends NakamaAsyncResult: + + const _SCHEMA = { + "presences": {"name": "presences", "type": TYPE_ARRAY, "required": true, "content": "UserPresence"}, + } + + # The status events for the users followed. + var presences := Array() + + func _init(p_ex = null): + super(p_ex) + + static func create(p_ns : GDScript, p_dict : Dictionary) -> Status: + return _safe_ret(NakamaSerializer.deserialize(p_ns, "Status", p_dict), Status) as Status + + func _to_string(): + if is_exception(): return get_exception()._to_string() + return "" % [presences] + + static func get_result_key() -> String: + return "status" + + +# A status update event about other users who've come online or gone offline. +class StatusPresenceEvent extends NakamaAsyncResult: + const _SCHEMA = { + "joins": {"name": "joins", "type": TYPE_ARRAY, "required": false, "content" : "UserPresence"}, + "leaves": {"name": "leaves", "type": TYPE_ARRAY, "required": false, "content" : "UserPresence"}, + } + + # Presences of users who joined the server. + # This join information is in response to a subscription made to be notified when a user comes online. + var joins : Array + + # Presences of users who left the server. + # This leave information is in response to a subscription made to be notified when a user goes offline. + var leaves : Array + + func _init(p_ex = null): + super(p_ex) + + func _to_string(): + if is_exception(): return get_exception()._to_string() + return "StatusPresenceEvent" % [joins, leaves] + + static func create(p_ns : GDScript, p_dict : Dictionary) -> StatusPresenceEvent: + return _safe_ret(NakamaSerializer.deserialize(p_ns, "StatusPresenceEvent", p_dict), StatusPresenceEvent) as StatusPresenceEvent + + static func get_result_key() -> String: + return "status_presence_event" + + +# A realtime socket stream on the server. +class Stream extends NakamaAsyncResult: + + const _SCHEMA = { + "mode": {"name": "mode", "type": TYPE_INT, "required": true}, + "subject": {"name": "subject", "type": TYPE_STRING, "required": false}, + "subcontext": {"name": "subcontext", "type": TYPE_STRING, "required": false}, + "label": {"name": "label", "type": TYPE_STRING, "required": false}, + } + + # The mode of the stream. + var mode : int + + # The subject of the stream. This is usually a user id. + var subject : String + + # The descriptor of the stream. Used with direct chat messages and contains a second user id. + var subcontext : String + + # Identifies streams which have a context across users like a chat channel room. + var label : String + + func _init(p_ex = null): + super(p_ex) + + func _to_string(): + if is_exception(): return get_exception()._to_string() + return "Stream" % [mode, subject, subcontext, label] + + static func create(p_ns : GDScript, p_dict : Dictionary) -> Stream: + return _safe_ret(NakamaSerializer.deserialize(p_ns, "Stream", p_dict), Stream) as Stream + + static func get_result_key() -> String: + return "stream" + + +# A batch of joins and leaves on the low level stream. +# Streams are built on to provide abstractions for matches, chat channels, etc. In most cases you'll never need to +# interact with the low level stream itself. +class StreamPresenceEvent extends NakamaAsyncResult: + const _SCHEMA = { + "stream": {"name": "stream", "type": "Stream", "required": true}, + "joins": {"name": "joins", "type": TYPE_ARRAY, "required": false, "content" : "UserPresence"}, + "leaves": {"name": "leaves", "type": TYPE_ARRAY, "required": false, "content" : "UserPresence"}, + } + + # Presences of users who left the stream. + var joins : Array + + # Presences of users who joined the stream. + var leaves : Array + + # The identifier for the stream. + var stream : Stream = null + + func _to_string(): + if is_exception(): return get_exception()._to_string() + return "StreamPresenceEvent" % [stream, joins, leaves] + + static func create(p_ns : GDScript, p_dict : Dictionary) -> StreamPresenceEvent: + return _safe_ret(NakamaSerializer.deserialize(p_ns, "StreamPresenceEvent", p_dict), StreamPresenceEvent) as StreamPresenceEvent + + static func get_result_key() -> String: + return "stream_presence_event" + + +# A state change received from a stream. +class StreamData extends NakamaAsyncResult: + + const _SCHEMA = { + "stream": {"name": "stream", "type": "Stream", "required": true}, + "sender": {"name": "sender", "type": "UserPresence", "required": false}, + "data": {"name": "state", "type": TYPE_STRING, "required": false}, + "reliable": {"name": "reliable", "type": TYPE_BOOL, "required": false}, + } + + # The user who sent the state change. May be `null`. + var sender : UserPresence = null + + # The contents of the state change. + var state : String + + # The identifier for the stream. + var stream : Stream + + # True if this data was delivered reliably, false otherwise. + var reliable : bool + + func _to_string(): + if is_exception(): return get_exception()._to_string() + return "StreamData" % [sender, state, stream] + + static func create(p_ns : GDScript, p_dict : Dictionary) -> StreamData: + return _safe_ret(NakamaSerializer.deserialize(p_ns, "StreamData", p_dict), StreamData) as StreamData + + static func get_result_key() -> String: + return "stream_data" + + +# An object which represents a connected user in the server. +# The server allows the same user to be connected with multiple sessions. To uniquely identify them a tuple of +# `{ node_id, user_id, session_id }` is used which is exposed as this object. +class UserPresence extends NakamaAsyncResult: + + const _SCHEMA = { + "persistence": {"name": "persistence", "type": TYPE_BOOL, "required": false}, + "session_id": {"name": "session_id", "type": TYPE_STRING, "required": true}, + "status": {"name": "status", "type": TYPE_STRING, "required": false}, + "username": {"name": "username", "type": TYPE_STRING, "required": false}, + "user_id": {"name": "user_id", "type": TYPE_STRING, "required": true}, + } + + # If this presence generates stored events like persistent chat messages or notifications. + var persistence : bool + + # The session id of the user. + var session_id : String + + # The status of the user with the presence on the server. + var status : String + + # The username for the user. + var username : String + + # The id of the user. + var user_id : String + + func _init(p_ex = null): + super(p_ex) + + func serialize() -> Dictionary: + return NakamaSerializer.serialize(self) + + func _to_string(): + if is_exception(): return get_exception()._to_string() + return "UserPresence" % [ + persistence, session_id, status, username, user_id] + + static func create(p_ns : GDScript, p_dict : Dictionary) -> UserPresence: + return _safe_ret(NakamaSerializer.deserialize(p_ns, "UserPresence", p_dict), UserPresence) as UserPresence + + static func get_result_key() -> String: + return "user_presence" + + +class Party extends NakamaAsyncResult: + + const _SCHEMA = { + "party_id": {"name": "party_id", "type": TYPE_STRING, "required": true}, + "open": {"name": "open", "type": TYPE_BOOL, "required": false}, + "max_size": {"name": "max_size", "type": TYPE_INT, "required": true}, + "self": {"name": "self_presence", "type": "UserPresence", "required": true}, + "leader": {"name": "leader", "type": "UserPresence", "required": true}, + "presences": {"name": "presences", "type": TYPE_ARRAY, "required": false, "content": "UserPresence"}, + } + + # Unique party identifier. + var party_id : String + + # Open flag. + var open : bool = false + + # Maximum number of party members. + var max_size : int + + # The presence of the current user. i.e. Your self. + var self_presence : NakamaRTAPI.UserPresence + + # Leader. + var leader : NakamaRTAPI.UserPresence + + # All current party members. + var presences : Array # of objects NakamaUserPresence + + func _init(p_ex = null): + super(p_ex) + + func serialize() -> Dictionary: + return NakamaSerializer.serialize(self) + + func _to_string(): + if is_exception(): return get_exception()._to_string() + return "Party" % [ + party_id, open, max_size, self_presence, leader, presences] + + static func create(p_ns : GDScript, p_dict : Dictionary) -> Party: + return _safe_ret(NakamaSerializer.deserialize(p_ns, "Party", p_dict), Party) as Party + + static func get_result_key() -> String: + return "party" + + +# Presence update for a particular party. +class PartyPresenceEvent extends NakamaAsyncResult: + const _SCHEMA = { + "party_id": {"name": "party_id", "type": TYPE_STRING, "required": true}, + "joins": {"name": "joins", "type": TYPE_ARRAY, "required": false, "content": "UserPresence"}, + "leaves": {"name": "leaves", "type": TYPE_ARRAY, "required": false, "content": "UserPresence"}, + } + # The party ID. + var party_id : String + # User presences that have just joined the party. + var joins : Array + # User presences that have just left the party. + var leaves : Array + + func _init(p_ex = null): + super(p_ex) + + func serialize() -> Dictionary: + return NakamaSerializer.serialize(self) + + func _to_string(): + if is_exception(): return get_exception()._to_string() + return "PartyPresenceEvent" % [party_id, joins, leaves] + + static func create(p_ns : GDScript, p_dict : Dictionary) -> PartyPresenceEvent: + return _safe_ret(NakamaSerializer.deserialize(p_ns, "PartyPresenceEvent", p_dict), PartyPresenceEvent) as PartyPresenceEvent + + static func get_result_key() -> String: + return "party_presence_event" + + +# Announcement of a new party leader. +class PartyLeader extends NakamaAsyncResult: + const _SCHEMA = { + "party_id": {"name": "party_id", "type": TYPE_STRING, "required": true}, + "presence": {"name": "presence", "type": "UserPresence", "required": true}, + } + # Party ID to promote a new leader for. + var party_id : String + # The presence of an existing party member to promote as the new leader. + var presence : NakamaRTAPI.UserPresence + + func _init(p_ex = null): + super(p_ex) + + func serialize() -> Dictionary: + return NakamaSerializer.serialize(self) + + func _to_string(): + if is_exception(): return get_exception()._to_string() + return "PartyLeader" % [party_id, presence] + + static func create(p_ns : GDScript, p_dict : Dictionary) -> PartyLeader: + return _safe_ret(NakamaSerializer.deserialize(p_ns, "PartyLeader", p_dict), PartyLeader) as PartyLeader + + static func get_result_key() -> String: + return "party_leader" + + +# Incoming notification for one or more new presences attempting to join the party. +class PartyJoinRequest extends NakamaAsyncResult: + const _SCHEMA = { + "party_id": {"name": "party_id", "type": TYPE_STRING, "required": true}, + "presences": {"name": "presences", "type": TYPE_ARRAY, "required": false, "content": "UserPresence"}, + } + # Party ID these presences are attempting to join. + var party_id : String + # Presences attempting to join. + var presences : Array + + func _init(p_ex = null): + super(p_ex) + + func serialize() -> Dictionary: + return NakamaSerializer.serialize(self) + + func _to_string(): + if is_exception(): return get_exception()._to_string() + return "PartyJoinRequest" % [party_id, presences] + + static func create(p_ns : GDScript, p_dict : Dictionary) -> PartyJoinRequest: + return _safe_ret(NakamaSerializer.deserialize(p_ns, "PartyJoinRequest", p_dict), PartyJoinRequest) as PartyJoinRequest + + static func get_result_key() -> String: + return "party_join_request" + + +# A response from starting a new party matchmaking process. +class PartyMatchmakerTicket extends NakamaAsyncResult: + const _SCHEMA = { + "party_id": {"name": "party_id", "type": TYPE_STRING, "required": true}, + "ticket": {"name": "ticket", "type": TYPE_STRING, "required": true}, + } + # Party ID. + var party_id : String + # The ticket that can be used to cancel matchmaking. + var ticket : String + + func _init(p_ex = null): + super(p_ex) + + func serialize() -> Dictionary: + return NakamaSerializer.serialize(self) + + func _to_string(): + if is_exception(): return get_exception()._to_string() + return "PartyMatchmakerTicket" % [party_id, ticket] + + static func create(p_ns : GDScript, p_dict : Dictionary) -> PartyMatchmakerTicket: + return _safe_ret(NakamaSerializer.deserialize(p_ns, "PartyMatchmakerTicket", p_dict), PartyMatchmakerTicket) as PartyMatchmakerTicket + + static func get_result_key() -> String: + return "party_matchmaker_ticket" + + +# Incoming party data delivered from the server. +class PartyData extends NakamaAsyncResult: + const _SCHEMA = { + "party_id": {"name": "party_id", "type": TYPE_STRING, "required": true}, + "presence": {"name": "presence", "type": "UserPresence", "required": false}, + "op_code": {"name": "op_code", "type": TYPE_INT, "required": true}, + "data": {"name": "data", "type": TYPE_STRING, "required": false} + } + # The party ID. + var party_id : String + # A reference to the user presence that sent this data, if any. + var presence : NakamaRTAPI.UserPresence + # Op code value. + var op_code : int + + # The raw base64-encoded contents of the state change. + var base64_data : String + + # The contents of the state change decoded as a UTF-8 string. + var _data + var data : String: + get: + if _data == null and base64_data != '': + _data = Marshalls.base64_to_utf8(base64_data) + return _data if _data != null else '' + set(v): + _data = v + + # The contents of the state change decoded as binary data. + var _binary_data + var binary_data : PackedByteArray: + get: + if _binary_data == null and base64_data != '': + _binary_data = Marshalls.base64_to_raw(base64_data) + return _binary_data + + func _init(p_ex = null): + super(p_ex) + + func serialize() -> Dictionary: + return NakamaSerializer.serialize(self) + + func _to_string(): + if is_exception(): return get_exception()._to_string() + return "PartyData" % [party_id, presence, op_code, data] + + static func create(p_ns : GDScript, p_dict : Dictionary) -> PartyData: + var out := _safe_ret(NakamaSerializer.deserialize(p_ns, "PartyData", p_dict), PartyData) as PartyData + # Store the base64 data, ready to be decoded when the developer requests it. + if out._data != null: + out.base64_data = out._data + out._data = null + return out + + static func get_result_key() -> String: + return "party_data" + +# End a party, kicking all party members and closing it. (this is both a message and a result) +class PartyClose extends NakamaAsyncResult: + const _SCHEMA = { + "party_id": {"name": "party_id", "type": TYPE_STRING, "required": true}, + } + # Party ID to close. + var party_id : String + + func _init(p_ex = null): + super(p_ex) + + func serialize(): + return NakamaSerializer.serialize(self) + + func get_msg_key() -> String: + return "party_close" + + func _to_string(): + if is_exception(): return get_exception()._to_string() + return "PartyClose" % [party_id] + + static func create(p_ns : GDScript, p_dict : Dictionary) -> PartyClose: + return _safe_ret(NakamaSerializer.deserialize(p_ns, "PartyClose", p_dict), PartyClose) as PartyClose + + static func get_result_key() -> String: + return "party_close" diff --git a/addons/com.heroiclabs.nakama/api/NakamaRTAPI.gd.uid b/addons/com.heroiclabs.nakama/api/NakamaRTAPI.gd.uid new file mode 100644 index 0000000..684d739 --- /dev/null +++ b/addons/com.heroiclabs.nakama/api/NakamaRTAPI.gd.uid @@ -0,0 +1 @@ +uid://bmho1of843y0x diff --git a/addons/com.heroiclabs.nakama/api/NakamaRTMessage.gd b/addons/com.heroiclabs.nakama/api/NakamaRTMessage.gd new file mode 100644 index 0000000..d3bedd6 --- /dev/null +++ b/addons/com.heroiclabs.nakama/api/NakamaRTMessage.gd @@ -0,0 +1,635 @@ +extends RefCounted +class_name NakamaRTMessage + +# Send a channel join message to the server. +class ChannelJoin: + + const _SCHEMA = { + "persistence": {"name": "persistence", "type": TYPE_BOOL, "required": true}, + "hidden": {"name": "hidden", "type": TYPE_BOOL, "required": true}, + "target": {"name": "target", "type": TYPE_STRING, "required": true}, + "type": {"name": "type", "type": TYPE_INT, "required": true}, + } + + enum ChannelType { + # A chat room which can be created dynamically with a name. + Room = 1, + # A private chat between two users. + DirectMessage = 2, + # A chat within a group on the server. + Group = 3 + } + + var persistence : bool + var hidden : bool + var target : String + var type : int + + func _init(p_target : String, p_type : int, p_persistence : bool, p_hidden : bool): + persistence = p_persistence + hidden = p_hidden + target = p_target + type = p_type if p_type >= ChannelType.Room and p_type <= ChannelType.Group else 0 # Will cause error server side + + func serialize() -> Dictionary: + return NakamaSerializer.serialize(self) + + func get_msg_key() -> String: + return "channel_join" + + func _to_string(): + return "ChannelJoin