Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,10 @@ ANDROID_HOME=
ANDROID_SDK_ROOT=
MOONLIT_PYTHON=

# Apple Developer Team ID that `pnpm ios:*` signs with. Empty keeps the team
# that publishes Moonlit Beacon; set your own to build a copy of the game.
MOONLIT_APPLE_TEAM_ID=

# Values that go out on the store listing as-is.
MOONLIT_SUPPORT_EMAIL=
MOONLIT_PUBLIC_SITE_URL=
1 change: 1 addition & 0 deletions .github/scripts/check-hygiene.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ const FORBIDDEN = [
[/\.godot\//, '.godot/ is committed. It contains the signing keystore password'],
[/\.zip$/, 'an original asset ZIP is committed'],
[/(^|\/)_tmp_/, 'a render temp file (_tmp_*) is committed'],
[/(^|\/)iapkit(_publishable)?\.cfg$/, 'an IAPKit key file is committed; exports generate it'],
];
for (const file of tracked) {
for (const [pattern, why] of FORBIDDEN) {
Expand Down
4 changes: 4 additions & 0 deletions .github/scripts/check-locale.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -65,9 +65,13 @@ const KEY = /\b[A-Z][A-Z0-9]*(?:_[A-Z0-9]+)+\b/g;
// Looks like a key but is not. Engine constants and node names.
const NOT_KEYS = /^(?:[A-Z]+_[A-Z0-9_]*(?:MODE|FILTER|PRESET|DIRECTION|ALIGNMENT|MASK|LAYER)|SUB_RESOURCE|EXT_RESOURCE|GD_SCENE|PACKED[A-Z0-9]*ARRAY|STYLE_BOX[A-Z_]*|NODE_PATH)$/;

// The vendored godot-iap addon never uses the game's keys; its env-var names look like keys.
const VENDORED_IAP = join(GAME, 'addons', 'godot-iap');

const walk = (dir) => readdirSync(dir).flatMap((name) => {
if (name === '.godot' || name === 'assets') return [];
const full = join(dir, name);
if (full === VENDORED_IAP) return [];
return statSync(full).isDirectory() ? walk(full) : [full];
});

Expand Down
8 changes: 4 additions & 4 deletions .github/workflows/android.yml
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ jobs:
echo "::error::Direct-distribution APK contains tests or production tools"
exit 1
fi
if grep -Eq '(^|/)iapkit\.cfg$' <<<"$APK_ENTRIES"; then
if grep -Eq '(^|/)iapkit_publishable\.cfg$' <<<"$APK_ENTRIES"; then
echo "::error::Direct-distribution APK contains IAPKit config"
exit 1
fi
Expand Down Expand Up @@ -216,12 +216,12 @@ jobs:
echo "::error::Play AAB contains tests or production tools"
exit 1
fi
IAPKIT_CFG_COUNT=$(grep -Ec '(^|/)iapkit\.cfg$' <<<"$AAB_ENTRIES")
IAPKIT_CFG_COUNT=$(grep -Ec '(^|/)iapkit_publishable\.cfg$' <<<"$AAB_ENTRIES")
if [ "$IAPKIT_CFG_COUNT" -ne 1 ]; then
echo "::error::Play AAB IAPKit config count is not 1"
exit 1
fi
IAPKIT_CFG=$(unzip -p "$AAB" '*/assets/iapkit.cfg')
IAPKIT_CFG=$(unzip -p "$AAB" '*/assets/iapkit_publishable.cfg')
if ! grep -Eq '^api_key="openiap-kit_pk_[A-Za-z0-9_-]{32,}"$' \
<<<"$IAPKIT_CFG"; then
echo "::error::Play AAB IAPKit publishable config format is invalid"
Expand All @@ -231,7 +231,7 @@ jobs:
echo "::error::Play AAB contains an IAPKit secret/admin key"
exit 1
fi
if [ -e apps/game/android/build/assetPackInstallTime/src/main/assets/iapkit.cfg ]; then
if [ -e apps/game/android/build/assetPackInstallTime/src/main/assets/iapkit_publishable.cfg ]; then
echo "::error::IAPKit config remained in the Gradle template after export"
exit 1
fi
Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ builds/
# copies game assets into them, so they are not committed.
apps/game/android/
# IAPKit publishable config created only during store export. Not a lasting source.
apps/game/iapkit_publishable.cfg
# godot-iap's own local settings file (dropped from release exports). Never commit it.
apps/game/iapkit.cfg
# Do not add a global `*.zip` rule.
# Original asset zips are already excluded via _downloads/ and
Expand Down
15 changes: 8 additions & 7 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -168,10 +168,10 @@ included. Changing one GDScript doc comment changes `runtime_sha256` and
invalidates the phone, 7-inch, 10-inch, and iPad sets, even if the pixels
did not move.

**`export_presets.cfg` and `iapkit.cfg` are excluded.** Bumping version and
build numbers does not break the fingerprint. That is a fingerprint fact;
the recapture rule above ("lock the version before capture") is separate
and still applies.
**`export_presets.cfg` and `iapkit_publishable.cfg` are excluded.** Bumping
version and build numbers does not break the fingerprint. That is a
fingerprint fact; the recapture rule above ("lock the version before
capture") is separate and still applies.

**If you touched `apps/game/`, run this separately:**

Expand Down Expand Up @@ -562,6 +562,7 @@ tool environments. Only `ios:upload` actually sends a binary;
`pnpm android:bundle` and iOS export need an IAPKit publishable key for
purchase verification. They look at `IAPKIT_API_KEY` first, and on macOS
fall back to the Keychain service `dev.openiap.kit.moonlitbeacon`, account
`MoonlitBeacon Mobile`. The generated `apps/game/iapkit.cfg` is deleted
after export and is not in Git. Never put an `openiap-kit_sk_…`
secret/admin key in an app build.
`MoonlitBeacon Mobile`. The generated `apps/game/iapkit_publishable.cfg` is
deleted after export and is not in Git. It is not named `iapkit.cfg`
because godot-iap 3.6 leaves that file out of release exports. Never put an
`openiap-kit_sk_…` secret/admin key in an app build.
5 changes: 3 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ pnpm verify # same checks CI runs

Store signing keys, IAPKit secrets, and App Store Connect keys stay on the
machine. Copy `.env.example` to `.env` and fill only what you need. Never
commit `.env`, `.godot/`, `*.jks`, `*.p8`, or `iapkit.cfg`.
commit `.env`, `.godot/`, `*.jks`, `*.p8`, `iapkit_publishable.cfg`, or
`iapkit.cfg`.

Fork pull requests build the debug APK only. GitHub withholds secrets from
forks, so the store AAB step (it needs `IAPKIT_API_KEY`) and its checks are
Expand All @@ -38,7 +39,7 @@ skipped there — that skip is expected, not a failure.

## godot-iap

The plugin is vendored from the official `godot-iap-3.5.1` zip with a small
The plugin is vendored from the official `godot-iap-3.6.1` zip with a small
project patch documented in `vendor/godot-iap/README.md`.
`pnpm test:godot-iap-vendor` checks that the patch reverses to the official
bytes.
Expand Down
6 changes: 0 additions & 6 deletions LICENSE
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,3 @@ 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.

---
Third-party assets in this repository (fonts, art, audio — see
`apps/docs/docs/assets/third-party.md`) keep their own licenses. In
particular the Nexon MapleStory font allows commercial use and embedding
but forbids sale and modification.
82 changes: 60 additions & 22 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,10 @@ Docs site: **https://hyodotdev.github.io/MoonlitBeacon/**
| Stretch | `canvas_items` + `expand`, nearest-neighbor filter |
| Reference device | Pixel 10 — 2424 × 1080 landscape, arm64-v8a |
| Controls | Full-screen floating move stick, auto-attack, dash button |
| IAP | **godot-iap 3.5.1** + IAPKit (publishable key at export time) |
| IAP | **godot-iap 3.6.1** + IAPKit (publishable key at export time) |
| Application id | `com.crossplatformkorea.moonlitbeacon` |
| Version | **2.1.0** (both stores live) |
| License | MIT code; third-party credits below |
| License | MIT code; third-party assets keep their own ([notices](THIRD_PARTY_NOTICES.md)) |

808 × 360 is exactly one third of the Pixel 10 landscape panel, so 16 px pixel
art scales by an integer 3×. Aspect is `expand`, not `keep`, so extra width
Expand All @@ -45,23 +45,59 @@ Windows / Web are out of the current ship scope. Desktop runs are for
development. `pointing/emulate_touch_from_mouse=true` lets the virtual stick
work with a mouse.

## godot-iap example

Store purchases go through the official OpenIAP Godot addon, not a bespoke
billing bridge.

- Plugin: `apps/game/addons/godot-iap/` from
[`godot-iap-3.5.1`](https://github.com/hyodotdev/openiap/releases/tag/godot-iap-3.5.1)
- Game adapter: `apps/game/scripts/iap/`
- Server verification: IAPKit (`openiap-kit_pk_…` publishable key only;
admin/secret keys never enter the app)
- Android remote dependency: `io.github.hyochan.openiap:openiap-google:3.5.2`
- Direct-distribution APKs (itch.io) skip the native store singleton via
`OS.has_feature("direct_distribution")`

The project patch on top of the official zip is small and documented in
[`vendor/godot-iap/README.md`](vendor/godot-iap/README.md).
`pnpm test:godot-iap-vendor` checks that it reverses to the official bytes.
## Using this as a reference

Moonlit Beacon is a shipping example of
[godot-iap](https://openiap.dev/docs/setup/godot) **3.6.1** with
[IAPKit](https://kit.openiap.dev) purchase verification. It sells one-time
products and consumables on the App Store and Google Play. There are no
subscriptions.

What the code shows:

- Fetch products, purchase, verify the receipt with IAPKit, then finish the
transaction (permanent unlocks) or consume it (continue coins) only after
the entitlement is saved.
- Restore purchases, re-sync when the app resumes, and take back only a
purchase the store and IAPKit report as refunded or revoked.
- Read purchases with `get_available_purchases_result()`, so a failed store
query is never mistaken for "no purchases".

Start with `apps/game/scripts/iap/iap_store.gd` (the `Shop` autoload: catalog,
ledger, restore, refunds) and `godot_iap_backend.gd`, the only file that calls
godot-iap.

To run purchases in your own copy, change:

1. **Application id.** `com.crossplatformkorea.moonlitbeacon` is this game's
store identity. Replace it everywhere (`git grep` finds each place);
product IDs are built from `APP_ID` in `iap_store.gd`.
2. **Signing and team.** Store builds need your own Android upload keystore
(`GODOT_ANDROID_KEYSTORE_RELEASE_*` in [`.env.example`](.env.example)).
iOS builds sign with `MOONLIT_APPLE_TEAM_ID`, which defaults to this
project's team.
3. **Store products.** Create the SKUs listed in
[Monetize](apps/docs/docs/monetize.md) in App Store Connect and Play
Console, or change the catalog in `iap_store.gd`.
4. **IAPKit key.** Set `IAPKIT_API_KEY` to your project's `openiap-kit_pk_…`
publishable key; never ship an `openiap-kit_sk_…` key. Store exports write
it to `apps/game/iapkit_publishable.cfg` and delete the file afterwards.
Do not use `res://iapkit.cfg`: godot-iap 3.6 leaves it out of release
exports.

How the pieces fit:

- `apps/game/addons/godot-iap/` is the official 3.6.1 addon plus two small
patches, documented in [`vendor/godot-iap/README.md`](vendor/godot-iap/README.md).
`pnpm test:godot-iap-vendor` checks them against the release.
- The iOS frameworks live in `vendor/godot-iap-ios/bin/`, outside `res://`,
because desktop Godot 4.7 logs errors for an iOS-only extension. Every
`pnpm ios:*` export copies them into the addon, puts the descriptor in the
PCK, runs `fix_ios_embed.sh` to embed the frameworks in Xcode, and removes
the copy again.
- The direct-distribution APK (itch.io) has no store SDK: `pnpm android:build`
moves the Android plugin aside, and the `direct_distribution` feature keeps
the shop off.

See also [Monetize](apps/docs/docs/monetize.md) and
[IAP store setup](notes/release/iap-store-setup.md).
Expand Down Expand Up @@ -211,11 +247,13 @@ Font
Noto Sans CJK SC — Google — SIL Open Font License 1.1

Store SDK
godot-iap 3.5.1 — OpenIAP contributors — MIT License
godot-iap 3.6.1 — OpenIAP contributors — MIT License

Made by
Hyo Dev
```

See [third-party assets](apps/docs/docs/assets/third-party.md).
Game code is [MIT](LICENSE). Contributions: [CONTRIBUTING.md](CONTRIBUTING.md).
Game code is [MIT](LICENSE). Third-party assets keep their own licenses; see
[THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md) and the
[third-party assets](apps/docs/docs/assets/third-party.md) list.
Contributions: [CONTRIBUTING.md](CONTRIBUTING.md).
12 changes: 12 additions & 0 deletions THIRD_PARTY_NOTICES.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# Third-party notices

The game code is MIT licensed ([LICENSE](LICENSE)). Third-party assets in this
repository (fonts, art, audio) keep their own licenses. Each one is listed in
[`apps/docs/docs/assets/third-party.md`](apps/docs/docs/assets/third-party.md),
and license texts live in [`apps/game/docs/licenses/`](apps/game/docs/licenses/).

In particular, the Nexon MapleStory font allows commercial use and embedding
but forbids sale and modification.

The vendored godot-iap addon is MIT licensed; its license is kept at
[`apps/game/addons/godot-iap/LICENSE`](apps/game/addons/godot-iap/LICENSE).
10 changes: 5 additions & 5 deletions apps/docs/docs/assets/third-party.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,18 +21,18 @@ the full license text or a link.

| Item | Detail |
| --- | --- |
| Name | godot-iap 3.5.1 |
| Name | godot-iap 3.6.1 |
| Author | OpenIAP contributors |
| License | MIT License |
| Source | https://github.com/hyodotdev/openiap/releases/tag/godot-iap-3.5.1 |
| Source | https://github.com/hyodotdev/openiap/releases/tag/godot-iap-3.6.1 |
| Scope | Product query, purchase, and restore for Android Play Billing and iOS StoreKit 2 |
| Status | ✅ in use (7 non-consumable items in the Moonlit Store) |
| Status | ✅ in use (10 items in the Moonlit Store: 7 non-consumable, 3 consumable) |

The plugin's original license is kept with `apps/game/addons/godot-iap/LICENSE`
and the iOS distribution `vendor/godot-iap-ios/LICENSE`. It is not an
external **art asset**, so it is not in the asset manifest.

The Android AAR is taken byte-for-byte from the official 3.5.1 release.
The Android AAR is taken byte-for-byte from the official 3.6.1 release.
Provenance, hashes, and the small GDScript integration patch are recorded in
`vendor/godot-iap-android/README.md` and `vendor/godot-iap/README.md`.

Expand Down Expand Up @@ -119,7 +119,7 @@ Font
SIL Open Font License 1.1

Store integration
godot-iap 3.5.1
godot-iap 3.6.1
OpenIAP contributors · MIT License

Made by
Expand Down
23 changes: 14 additions & 9 deletions apps/docs/docs/monetize.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ business information with it.
If you do not make that decision first, everything else is wasted work.

The direct-distribution APK and the itch.io build still ship without
payments. Store-build shop code is implemented, but live sales stay off
until each platform's product listing and review are done.
payments. The Google Play and App Store builds sell the items in
[section 8](#8-so-what-does-this-game-sell).

## 2. In an endless roguelike, only three things can be sold

Expand Down Expand Up @@ -168,14 +168,14 @@ Godot has no official IAP support. This course uses **[godot-iap][iap]** —
a cross-platform plugin that follows the OpenIAP spec and covers iOS
StoreKit 2 and Android Play Billing with one API. MIT.

[iap]: https://github.com/hyodotdev/openiap/releases/tag/godot-iap-3.5.1
[iap]: https://github.com/hyodotdev/openiap/releases/tag/godot-iap-3.6.1

| | |
| --- | --- |
| Requires | Godot 4.3+ · iOS 17+ · Android API 24+ |
| Version | godot-iap 3.5.1 |
| Version | godot-iap 3.6.1 |
| Repo | `libraries/godot-iap` in `hyodotdev/openiap` |
| Docs | `openiap.dev` — `llms-full.txt` can fetch the whole API at once |
| Docs | [Godot setup](https://openiap.dev/docs/setup/godot) · `llms-full.txt` fetches the whole API at once |

Moonlit Beacon is Godot 4.7.1, so it meets the requirement.

Expand Down Expand Up @@ -207,6 +207,9 @@ GodotIapPlugin.request_purchase(props)

# Always finish after the grant
await GodotIapPlugin.finish_transaction(purchase, is_consumable)

# Restore and re-sync: the result form keeps a failed query apart from "none"
var owned: Dictionary = await GodotIapPlugin.get_available_purchases_result()
```

:::danger Do not skip `finish_transaction()`
Expand Down Expand Up @@ -308,7 +311,7 @@ Continues exist, but **there is no urgency-payment timer, energy, or
gacha.** Coins are bought ahead in the shop after checking quantity and
price, and unused coins stay.

## 9. Implementation status and pre-ship blockers
## 9. Implementation status and store-side steps

Shop UI, IAPKit server verification, permanent entitlement storage,
duplicate-transaction prevention, purchase restore, and hero-origin
Expand All @@ -326,7 +329,9 @@ missing list does not revoke an already saved entitlement.
On a desktop run you launched yourself, and on the itch.io APK, the shop
not opening is expected.

The following, though, are **external blockers** code cannot stand in for.
Both store listings sell the 10 items today. The steps below are the
store-side work code cannot stand in for; a copy of this project has to do
them again with its own accounts.

1. The actual selling party must enter and get approved developer
accounts, contracts, tax, and payout information in App Store Connect
Expand All @@ -352,5 +357,5 @@ The following, though, are **external blockers** code cannot stand in for.

Follow `notes/release/iap-store-setup.md` in the repo for the actual
console input and test order.
Until this checklist is finished, keep direct-distribution APK and
itch.io copy at "no payments."
The direct-distribution APK and itch.io copy stay at "no payments" either
way: itch.io cannot sell in-app purchases.
2 changes: 1 addition & 1 deletion apps/game/addons/godot-iap/android/GodotIap.gdap
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,4 @@ binary="GodotIap.release.aar"

[dependencies]
local=[]
remote=["io.github.hyochan.openiap:openiap-google:3.5.2", "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.11.0"]
remote=["io.github.hyochan.openiap:openiap-google:3.6.1", "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.11.0"]
Loading
Loading