4 Commits
Author SHA1 Message Date
adrianoftyrielandPaperclip 4d7cd6182f Rename the installed Android package to ca.wallacenet.prolibertategames
Build and Release APK / Build APK and publish release (push) Failing after 7s
Follows the Gitea repo rename: the applicationId (and its .dev variant)
now matches the repo, the launcher name and the docs, instead of carrying
the abandoned Pilgrims of Katan identity.

The JVM namespace stays org.prolibertate.games, so no source file moves;
only the installed identity changes.

Co-Authored-By: Paperclip <[email protected]>
2026-10-14 14:47:13 +00:00
adrianoftyrielandPaperclip b58b4251dc Rename the Gitea repo references to ProLibertateGames
Build and Release APK / Build APK and publish release (push) Canceled after 0s
The repo was renamed from PilgrimsOfKatan to ProLibertateGames on the
Gitea instance. Update every reference so nothing points at the old name:

- OTA updater: the repo constant and the User-Agent
- publish-release.sh: the default repo, the asset name, the release title
- CI/release workflows: the artifact names and the staged APK name
- settings.gradle.kts: the Gradle project name
- Protocol.kt / LanTransport.kt: the mDNS service label and the Wi-Fi lock tag
- docs (README, CLAUDE.md, docs/CI.md) and the update-policy test fixtures

The installed applicationId is left unchanged in this commit.

Co-Authored-By: Paperclip <[email protected]>
2026-10-14 14:21:43 +00:00
adrianoftyrielandPaperclip 5c797c8807 Restore the Pro Libertate Games title and splash; list Katan under Board Games
Build and Release APK / Build APK and publish release (push) Canceled after 0s
The bootstrap rebranded the app to Pilgrims of Katan (WALA-118 review);
the decision is to keep the upstream's own identity instead.

- Title and splash: the upstream originals restored exactly — the Wallace
  tartan splash under the "Pro Libertate Games" uncial wordmark
  (SplashScreen.kt, Tartan.kt), the tartan launcher icon, and the
  Theme.ProLibertateGames window theme. The game screens go back to the
  upstream WallaceGold/WallaceRed accents the tartan is woven from.
- Names and colours: launcher names back to "Pro Libertate Games" /
  "Pro Libertate Games DEV"; the channel colours back to the clan red /
  hunting green setts (resValue tartan_field and friends). The installed
  identity stays ca.wallacenet.pilgrimsofkatan (+ .dev) — that is the
  package, not the brand.
- Catalogue: Katan is listed under Board Games (3-4 players, original
  blurb, no third-party marks) with available = false, so dev builds show
  it as "Coming soon" and a production build hides it until its engine
  and screen land.
- Docs: README/CLAUDE.md/docs/CI.md updated to the restored identity;
  CatalogTest's invariant now names Katan as the one deliberate exception
  rather than asserting nothing is unfinished.

Structural bootstrap untouched: applicationId, OTA repointed at the Gitea
releases, CI workflows, version stamping.

Verified: ./gradlew assembleDebug and testDebugUnitTest green — 628 tests,
0 failures.

Co-Authored-By: Paperclip <[email protected]>
2026-10-14 02:15:30 +00:00
adrianoftyrielandPaperclip 05c443a4b5 Publish: verify the release's stored state after attaching the APK
Build and Release APK / Build APK and publish release (push) Canceled after 0s
A live publish on git.wallacenet.ca kept coming back invisible to the updater:
the create accepted "draft": false, and the release was still stored as a
draft once the APK was attached — so the anonymous list the app reads came
back empty even though every publish step reported success.

The script now attaches first, then reads the release back and PATCHes the
state if it did not stick, failing loudly if the correction is refused. A
draft is not a channel a preview build can silently become.

Also lists all 22 catalogue games in the README's table; seven were missing,
which read as "the app has 15 games".

Verified against the live instance: anonymous list shows
v1.4.1001-dev as a published prerelease with a tag-named asset, the
production /releases/latest correctly returns 404 while only a prerelease
exists, and an anonymous download through the URL the app builds matches the
built APK byte for byte.

Co-Authored-By: Paperclip <[email protected]>
2026-10-13 22:27:33 +00:00
31 changed files with 595 additions and 225 deletions
+3 -3
View File
@@ -69,7 +69,7 @@ jobs:
- name: Upload debug APK
uses: actions/upload-artifact@v4
with:
name: PilgrimsOfKatan-debug-apk
name: ProLibertateGames-debug-apk
path: app/build/outputs/apk/debug/app-debug.apk
if-no-files-found: error
retention-days: 14
@@ -79,7 +79,7 @@ jobs:
if: always()
uses: actions/upload-artifact@v4
with:
name: PilgrimsOfKatan-reports
name: ProLibertateGames-reports
path: |
app/build/reports/
if-no-files-found: ignore
@@ -109,7 +109,7 @@ jobs:
stamp="$(( ${{ github.run_number }} + offset ))"
version="v${series}.${stamp}-dev"
src="app/build/outputs/apk/debug/app-debug.apk"
dest="PilgrimsOfKatan-${version}.apk"
dest="ProLibertateGames-${version}.apk"
cp "$src" "$dest"
echo "version=$version" >> "$GITHUB_OUTPUT"
echo "apk=$dest" >> "$GITHUB_OUTPUT"
+2 -2
View File
@@ -64,7 +64,7 @@ jobs:
stamp="$(( ${{ github.run_number }} + offset ))"
version="v${series}.${stamp}"
src="app/build/outputs/apk/debug/app-debug.apk"
dest="PilgrimsOfKatan-${version}.apk"
dest="ProLibertateGames-${version}.apk"
cp "$src" "$dest"
echo "version=$version" >> "$GITHUB_OUTPUT"
echo "apk=$dest" >> "$GITHUB_OUTPUT"
@@ -72,7 +72,7 @@ jobs:
- name: Upload APK as workflow artifact
uses: actions/upload-artifact@v4
with:
name: PilgrimsOfKatan-apk
name: ProLibertateGames-apk
path: ${{ steps.stage.outputs.apk }}
if-no-files-found: error
+13 -11
View File
@@ -1,23 +1,25 @@
# Working on this repo (agent notes)
Pilgrims of Katan — an Android collection of card and board games, playable
against AI or over Wi-Fi (LAN). Bootstrapped from
ProLibertateGames — Wallacenet's build of an Android collection of card and
board games, playable against AI or over Wi-Fi (LAN). Upstream:
[adrianoftyriel/ProLibertateGames](https://github.com/adrianoftyriel/ProLibertateGames)
(GPL-3.0, kept as the `upstream` remote) for Wallacenet. Canonical repo:
<https://git.wallacenet.ca/devadmin/PilgrimsOfKatan>.
(GPL-3.0, kept as the `upstream` remote). Canonical repo:
<https://git.wallacenet.ca/devadmin/ProLibertateGames>.
## Ground rules
- **Licences travel.** `LICENSE` is GPL-3.0 and must stay. The Uncial Antiqua
font ships with its OFL licence at `docs/licenses/UncialAntiqua-OFL.txt`;
never delete or edit either. Attribution to the upstream lives in the README.
- **No Catan trademarks or brand assets**, and none of the upstream's Wallace
clan branding (it was removed in the rebrand; see README → Branding). The
brand is: *Pilgrims of Katan*, `ca.wallacenet.pilgrimsofkatan` (+ `.dev`),
slate blue / dev green, hexagon emblem.
- **No Catan trademarks or brand assets** — the one hard line. The app's own
branding is the upstream's, restored (see README → Branding): tartan launcher
icon and splash, Wallace clan red on production and hunting green on dev,
launcher names *Pro Libertate Games* / *Pro Libertate Games DEV*. The
installed identity is `ca.wallacenet.prolibertategames` (+ `.dev`).
- **The existing catalogue is the regression safety net.** Keep every game
green; new games (Catan) arrive as separate tasks, not edits to old ones.
All 22 games are `available = true`.
green; new games (Katan) arrive as separate tasks, not edits to old ones.
The 22 reference games are `available = true`; Katan is listed with
`available = false` (*Coming soon*) until its game task lands.
- **Docs conventions**: every game gets `docs/RULES-<id>.md` (index and gaps in
`docs/README.md`); build/CI/release decisions go in `docs/CI.md`. Update the
docs when behaviour changes — they are the check against the rules code.
@@ -44,7 +46,7 @@ One flag — `isDevBuild` in `app/build.gradle.kts` — drives applicationId,
launcher name and brand colour together. Never set them individually.
In-app OTA points at this repo's own Gitea releases
(`/api/v1/repos/devadmin/PilgrimsOfKatan/releases…`) and is **on**:
(`/api/v1/repos/devadmin/ProLibertateGames/releases…`) and is **on**:
`BuildConfig.OTA_ENABLED` is `true`. The channel is a decision already made and
recorded — `docs/CI.md` documents the endpoints, the build-number scheme, and
what publishing requires. Setting the flag to `false` is the kill switch.
+42 -39
View File
@@ -1,13 +1,15 @@
# Pilgrims of Katan
# ProLibertateGames
A collection of card and board games for Android, playable against the computer
or against other people over Wi-Fi (LAN).
Derived from [ProLibertateGames](https://github.com/adrianoftyriel/ProLibertateGames)
by adrianoftyriel, used under the GNU GPL v3 — see [LICENSE](LICENSE). Game
rules documentation lives under [docs/](docs/README.md); build, CI and release
decisions live in [docs/CI.md](docs/CI.md). Catan itself arrives in later work;
this app ships the reference catalogue unchanged as the regression safety net.
Wallacenet's build of [ProLibertateGames](https://github.com/adrianoftyriel/ProLibertateGames)
by adrianoftyriel (GNU GPL v3 — see [LICENSE](LICENSE)), hosted at
<https://git.wallacenet.ca/devadmin/ProLibertateGames>. Game rules documentation
lives under [docs/](docs/README.md); build, CI and release decisions live in
[docs/CI.md](docs/CI.md). Katan itself arrives in later work — it is listed under
Board Games as *Coming soon* — and this app ships the reference catalogue
unchanged as the regression safety net.
## What works today
@@ -15,16 +17,16 @@ The app shell is complete and every game in the catalogue is playable end to end
| | |
| --- | --- |
| **Cards** | Euchre, Kaiser, President, Golf, Wizard, Crazy 8s, Cribbage |
| **Board** | Sequence, Chess, Checkers, Backgammon, Mastermind, Nine Men's Morris, Pirates and Bulgars, Ta Yü |
| **Cards** | Euchre, Kaiser, President, Golf, Wizard, Crazy 8s, Cribbage, Hearts, Klondike, FreeCell, Spider, Pyramid |
| **Table** | Sequence, Chess, Checkers, Backgammon, Mastermind, Nine Men's Morris, Peg Solitaire, Yahtzee, Pirates and Bulgars, Ta Yü, Katan *(coming soon)* |
Nothing is marked *Coming soon* at the moment, because nothing is waiting. The
machinery for it is still there and still tested: a game listed in `GameCatalog`
with `available = false` appears in the menu marked *Coming soon* **on dev builds
only**, so a production release lists just what can actually be played and nobody
installs it and taps into a dead end. The menu decides this from the installed
APK's own version name — the same source the updater uses to know which channel
it is on.
One entry is marked *Coming soon*: Katan, the island game, whose engine and
screen land with their own task. The machinery is still there and still tested:
a game listed in `GameCatalog` with `available = false` appears in the menu
marked *Coming soon* **on dev builds only**, so a production release lists just
what can actually be played and nobody installs it and taps into a dead end. The
menu decides this from the installed APK's own version name — the same source
the updater uses to know which channel it is on.
Adding a game means writing a `GameRules` implementation, a `GameAi` and a
screen, then flipping `available` — the menu, lobby, networking, settings and OTA
@@ -135,21 +137,21 @@ it: every rule above is a pure function and unit-tested on the JVM.
## Branding
Neutral, original, and deliberately *not* Catan's: no third-party marks or
trade dress ship in this app (rebrand scope: WALA-118). The launcher icon is a
hexagonal ring around a solid core — a game tile, not any game's logo — drawn
as vector drawables so it stays sharp at every density and needs no PNGs. The
app opens on the brand field fading to its deep shade, the name set in a Celtic
uncial hand for two seconds before it fades into the menu.
The app wears the upstream's own branding, restored (WALA-118 review): the
Wallace tartan is the launcher icon and the splash, drawn as vector drawables and
Compose code so it stays sharp at every density and needs no PNGs. The app opens
on the tartan held under the name, set in a Celtic uncial hand for two seconds
before it fades into the menu.
A dev build wears the **dev green** field where production wears **slate
blue**, and installs as *Pilgrims of Katan DEV*. That is what tells the two
A dev build wears the **hunting green** sett where production wears the **clan
red**, and installs as *Pro Libertate Games DEV*. That is what tells the two
copies apart when both are on the phone; see
[Both channels on one device](#both-channels-on-one-device). The palette lives
in `ui/theme/Theme.kt` (`BrandField`, `BrandDeep`, `BrandAccent`) and the two
channel colours are generated per build by `resValue` in `app/build.gradle.kts`
— the icon vectors reference `@color/brand_field` rather than a literal, which
is how one set of vectors serves both colourways.
[Both channels on one device](#both-channels-on-one-device). The tartan is drawn
by `ui/theme/Tartan.kt` (`TartanBackground`, `threadWidthFor`) off
`BuildConfig.DEV_BUILD`; the matching vector colours are generated per build by
`resValue` in `app/build.gradle.kts` — the icon vectors reference
`@color/tartan_field` rather than a literal, which is how one set of vectors
serves both colourways.
Two honest caveats:
@@ -158,9 +160,10 @@ Two honest caveats:
the SIL Open Font License 1.1. The full licence is at
[docs/licenses/UncialAntiqua-OFL.txt](docs/licenses/UncialAntiqua-OFL.txt) and
must stay with the font — the OFL requires it to be distributed alongside.
- **The upstream's clan tartan is gone; its code is not.** The Wallace tartan
was the upstream's brand and left with the rebrand. The GPL-licensed history
that produced this app remains in git, and the source stays at
- **Katan is the one place this app deliberately diverges.** No Catan
trademarks or brand assets ship anywhere in it; the island game is listed
under its own name with original art only. The GPL-licensed history that
produced this app remains in git, and the source stays at
[adrianoftyriel/ProLibertateGames](https://github.com/adrianoftyriel/ProLibertateGames)
as the `upstream` remote.
@@ -187,10 +190,10 @@ smaller than a fingertip.
Requires JDK 17 and the Android SDK (compileSdk 34).
A local build is a dev build: it installs as
`ca.wallacenet.pilgrimsofkatan.dev` under the name *Pilgrims of Katan DEV* on
the dev green field, so it goes on beside a production copy rather than over it.
Set `PLG_CHANNEL=production` to build the production package
(`ca.wallacenet.pilgrimsofkatan`, slate blue).
`ca.wallacenet.prolibertategames.dev` under the name *Pro Libertate Games DEV* on
the hunting-green sett, so it goes on beside a production copy rather than over
it. Set `PLG_CHANNEL=production` to build the production package
(`ca.wallacenet.prolibertategames`, clan red).
## CI
@@ -262,7 +265,7 @@ unit-tested on the JVM like the rules engines are.
### Both channels on one device
A dev build installs under its own `applicationId`,
`ca.wallacenet.pilgrimsofkatan.dev`, so it is a separate app as far as Android
`ca.wallacenet.prolibertategames.dev`, so it is a separate app as far as Android
is concerned and can sit beside the production copy rather than replacing it. Each channel still updates itself
normally, because an update within a channel keeps the same package.
@@ -270,9 +273,9 @@ Three things move together, all off `isDevBuild` in `app/build.gradle.kts`:
| | Production | Dev |
| --- | --- | --- |
| `applicationId` | `ca.wallacenet.pilgrimsofkatan` | `ca.wallacenet.pilgrimsofkatan.dev` |
| Launcher name | Pilgrims of Katan | Pilgrims of Katan DEV |
| Brand field | slate blue | deep green |
| `applicationId` | `ca.wallacenet.prolibertategames` | `ca.wallacenet.prolibertategames.dev` |
| Launcher name | Pro Libertate Games | Pro Libertate Games DEV |
| Tartan sett | clan red | hunting green |
Anything that is not an explicit `PLG_CHANNEL=production` build is a dev build,
so a local `assembleDebug` is a dev build too and installs alongside a release
+18 -16
View File
@@ -40,9 +40,9 @@ val versionSeries = (project.findProperty("plgVersionSeries") as String?) ?: "1.
//
// This one flag drives everything that makes a dev build a separate app rather
// than a replacement for the production one — its own applicationId, its own
// launcher name, and its own brand colour. They have to move together: a dev
// package wearing the production name and colours is exactly the confusion
// having both installed is meant to avoid.
// launcher name, and the hunting sett in place of the clan sett. They have to
// move together: a dev package wearing the production name and colours is
// exactly the confusion having both installed is meant to avoid.
val isDevBuild = buildChannel != "production"
android {
@@ -58,7 +58,7 @@ android {
// A dev build installs under its own package, so a dev copy and a
// production copy can sit side by side on one device instead of one
// replacing the other.
applicationId = if (isDevBuild) "ca.wallacenet.pilgrimsofkatan.dev" else "ca.wallacenet.pilgrimsofkatan"
applicationId = if (isDevBuild) "ca.wallacenet.prolibertategames.dev" else "ca.wallacenet.prolibertategames"
minSdk = 24
targetSdk = 34
// The -dev suffix in versionName is what the installed app reads to
@@ -83,19 +83,21 @@ android {
resValue(
"string",
"app_name",
if (isDevBuild) "Pilgrims of Katan DEV" else "Pilgrims of Katan",
if (isDevBuild) "Pro Libertate Games DEV" else "Pro Libertate Games",
)
// The one colour that separates the two installs: slate blue on
// production, deep green on dev. The launcher icon and the splash are
// both built from it, so the drawer shows at a glance which of the two
// copies is which — the two have to stay in step or the icon and the
// splash it opens on would be different brands.
resValue("color", "brand_field", if (isDevBuild) "#2A6B54" else "#2B4A78")
// A darker shade of the same hue, held behind the window for the
// instant before the splash draws, so a launch never flashes the wrong
// colour.
resValue("color", "splash_background", if (isDevBuild) "#153B2F" else "#182A44")
// The one colour that separates the two setts: Wallace red for
// production, Wallace Hunting green for dev. The icon vectors reference
// this rather than a literal, and Tartan.kt picks the matching Compose
// colour off DEV_BUILD below — the two have to stay in step or the
// launcher icon and the splash it opens on would be different cloth.
resValue("color", "tartan_field", if (isDevBuild) "#1E7A3C" else "#E01B24")
// Shades of the same field, so a launch does not flash the wrong colour
// before the tartan is drawn: splash_background is held behind the
// window for that instant, ic_launcher_background is the icon's flat
// fallback.
resValue("color", "splash_background", if (isDevBuild) "#0D2E17" else "#4A0B10")
resValue("color", "ic_launcher_background", if (isDevBuild) "#145C2D" else "#B3121B")
buildConfigField("boolean", "DEV_BUILD", isDevBuild.toString())
@@ -131,7 +133,7 @@ android {
buildFeatures {
compose = true
// For DEV_BUILD, which is how the Compose splash picks its brand colour.
// For DEV_BUILD, which is how the Compose tartan picks its sett.
buildConfig = true
}
+1 -1
View File
@@ -25,7 +25,7 @@
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.PilgrimsOfKatan">
android:theme="@style/Theme.ProLibertateGames">
<!-- No fixed orientation: the UI adapts to portrait, landscape and
tablet widths. configChanges keeps Compose state across rotation
@@ -12,7 +12,7 @@ import org.prolibertate.games.score.ScorekeeperRepository
import org.prolibertate.games.settings.SettingsRepository
import org.prolibertate.games.ui.AppEnv
import org.prolibertate.games.ui.AppRoot
import org.prolibertate.games.ui.theme.PilgrimsTheme
import org.prolibertate.games.ui.theme.ProLibertateTheme
import org.prolibertate.games.update.Updater
import java.util.UUID
@@ -43,7 +43,7 @@ class MainActivity : ComponentActivity() {
)
setContent {
PilgrimsTheme {
ProLibertateTheme {
AppRoot(env)
}
}
@@ -94,6 +94,13 @@ object GameCatalog {
const val MORRIS = "morris"
const val PIRATES = "pirates"
/**
* The island game the collection is named for. Listed under Board Games
* and marked *Coming soon*; the engine and its screen land with the game
* task, and `available` flips on once two-device play is verified.
*/
const val CATAN = "catan"
val all: List<GameDescriptor> = listOf(
GameDescriptor(
id = EUCHRE,
@@ -333,6 +340,19 @@ object GameCatalog {
"because your score multiplies them.",
available = true,
),
GameDescriptor(
id = CATAN,
title = "Katan",
menus = setOf(GameMenu.BOARD),
minPlayers = 3,
maxPlayers = 4,
teamBased = false,
blurb = "Trade, build, and settle the island.",
// Listed, not playable: the engine and screen land with the Katan
// game task. Dev builds show the entry as *Coming soon*; a
// production build lists only what can actually be played.
available = false,
),
)
fun byId(id: String): GameDescriptor? = all.firstOrNull { it.id == id }
@@ -308,6 +308,6 @@ class LanTransport(private val context: Context) : Transport {
}
const val CONNECT_TIMEOUT_MS = 8_000
const val WIFI_LOCK_TAG = "PilgrimsOfKatan:lan"
const val WIFI_LOCK_TAG = "ProLibertateGames:lan"
}
}
@@ -33,7 +33,7 @@ const val PURPOSE_SCOREKEEPER = "scorekeeper"
/** Service type and name advertised over mDNS. */
const val SERVICE_TYPE = "_plgames._tcp"
const val SERVICE_NAME = "PilgrimsOfKatan"
const val SERVICE_NAME = "ProLibertateGames"
val protocolJson: Json = Json {
ignoreUnknownKeys = true
@@ -4,6 +4,7 @@ import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.tween
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
@@ -19,7 +20,6 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shadow
import androidx.compose.ui.text.TextStyle
@@ -30,9 +30,8 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import kotlinx.coroutines.delay
import org.prolibertate.games.R
import org.prolibertate.games.ui.theme.BrandAccent
import org.prolibertate.games.ui.theme.BrandDeep
import org.prolibertate.games.ui.theme.BrandField
import org.prolibertate.games.ui.theme.TartanBackground
import org.prolibertate.games.ui.theme.threadWidthFor
/** How long the splash holds at full opacity before fading. */
private const val SPLASH_HOLD_MILLIS = 2_000L
@@ -46,11 +45,7 @@ private const val SPLASH_FADE_MILLIS = 600
val CelticFontFamily = FontFamily(Font(R.font.uncial_antiqua))
/**
* The launch screen: the brand field under the name of the game.
*
* The gradient runs from the same hue as the window's `splash_background`
* held behind it, so the first frame and the splash are one brand in two
* shades rather than a colour change.
* The launch screen: Wallace tartan under the name of the house.
*
* Drawn as an overlay rather than a separate destination, so the menu is
* already composed and ready behind it and the fade reveals a live screen
@@ -75,17 +70,30 @@ fun SplashScreen(
onFinished()
}
BoxWithConstraints(
modifier = modifier
.fillMaxSize()
.alpha(alpha)
.background(Brush.verticalGradient(listOf(BrandField, BrandDeep))),
) {
BoxWithConstraints(modifier = modifier.fillMaxSize().alpha(alpha)) {
// Read the constraints here and hold them in locals: inside Column the
// implicit receiver becomes ColumnScope, and maxWidth/maxHeight are no
// longer in scope.
val shortestSide = minOf(maxWidth, maxHeight).value
// Weave scale is derived from the sett, not a fixed fraction, so a
// couple of full repeats are always on screen whatever the threadcount
// says and whatever the display is.
val threadWidth = threadWidthFor(across = shortestSide.dp, repeats = 2f)
val titleSize = (shortestSide * 0.13f).sp
TartanBackground(
modifier = Modifier.fillMaxSize(),
threadWidth = threadWidth,
// On the bias, matching the icon.
rotationDegrees = 45f,
)
// A scrim keeps the lettering legible over the busiest part of the sett.
Box(
modifier = Modifier
.fillMaxSize()
.background(Color.Black.copy(alpha = 0.45f))
)
Column(
modifier = Modifier
@@ -95,7 +103,7 @@ fun SplashScreen(
horizontalAlignment = Alignment.CenterHorizontally,
) {
Text(
text = "Pilgrims",
text = "Pro Libertate",
style = TextStyle(
fontFamily = CelticFontFamily,
fontSize = titleSize,
@@ -109,11 +117,11 @@ fun SplashScreen(
),
)
Text(
text = "of Katan",
text = "Games",
style = TextStyle(
fontFamily = CelticFontFamily,
fontSize = titleSize,
color = BrandAccent,
color = Color(0xFFF2C200),
textAlign = TextAlign.Center,
shadow = Shadow(
color = Color.Black,
@@ -19,7 +19,7 @@ import androidx.compose.ui.unit.dp
import org.prolibertate.games.ui.common.CARD_ASPECT
import org.prolibertate.games.ui.theme.FeltGreenDark
import org.prolibertate.games.ui.theme.Parchment
import org.prolibertate.games.ui.theme.AccentGold
import org.prolibertate.games.ui.theme.WallaceGold
/**
* The furniture every patience table needs.
@@ -48,7 +48,7 @@ fun TableSlot(
modifier = modifier
.width(width)
.border(
border = BorderStroke(if (highlighted) 3.dp else 0.dp, AccentGold),
border = BorderStroke(if (highlighted) 3.dp else 0.dp, WallaceGold),
shape = RoundedCornerShape(4.dp),
)
.clickable { onClick() },
@@ -48,7 +48,7 @@ import org.prolibertate.games.ui.common.CardBackView
import org.prolibertate.games.ui.common.PlayingCardView
import org.prolibertate.games.ui.theme.FeltGreenDark
import org.prolibertate.games.ui.theme.Parchment
import org.prolibertate.games.ui.theme.AccentGold
import org.prolibertate.games.ui.theme.WallaceGold
private const val TITLE = "Klondike"
@@ -311,7 +311,7 @@ private fun Slot(
modifier = modifier
.width(CARD_WIDTH)
.border(
border = BorderStroke(if (highlighted) 3.dp else 0.dp, AccentGold),
border = BorderStroke(if (highlighted) 3.dp else 0.dp, WallaceGold),
shape = RoundedCornerShape(4.dp),
)
.clickable { onClick() },
@@ -41,8 +41,8 @@ import org.prolibertate.games.net.MatchController
import org.prolibertate.games.ui.ScreenScaffold
import org.prolibertate.games.ui.theme.FeltGreenDark
import org.prolibertate.games.ui.theme.Parchment
import org.prolibertate.games.ui.theme.AccentGold
import org.prolibertate.games.ui.theme.AccentRed
import org.prolibertate.games.ui.theme.WallaceGold
import org.prolibertate.games.ui.theme.WallaceRed
private const val TITLE = "Peg Solitaire"
@@ -214,15 +214,15 @@ private fun HoleView(
Box(modifier = modifier.size(size), contentAlignment = Alignment.Center) {
val diameter = size * 0.72f
val ring = when {
selected -> AccentGold
isDestination -> AccentGold
selected -> WallaceGold
isDestination -> WallaceGold
else -> Parchment.copy(alpha = 0.35f)
}
Box(
modifier = Modifier
.size(diameter)
.background(
color = if (filled) AccentRed else FeltGreenDark,
color = if (filled) WallaceRed else FeltGreenDark,
shape = CircleShape,
)
.border(
@@ -42,8 +42,8 @@ import org.prolibertate.games.game.yahtzee.scoreOf
import org.prolibertate.games.net.MatchController
import org.prolibertate.games.ui.ScreenScaffold
import org.prolibertate.games.ui.theme.Parchment
import org.prolibertate.games.ui.theme.AccentGold
import org.prolibertate.games.ui.theme.AccentRed
import org.prolibertate.games.ui.theme.WallaceGold
import org.prolibertate.games.ui.theme.WallaceRed
private const val TITLE = "Yahtzee"
@@ -183,7 +183,7 @@ private fun Die(face: Int?, held: Boolean, enabled: Boolean, onClick: () -> Unit
.background(if (face == null) Parchment.copy(alpha = 0.3f) else Parchment, RoundedCornerShape(8.dp))
.border(
width = if (held) 3.dp else 1.dp,
color = if (held) AccentGold else AccentRed.copy(alpha = 0.4f),
color = if (held) WallaceGold else WallaceRed.copy(alpha = 0.4f),
shape = RoundedCornerShape(8.dp),
)
.clickable(enabled = enabled) { onClick() },
@@ -193,7 +193,7 @@ private fun Die(face: Int?, held: Boolean, enabled: Boolean, onClick: () -> Unit
text = face?.toString() ?: "·",
style = MaterialTheme.typography.titleLarge,
fontWeight = FontWeight.Bold,
color = AccentRed,
color = WallaceRed,
)
}
}
@@ -263,7 +263,7 @@ private fun CardRow(
},
modifier = Modifier.weight(1f),
style = MaterialTheme.typography.bodySmall,
color = if (written == null && open) AccentRed else MaterialTheme.colorScheme.onSurface,
color = if (written == null && open) WallaceRed else MaterialTheme.colorScheme.onSurface,
)
}
}
@@ -0,0 +1,187 @@
package org.prolibertate.games.ui.theme
import androidx.compose.foundation.Canvas
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.drawscope.DrawScope
import androidx.compose.ui.graphics.drawscope.rotate
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import org.prolibertate.games.BuildConfig
/** One band of a tartan sett, measured in threads. */
data class TartanStripe(val color: Color, val threads: Int)
val WallaceRed = Color(0xFFE01B24)
val WallaceBlack = Color(0xFF0C0C0C)
val WallaceGold = Color(0xFFF2C518)
/**
* The green the hunting sett puts where the clan sett puts red.
*
* Must match `tartan_field` for a dev build in app/build.gradle.kts, which is
* what the launcher icon is woven from — otherwise the icon and the splash it
* opens on would be visibly different cloth.
*/
val WallaceHuntingGreen = Color(0xFF1E7A3C)
/**
* Expands a half-sett into a full repeat by reflecting it.
*
* Tartans are specified as a half-sett between two pivots and woven out to
* both sides, so `A B C` becomes `A B C B` — the pivot stripes are not
* doubled. Writing the data this way keeps it in the same form a threadcount
* is published in, instead of a flat list that has to be kept symmetric by
* hand.
*/
fun List<TartanStripe>.pivoted(): List<TartanStripe> =
if (size < 3) this else this + drop(1).dropLast(1).reversed()
/**
* The Wallace sett, from its published threadcount: **K/4 R32 K32 Y/4**.
*
* The slashes mark the pivots, so this is the half-sett and it reflects out to
* `K4 R32 K32 Y4 K32 R32` — 136 threads, roughly half black, half red, with a
* yellow line worth 3%. Two consequences that are easy to get backwards and
* that define the pattern:
*
* - the yellow overstripe runs down the **centre of a wide black band**, not
* between red and black;
* - the narrow black guard sits **between two red blocks**, splitting the red
* into pairs.
*
* The threadcount itself lives in [wallaceSett], because the hunting colourway
* below is the same cloth in a different colour and must not drift from it.
*/
val WallaceSett: List<TartanStripe> = wallaceSett(WallaceRed)
/**
* Wallace Hunting: the same threadcount woven in green.
*
* Hunting setts are the clan cloth in muted, outdoor colours, and for Wallace
* that is the one substitution — green for the red — with the black ground and
* the yellow overstripe untouched. Deriving it from the same builder rather
* than writing the numbers out twice is what keeps that true: a change to the
* threadcount cannot land in one colourway and miss the other.
*/
val WallaceHuntingSett: List<TartanStripe> = wallaceSett(WallaceHuntingGreen)
/** The Wallace threadcount woven with [field] in place of the red. */
private fun wallaceSett(field: Color): List<TartanStripe> = listOf(
TartanStripe(WallaceBlack, 4),
TartanStripe(field, 32),
TartanStripe(WallaceBlack, 32),
TartanStripe(WallaceGold, 4),
).pivoted()
/**
* The cloth this build wears: the clan sett on production, the hunting sett on
* dev.
*
* A dev build installs as its own app beside the production one, so it has to
* be recognisable at a glance in the app drawer and on the splash — the green
* says which of the two copies just opened without anything having to be read.
*/
val AppSett: List<TartanStripe> =
if (BuildConfig.DEV_BUILD) WallaceHuntingSett else WallaceSett
/** Threads in one full repeat of a sett. */
val List<TartanStripe>.threadsPerRepeat: Int get() = sumOf { it.threads }
/**
* The thread width that fits [repeats] repeats of [sett] across [across].
*
* Scale has to be derived from the sett rather than fixed, because changing a
* threadcount changes how many threads a repeat contains — a fixed width and a
* larger sett silently zooms in until the cloth is a couple of enormous blocks
* and stops reading as tartan at all.
*/
fun threadWidthFor(
sett: List<TartanStripe> = AppSett,
across: Dp,
repeats: Float = 2f,
): Dp = across / (sett.threadsPerRepeat * repeats)
/**
* Draws a tartan by laying the sett down vertically as warp, then across as
* weft at half opacity. The overlap is what produces the blended squares and
* the sense of a woven cloth rather than a printed grid — red warp under black
* weft comes out a darker red, exactly as it does on the loom.
*
* [rotationDegrees] turns the cloth on the bias. The stripes are drawn well
* past the edges of the canvas so the corners stay covered once it is turned.
*/
fun DrawScope.drawTartan(
sett: List<TartanStripe> = AppSett,
threadSize: Float,
rotationDegrees: Float = 0f,
) {
if (sett.isEmpty() || threadSize <= 0f) return
val repeat = sett.sumOf { it.threads } * threadSize
if (repeat <= 0f) return
// Turning a square by 45 degrees needs about 0.21 of its size spare on
// every edge; three quarters is generous enough for any angle.
val pad = if (rotationDegrees == 0f) 0f else size.maxDimension * 0.75f
val left = -pad
val top = -pad
val right = size.width + pad
val bottom = size.height + pad
rotate(degrees = rotationDegrees) {
// Warp: full-height bands.
var x = left - (left.mod(repeat))
while (x < right) {
var offset = x
for (stripe in sett) {
val width = stripe.threads * threadSize
drawRect(
color = stripe.color,
topLeft = Offset(offset, top),
size = Size(width, bottom - top),
)
offset += width
}
x += repeat
}
// Weft: full-width bands, half opacity so the warp shows through.
var y = top - (top.mod(repeat))
while (y < bottom) {
var offset = y
for (stripe in sett) {
val height = stripe.threads * threadSize
drawRect(
color = stripe.color.copy(alpha = 0.5f),
topLeft = Offset(left, offset),
size = Size(right - left, height),
)
offset += height
}
y += repeat
}
}
}
/**
* A tartan fill. [threadWidth] sets the weave scale, so the same cloth can be
* a fine weave on a phone and a coarse one on a tablet without redrawing it.
*/
@Composable
fun TartanBackground(
modifier: Modifier = Modifier,
sett: List<TartanStripe> = AppSett,
threadWidth: Dp = 2.dp,
rotationDegrees: Float = 0f,
) {
Canvas(modifier = modifier) {
drawTartan(
sett = sett,
threadSize = threadWidth.toPx(),
rotationDegrees = rotationDegrees,
)
}
}
@@ -6,7 +6,6 @@ import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color
import org.prolibertate.games.BuildConfig
/** Card-table green, which the board and table surfaces are drawn on. */
val FeltGreen = Color(0xFF1B5E20)
@@ -15,31 +14,6 @@ val Parchment = Color(0xFFF4EFE4)
val CardRed = Color(0xFFB3261E)
val CardBlack = Color(0xFF1C1B1F)
/**
* The brand palette for Pilgrims of Katan — original and neutral, shared by
* every game in the catalogue. Slate blue on production, deep green on dev, so
* the two installs are told apart at a glance; both must stay in step with the
* resValues in app/build.gradle.kts or the icon and the splash it opens on
* would be different brands.
*
* The upstream shipped a clan tartan here. This project carries no clan or
* third-party trade dress (WALA-118).
*/
val BrandField: Color = if (BuildConfig.DEV_BUILD) Color(0xFF2A6B54) else Color(0xFF2B4A78)
val BrandDeep: Color = if (BuildConfig.DEV_BUILD) Color(0xFF153B2F) else Color(0xFF182A44)
/** The parchment accent: the emblem on the icon, the second line of the splash. */
val BrandAccent: Color = Color(0xFFEFD9A4)
/**
* Accents used *inside* a game table — a highlight, a held die, a danger mark.
* Deliberately channel-free: what a game shows must not change with which
* install it is running in. Values unchanged from the upstream, so the games
* themselves look exactly as they did.
*/
val AccentRed = Color(0xFFE01B24)
val AccentGold = Color(0xFFF2C518)
val TeamColours: List<Color> = listOf(
Color(0xFF1565C0), // blue
Color(0xFFC62828), // red
@@ -65,7 +39,7 @@ private val LightColours = lightColorScheme(
)
@Composable
fun PilgrimsTheme(
fun ProLibertateTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
content: @Composable () -> Unit,
) {
@@ -12,7 +12,7 @@ import java.net.URI
* meaning to be had from comparing across them, so nothing does.
*
* They also carry different applicationIds — a dev build installs as
* `…pilgrimsofkatan.dev` — so both can sit on one device, each updating
* `…prolibertategames.dev` — so both can sit on one device, each updating
* itself down its own line and leaving the other alone.
*/
enum class UpdateChannel(val label: String, val blurb: String) {
@@ -126,7 +126,7 @@ class Updater(private val activity: Activity) {
(URL(url).openConnection() as HttpURLConnection).apply {
requestMethod = "GET"
setRequestProperty("Accept", "application/json")
setRequestProperty("User-Agent", "PilgrimsOfKatan-Updater")
setRequestProperty("User-Agent", "ProLibertateGames-Updater")
connectTimeout = 15_000
readTimeout = 15_000
}
@@ -206,7 +206,7 @@ class Updater(private val activity: Activity) {
const val SERVER = "https://git.wallacenet.ca"
const val SERVER_HOST = "git.wallacenet.ca"
const val OWNER = "devadmin"
const val REPO = "PilgrimsOfKatan"
const val REPO = "ProLibertateGames"
/** Release routes, Gitea's `/api/v1` spelling of GitHub's API. */
const val API = "$SERVER/api/v1/repos/$OWNER/$REPO"
@@ -1,16 +1,70 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
The flat field behind the launcher emblem: brand_field, slate blue on
production and deep green on dev, generated by the build (see resValue in
app/build.gradle.kts) so the two installs are told apart in the app drawer
by the icon alone.
A single square of the Wallace tartan, set on the bias.
Generated from the published threadcount K/4 R32 K32 Y/4 — the same numbers
as WallaceSett in Tartan.kt. The slashes are pivots, so the half-sett
reflects out to K4 R32 K32 Y4 K32 R32: the yellow overstripe runs down the
centre of a wide black band, and the narrow black guard splits the red into
pairs.
The stripes reference @color/tartan_field rather than a literal, because the
same threadcount is woven in two colourways: Wallace red on a production
build, Wallace Hunting green on a dev one, so the two installs are told apart
in the app drawer by the icon alone. The build generates that colour; see
resValue in app/build.gradle.kts.
Warp is opaque, weft laid over it at half opacity, which is what gives the
blended squares rather than a flat grid. Paths run well past the viewport
because the group is rotated 45 degrees; the overspill keeps the corners
covered. One full repeat fills the adaptive icon's safe zone.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:fillColor="@color/brand_field"
android:pathData="M0,0h108v108h-108z" />
<group
android:rotation="45"
android:pivotX="54.0"
android:pivotY="54.0">
<path android:fillColor="@color/tartan_ground" android:pathData="M-90.00,-90.00h288.00v288.00h-288.00z" />
<path android:fillColor="@color/tartan_field" android:pathData="M-88.94,-90.00h16.94v288.00h-16.94z" />
<path android:fillColor="@color/tartan_field" android:pathData="M-69.88,-90.00h16.94v288.00h-16.94z" />
<path android:fillColor="@color/tartan_over" android:pathData="M-36.00,-90.00h2.12v288.00h-2.12z" />
<path android:fillColor="@color/tartan_field" android:pathData="M-16.94,-90.00h16.94v288.00h-16.94z" />
<path android:fillColor="@color/tartan_field" android:pathData="M2.12,-90.00h16.94v288.00h-16.94z" />
<path android:fillColor="@color/tartan_over" android:pathData="M36.00,-90.00h2.12v288.00h-2.12z" />
<path android:fillColor="@color/tartan_field" android:pathData="M55.06,-90.00h16.94v288.00h-16.94z" />
<path android:fillColor="@color/tartan_field" android:pathData="M74.12,-90.00h16.94v288.00h-16.94z" />
<path android:fillColor="@color/tartan_over" android:pathData="M108.00,-90.00h2.12v288.00h-2.12z" />
<path android:fillColor="@color/tartan_field" android:pathData="M127.06,-90.00h16.94v288.00h-16.94z" />
<path android:fillColor="@color/tartan_field" android:pathData="M146.12,-90.00h16.94v288.00h-16.94z" />
<path android:fillColor="@color/tartan_over" android:pathData="M180.00,-90.00h2.12v288.00h-2.12z" />
<path android:fillColor="@color/tartan_ground" android:fillAlpha="0.5" android:pathData="M-90.00,-105.88h288.00v16.94h-288.00z" />
<path android:fillColor="@color/tartan_field" android:fillAlpha="0.5" android:pathData="M-90.00,-88.94h288.00v16.94h-288.00z" />
<path android:fillColor="@color/tartan_ground" android:fillAlpha="0.5" android:pathData="M-90.00,-72.00h288.00v2.12h-288.00z" />
<path android:fillColor="@color/tartan_field" android:fillAlpha="0.5" android:pathData="M-90.00,-69.88h288.00v16.94h-288.00z" />
<path android:fillColor="@color/tartan_ground" android:fillAlpha="0.5" android:pathData="M-90.00,-52.94h288.00v16.94h-288.00z" />
<path android:fillColor="@color/tartan_over" android:fillAlpha="0.5" android:pathData="M-90.00,-36.00h288.00v2.12h-288.00z" />
<path android:fillColor="@color/tartan_ground" android:fillAlpha="0.5" android:pathData="M-90.00,-33.88h288.00v16.94h-288.00z" />
<path android:fillColor="@color/tartan_field" android:fillAlpha="0.5" android:pathData="M-90.00,-16.94h288.00v16.94h-288.00z" />
<path android:fillColor="@color/tartan_ground" android:fillAlpha="0.5" android:pathData="M-90.00,0.00h288.00v2.12h-288.00z" />
<path android:fillColor="@color/tartan_field" android:fillAlpha="0.5" android:pathData="M-90.00,2.12h288.00v16.94h-288.00z" />
<path android:fillColor="@color/tartan_ground" android:fillAlpha="0.5" android:pathData="M-90.00,19.06h288.00v16.94h-288.00z" />
<path android:fillColor="@color/tartan_over" android:fillAlpha="0.5" android:pathData="M-90.00,36.00h288.00v2.12h-288.00z" />
<path android:fillColor="@color/tartan_ground" android:fillAlpha="0.5" android:pathData="M-90.00,38.12h288.00v16.94h-288.00z" />
<path android:fillColor="@color/tartan_field" android:fillAlpha="0.5" android:pathData="M-90.00,55.06h288.00v16.94h-288.00z" />
<path android:fillColor="@color/tartan_ground" android:fillAlpha="0.5" android:pathData="M-90.00,72.00h288.00v2.12h-288.00z" />
<path android:fillColor="@color/tartan_field" android:fillAlpha="0.5" android:pathData="M-90.00,74.12h288.00v16.94h-288.00z" />
<path android:fillColor="@color/tartan_ground" android:fillAlpha="0.5" android:pathData="M-90.00,91.06h288.00v16.94h-288.00z" />
<path android:fillColor="@color/tartan_over" android:fillAlpha="0.5" android:pathData="M-90.00,108.00h288.00v2.12h-288.00z" />
<path android:fillColor="@color/tartan_ground" android:fillAlpha="0.5" android:pathData="M-90.00,110.12h288.00v16.94h-288.00z" />
<path android:fillColor="@color/tartan_field" android:fillAlpha="0.5" android:pathData="M-90.00,127.06h288.00v16.94h-288.00z" />
<path android:fillColor="@color/tartan_ground" android:fillAlpha="0.5" android:pathData="M-90.00,144.00h288.00v2.12h-288.00z" />
<path android:fillColor="@color/tartan_field" android:fillAlpha="0.5" android:pathData="M-90.00,146.12h288.00v16.94h-288.00z" />
<path android:fillColor="@color/tartan_ground" android:fillAlpha="0.5" android:pathData="M-90.00,163.06h288.00v16.94h-288.00z" />
<path android:fillColor="@color/tartan_over" android:fillAlpha="0.5" android:pathData="M-90.00,180.00h288.00v2.12h-288.00z" />
<path android:fillColor="@color/tartan_ground" android:fillAlpha="0.5" android:pathData="M-90.00,182.12h288.00v16.94h-288.00z" />
</group>
</vector>
@@ -1,23 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
The brand emblem: a hexagonal ring around a solid hexagonal core, in
parchment on the brand field behind it (see ic_launcher_background).
The hexagon is a game tile, not any game's mark — deliberately generic
branding for Pilgrims of Katan, carrying no third-party logo or trade dress.
Everything sits inside the adaptive icon's safe zone (a centred 66dp circle
on this 108dp canvas) so no launcher mask can clip it.
Deliberately empty. The icon is the cloth itself, so the adaptive icon's
foreground layer carries nothing — anything drawn here would sit on top of the
tartan and break the "single square of tartan" idea.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<!-- Ring and core in one even-odd path: outer hexagon, inner hexagon,
core hexagon — odd coverage fills, so the ring and the core are drawn
and the gap between them stays open to the field showing through. -->
<path
android:fillColor="@color/brand_accent"
android:fillType="evenOdd"
android:pathData="M84,54 L69,79.98 L39,79.98 L24,54 L39,28.02 L69,28.02 Z M72,54 L63,69.59 L45,69.59 L36,54 L45,38.41 L63,38.41 Z M63,54 L58.5,61.79 L49.5,61.79 L45,54 L49.5,46.21 L58.5,46.21 Z" />
android:fillColor="#00000000"
android:pathData="M0,0h108v108h-108z" />
</vector>
+60 -12
View File
@@ -1,22 +1,70 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
The legacy (pre-API 26) launcher icon: the same brand emblem as the adaptive
icon's foreground, over the same brand field, in the one layer those
launchers get.
A single square of the Wallace tartan, set on the bias.
Field first, emblem second: two paths, because the emblem's ring relies on
even-odd parity and a filled background inside the same path would flip it.
Generated from the published threadcount K/4 R32 K32 Y/4 — the same numbers
as WallaceSett in Tartan.kt. The slashes are pivots, so the half-sett
reflects out to K4 R32 K32 Y4 K32 R32: the yellow overstripe runs down the
centre of a wide black band, and the narrow black guard splits the red into
pairs.
The stripes reference @color/tartan_field rather than a literal, because the
same threadcount is woven in two colourways: Wallace red on a production
build, Wallace Hunting green on a dev one, so the two installs are told apart
in the app drawer by the icon alone. The build generates that colour; see
resValue in app/build.gradle.kts.
Warp is opaque, weft laid over it at half opacity, which is what gives the
blended squares rather than a flat grid. Paths run well past the viewport
because the group is rotated 45 degrees; the overspill keeps the corners
covered. One full repeat fills the adaptive icon's safe zone.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="48dp"
android:height="48dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:fillColor="@color/brand_field"
android:pathData="M0,0h108v108h-108z" />
<path
android:fillColor="@color/brand_accent"
android:fillType="evenOdd"
android:pathData="M84,54 L69,79.98 L39,79.98 L24,54 L39,28.02 L69,28.02 Z M72,54 L63,69.59 L45,69.59 L36,54 L45,38.41 L63,38.41 Z M63,54 L58.5,61.79 L49.5,61.79 L45,54 L49.5,46.21 L58.5,46.21 Z" />
<group
android:rotation="45"
android:pivotX="54.0"
android:pivotY="54.0">
<path android:fillColor="@color/tartan_ground" android:pathData="M-90.00,-90.00h288.00v288.00h-288.00z" />
<path android:fillColor="@color/tartan_field" android:pathData="M-88.94,-90.00h16.94v288.00h-16.94z" />
<path android:fillColor="@color/tartan_field" android:pathData="M-69.88,-90.00h16.94v288.00h-16.94z" />
<path android:fillColor="@color/tartan_over" android:pathData="M-36.00,-90.00h2.12v288.00h-2.12z" />
<path android:fillColor="@color/tartan_field" android:pathData="M-16.94,-90.00h16.94v288.00h-16.94z" />
<path android:fillColor="@color/tartan_field" android:pathData="M2.12,-90.00h16.94v288.00h-16.94z" />
<path android:fillColor="@color/tartan_over" android:pathData="M36.00,-90.00h2.12v288.00h-2.12z" />
<path android:fillColor="@color/tartan_field" android:pathData="M55.06,-90.00h16.94v288.00h-16.94z" />
<path android:fillColor="@color/tartan_field" android:pathData="M74.12,-90.00h16.94v288.00h-16.94z" />
<path android:fillColor="@color/tartan_over" android:pathData="M108.00,-90.00h2.12v288.00h-2.12z" />
<path android:fillColor="@color/tartan_field" android:pathData="M127.06,-90.00h16.94v288.00h-16.94z" />
<path android:fillColor="@color/tartan_field" android:pathData="M146.12,-90.00h16.94v288.00h-16.94z" />
<path android:fillColor="@color/tartan_over" android:pathData="M180.00,-90.00h2.12v288.00h-2.12z" />
<path android:fillColor="@color/tartan_ground" android:fillAlpha="0.5" android:pathData="M-90.00,-105.88h288.00v16.94h-288.00z" />
<path android:fillColor="@color/tartan_field" android:fillAlpha="0.5" android:pathData="M-90.00,-88.94h288.00v16.94h-288.00z" />
<path android:fillColor="@color/tartan_ground" android:fillAlpha="0.5" android:pathData="M-90.00,-72.00h288.00v2.12h-288.00z" />
<path android:fillColor="@color/tartan_field" android:fillAlpha="0.5" android:pathData="M-90.00,-69.88h288.00v16.94h-288.00z" />
<path android:fillColor="@color/tartan_ground" android:fillAlpha="0.5" android:pathData="M-90.00,-52.94h288.00v16.94h-288.00z" />
<path android:fillColor="@color/tartan_over" android:fillAlpha="0.5" android:pathData="M-90.00,-36.00h288.00v2.12h-288.00z" />
<path android:fillColor="@color/tartan_ground" android:fillAlpha="0.5" android:pathData="M-90.00,-33.88h288.00v16.94h-288.00z" />
<path android:fillColor="@color/tartan_field" android:fillAlpha="0.5" android:pathData="M-90.00,-16.94h288.00v16.94h-288.00z" />
<path android:fillColor="@color/tartan_ground" android:fillAlpha="0.5" android:pathData="M-90.00,0.00h288.00v2.12h-288.00z" />
<path android:fillColor="@color/tartan_field" android:fillAlpha="0.5" android:pathData="M-90.00,2.12h288.00v16.94h-288.00z" />
<path android:fillColor="@color/tartan_ground" android:fillAlpha="0.5" android:pathData="M-90.00,19.06h288.00v16.94h-288.00z" />
<path android:fillColor="@color/tartan_over" android:fillAlpha="0.5" android:pathData="M-90.00,36.00h288.00v2.12h-288.00z" />
<path android:fillColor="@color/tartan_ground" android:fillAlpha="0.5" android:pathData="M-90.00,38.12h288.00v16.94h-288.00z" />
<path android:fillColor="@color/tartan_field" android:fillAlpha="0.5" android:pathData="M-90.00,55.06h288.00v16.94h-288.00z" />
<path android:fillColor="@color/tartan_ground" android:fillAlpha="0.5" android:pathData="M-90.00,72.00h288.00v2.12h-288.00z" />
<path android:fillColor="@color/tartan_field" android:fillAlpha="0.5" android:pathData="M-90.00,74.12h288.00v16.94h-288.00z" />
<path android:fillColor="@color/tartan_ground" android:fillAlpha="0.5" android:pathData="M-90.00,91.06h288.00v16.94h-288.00z" />
<path android:fillColor="@color/tartan_over" android:fillAlpha="0.5" android:pathData="M-90.00,108.00h288.00v2.12h-288.00z" />
<path android:fillColor="@color/tartan_ground" android:fillAlpha="0.5" android:pathData="M-90.00,110.12h288.00v16.94h-288.00z" />
<path android:fillColor="@color/tartan_field" android:fillAlpha="0.5" android:pathData="M-90.00,127.06h288.00v16.94h-288.00z" />
<path android:fillColor="@color/tartan_ground" android:fillAlpha="0.5" android:pathData="M-90.00,144.00h288.00v2.12h-288.00z" />
<path android:fillColor="@color/tartan_field" android:fillAlpha="0.5" android:pathData="M-90.00,146.12h288.00v16.94h-288.00z" />
<path android:fillColor="@color/tartan_ground" android:fillAlpha="0.5" android:pathData="M-90.00,163.06h288.00v16.94h-288.00z" />
<path android:fillColor="@color/tartan_over" android:fillAlpha="0.5" android:pathData="M-90.00,180.00h288.00v2.12h-288.00z" />
<path android:fillColor="@color/tartan_ground" android:fillAlpha="0.5" android:pathData="M-90.00,182.12h288.00v16.94h-288.00z" />
</group>
</vector>
+60 -12
View File
@@ -1,22 +1,70 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
The legacy (pre-API 26) launcher icon: the same brand emblem as the adaptive
icon's foreground, over the same brand field, in the one layer those
launchers get.
A single square of the Wallace tartan, set on the bias.
Field first, emblem second: two paths, because the emblem's ring relies on
even-odd parity and a filled background inside the same path would flip it.
Generated from the published threadcount K/4 R32 K32 Y/4 — the same numbers
as WallaceSett in Tartan.kt. The slashes are pivots, so the half-sett
reflects out to K4 R32 K32 Y4 K32 R32: the yellow overstripe runs down the
centre of a wide black band, and the narrow black guard splits the red into
pairs.
The stripes reference @color/tartan_field rather than a literal, because the
same threadcount is woven in two colourways: Wallace red on a production
build, Wallace Hunting green on a dev one, so the two installs are told apart
in the app drawer by the icon alone. The build generates that colour; see
resValue in app/build.gradle.kts.
Warp is opaque, weft laid over it at half opacity, which is what gives the
blended squares rather than a flat grid. Paths run well past the viewport
because the group is rotated 45 degrees; the overspill keeps the corners
covered. One full repeat fills the adaptive icon's safe zone.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="48dp"
android:height="48dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:fillColor="@color/brand_field"
android:pathData="M0,0h108v108h-108z" />
<path
android:fillColor="@color/brand_accent"
android:fillType="evenOdd"
android:pathData="M84,54 L69,79.98 L39,79.98 L24,54 L39,28.02 L69,28.02 Z M72,54 L63,69.59 L45,69.59 L36,54 L45,38.41 L63,38.41 Z M63,54 L58.5,61.79 L49.5,61.79 L45,54 L49.5,46.21 L58.5,46.21 Z" />
<group
android:rotation="45"
android:pivotX="54.0"
android:pivotY="54.0">
<path android:fillColor="@color/tartan_ground" android:pathData="M-90.00,-90.00h288.00v288.00h-288.00z" />
<path android:fillColor="@color/tartan_field" android:pathData="M-88.94,-90.00h16.94v288.00h-16.94z" />
<path android:fillColor="@color/tartan_field" android:pathData="M-69.88,-90.00h16.94v288.00h-16.94z" />
<path android:fillColor="@color/tartan_over" android:pathData="M-36.00,-90.00h2.12v288.00h-2.12z" />
<path android:fillColor="@color/tartan_field" android:pathData="M-16.94,-90.00h16.94v288.00h-16.94z" />
<path android:fillColor="@color/tartan_field" android:pathData="M2.12,-90.00h16.94v288.00h-16.94z" />
<path android:fillColor="@color/tartan_over" android:pathData="M36.00,-90.00h2.12v288.00h-2.12z" />
<path android:fillColor="@color/tartan_field" android:pathData="M55.06,-90.00h16.94v288.00h-16.94z" />
<path android:fillColor="@color/tartan_field" android:pathData="M74.12,-90.00h16.94v288.00h-16.94z" />
<path android:fillColor="@color/tartan_over" android:pathData="M108.00,-90.00h2.12v288.00h-2.12z" />
<path android:fillColor="@color/tartan_field" android:pathData="M127.06,-90.00h16.94v288.00h-16.94z" />
<path android:fillColor="@color/tartan_field" android:pathData="M146.12,-90.00h16.94v288.00h-16.94z" />
<path android:fillColor="@color/tartan_over" android:pathData="M180.00,-90.00h2.12v288.00h-2.12z" />
<path android:fillColor="@color/tartan_ground" android:fillAlpha="0.5" android:pathData="M-90.00,-105.88h288.00v16.94h-288.00z" />
<path android:fillColor="@color/tartan_field" android:fillAlpha="0.5" android:pathData="M-90.00,-88.94h288.00v16.94h-288.00z" />
<path android:fillColor="@color/tartan_ground" android:fillAlpha="0.5" android:pathData="M-90.00,-72.00h288.00v2.12h-288.00z" />
<path android:fillColor="@color/tartan_field" android:fillAlpha="0.5" android:pathData="M-90.00,-69.88h288.00v16.94h-288.00z" />
<path android:fillColor="@color/tartan_ground" android:fillAlpha="0.5" android:pathData="M-90.00,-52.94h288.00v16.94h-288.00z" />
<path android:fillColor="@color/tartan_over" android:fillAlpha="0.5" android:pathData="M-90.00,-36.00h288.00v2.12h-288.00z" />
<path android:fillColor="@color/tartan_ground" android:fillAlpha="0.5" android:pathData="M-90.00,-33.88h288.00v16.94h-288.00z" />
<path android:fillColor="@color/tartan_field" android:fillAlpha="0.5" android:pathData="M-90.00,-16.94h288.00v16.94h-288.00z" />
<path android:fillColor="@color/tartan_ground" android:fillAlpha="0.5" android:pathData="M-90.00,0.00h288.00v2.12h-288.00z" />
<path android:fillColor="@color/tartan_field" android:fillAlpha="0.5" android:pathData="M-90.00,2.12h288.00v16.94h-288.00z" />
<path android:fillColor="@color/tartan_ground" android:fillAlpha="0.5" android:pathData="M-90.00,19.06h288.00v16.94h-288.00z" />
<path android:fillColor="@color/tartan_over" android:fillAlpha="0.5" android:pathData="M-90.00,36.00h288.00v2.12h-288.00z" />
<path android:fillColor="@color/tartan_ground" android:fillAlpha="0.5" android:pathData="M-90.00,38.12h288.00v16.94h-288.00z" />
<path android:fillColor="@color/tartan_field" android:fillAlpha="0.5" android:pathData="M-90.00,55.06h288.00v16.94h-288.00z" />
<path android:fillColor="@color/tartan_ground" android:fillAlpha="0.5" android:pathData="M-90.00,72.00h288.00v2.12h-288.00z" />
<path android:fillColor="@color/tartan_field" android:fillAlpha="0.5" android:pathData="M-90.00,74.12h288.00v16.94h-288.00z" />
<path android:fillColor="@color/tartan_ground" android:fillAlpha="0.5" android:pathData="M-90.00,91.06h288.00v16.94h-288.00z" />
<path android:fillColor="@color/tartan_over" android:fillAlpha="0.5" android:pathData="M-90.00,108.00h288.00v2.12h-288.00z" />
<path android:fillColor="@color/tartan_ground" android:fillAlpha="0.5" android:pathData="M-90.00,110.12h288.00v16.94h-288.00z" />
<path android:fillColor="@color/tartan_field" android:fillAlpha="0.5" android:pathData="M-90.00,127.06h288.00v16.94h-288.00z" />
<path android:fillColor="@color/tartan_ground" android:fillAlpha="0.5" android:pathData="M-90.00,144.00h288.00v2.12h-288.00z" />
<path android:fillColor="@color/tartan_field" android:fillAlpha="0.5" android:pathData="M-90.00,146.12h288.00v16.94h-288.00z" />
<path android:fillColor="@color/tartan_ground" android:fillAlpha="0.5" android:pathData="M-90.00,163.06h288.00v16.94h-288.00z" />
<path android:fillColor="@color/tartan_over" android:fillAlpha="0.5" android:pathData="M-90.00,180.00h288.00v2.12h-288.00z" />
<path android:fillColor="@color/tartan_ground" android:fillAlpha="0.5" android:pathData="M-90.00,182.12h288.00v16.94h-288.00z" />
</group>
</vector>
+8 -8
View File
@@ -1,16 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
The colours the brand shares between channels. The two that differ —
brand_field (slate blue on production, deep green on dev) and its darker
shade splash_background — are generated by the build. See resValue in
app/build.gradle.kts.
The colours the sett shares between channels. The one that differs —
tartan_field, red on production and hunting green on dev — is generated by the
build, along with splash_background and ic_launcher_background, which are
shades of it. See resValue in app/build.gradle.kts.
-->
<resources>
<color name="black">#FF000000</color>
<color name="white">#FFFFFFFF</color>
<!-- The parchment accent drawn on the brand field: the emblem on the
launcher icon and the second line of the splash. Same number as the
accent in Theme.kt. -->
<color name="brand_accent">#EFD9A4</color>
<!-- The ground the sett is woven on, and the overstripe down its centre.
Same numbers as WallaceBlack and WallaceGold in Tartan.kt. -->
<color name="tartan_ground">#0C0C0C</color>
<color name="tartan_over">#F2C518</color>
</resources>
+1 -1
View File
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
app_name is generated by the build rather than declared here, because it
differs by channel: a dev build installs as "Pilgrims of Katan DEV"
differs by channel: a dev build installs as "Pro Libertate Games DEV"
alongside the production copy. See resValue in app/build.gradle.kts.
-->
<resources>
+1 -1
View File
@@ -2,7 +2,7 @@
<resources>
<!-- Compose supplies the real theme; this only needs to be a Material 3
base with no action bar so the app draws its own top bars. -->
<style name="Theme.PilgrimsOfKatan" parent="android:Theme.Material.NoActionBar">
<style name="Theme.ProLibertateGames" parent="android:Theme.Material.NoActionBar">
<item name="android:windowNoTitle">true</item>
<item name="android:windowContentOverlay">@null</item>
<item name="android:windowBackground">@color/splash_background</item>
@@ -106,17 +106,19 @@ class CatalogTest {
}
@Test
fun `every game in the catalogue can be played`() {
// There is nothing left marked "coming soon". The machinery that hides
// unfinished games from a production build is still there and still
// tested above — it simply has nothing to hide at the moment, and this
// is what will fail the day something is added to the list before its
// engine is written.
assertTrue(GameCatalog.all.all { it.available })
fun `every reference game in the catalogue can be played`() {
// Every game the app ships is playable. The one exception is Katan,
// listed so the slot exists and marked *Coming soon* until its engine
// and screen land — `available = false` is what keeps an unfinished
// game out of a production build while leaving it visible on a dev one.
assertEquals(
GameCatalog.inMenu(GameMenu.BOARD, includeComingSoon = false),
GameCatalog.inMenu(GameMenu.BOARD, includeComingSoon = true),
listOf(GameCatalog.CATAN),
GameCatalog.all.filterNot { it.available }.map { it.id },
)
// It is also the only difference the two channels see in a menu.
val hiddenFromProduction = GameCatalog.inMenu(GameMenu.BOARD, includeComingSoon = true) -
GameCatalog.inMenu(GameMenu.BOARD, includeComingSoon = false)
assertEquals(listOf(GameCatalog.byId(GameCatalog.CATAN)!!), hiddenFromProduction)
}
@Test
@@ -22,7 +22,7 @@ class UpdatePolicyTest {
tag = tag,
versionCode = versionCode,
apkUrl = "https://example.invalid/$tag.apk",
apkName = "PilgrimsOfKatan-$tag.apk",
apkName = "ProLibertateGames-$tag.apk",
channel = channel,
)
@@ -60,16 +60,16 @@ class UpdatePolicyTest {
// The instance reports its canonical host; the app was told to trust
// another. Same bytes, one host, whatever the server names itself.
assertEquals(
"https://git.wallacenet.ca/devadmin/PilgrimsOfKatan/releases/download/" +
"v1.4.1001-dev/PilgrimsOfKatan-v1.4.1001-dev.apk",
"https://git.wallacenet.ca/devadmin/ProLibertateGames/releases/download/" +
"v1.4.1001-dev/ProLibertateGames-v1.4.1001-dev.apk",
onReportedHost(
"https://git.saskvalleytech.com/devadmin/PilgrimsOfKatan/releases/download/" +
"v1.4.1001-dev/PilgrimsOfKatan-v1.4.1001-dev.apk",
"https://git.saskvalleytech.com/devadmin/ProLibertateGames/releases/download/" +
"v1.4.1001-dev/ProLibertateGames-v1.4.1001-dev.apk",
"git.wallacenet.ca",
),
)
// A URL that already says the right host is left exactly as it is.
val already = "https://git.wallacenet.ca/devadmin/PilgrimsOfKatan/releases/download/v1.4.1/a.apk"
val already = "https://git.wallacenet.ca/devadmin/ProLibertateGames/releases/download/v1.4.1/a.apk"
assertEquals(already, onReportedHost(already, "git.wallacenet.ca"))
// Anything unparseable survives untouched rather than being dropped.
assertEquals("not a url", onReportedHost("not a url", "git.wallacenet.ca"))
+18 -13
View File
@@ -1,11 +1,11 @@
# CI, releases, and OTA — decisions and how to build
Status as of the Pilgrims of Katan bootstrap (WALA-118).
Status as of the ProLibertateGames bootstrap (WALA-118).
## Decisions
1. **The release channel is this instance's own releases.** The app ships from
<https://git.wallacenet.ca/devadmin/PilgrimsOfKatan>, and the in-app updater
<https://git.wallacenet.ca/devadmin/ProLibertateGames>, and the in-app updater
reads that repo's releases over Gitea's REST API. GitHub is not a channel for
this project; the only GitHub URLs left are the `upstream` remote and the
attribution link in the README.
@@ -80,15 +80,15 @@ release — the honest answer for a build nobody stamped.
export JAVA_HOME=... # a JDK 17 install
export ANDROID_HOME=... # SDK root (or set sdk.dir in local.properties)
./gradlew assembleDebug # dev-channel app (default: ca.wallacenet.pilgrimsofkatan.dev)
PLG_CHANNEL=production ./gradlew assembleDebug # production app (ca.wallacenet.pilgrimsofkatan)
./gradlew assembleDebug # dev-channel app (default: ca.wallacenet.prolibertategames.dev)
PLG_CHANNEL=production ./gradlew assembleDebug # production app (ca.wallacenet.prolibertategames)
./gradlew testDebugUnitTest # JVM unit tests
./gradlew lintDebug # Android lint (CI runs this too)
```
The two variants install side by side: different applicationId, different
launcher name (`Pilgrims of Katan DEV` vs `Pilgrims of Katan`), different brand
colour.
launcher name (`Pro Libertate Games DEV` vs `Pro Libertate Games`), different
tartan sett.
## Manual release (works today, no runner needed)
@@ -97,10 +97,15 @@ GITEA_TOKEN=… ./scripts/publish-release.sh v1.4.1001 app/build/outputs/apk/deb
```
The script takes `<tag> <apk> <prerelease|release> [target-sha]`, creates the
release through `POST /api/v1/repos/{owner}/{repo}/releases`, and attaches the
APK through `POST …/releases/{id}/assets` — which is the upload route Gitea's
Swagger declares for this instance (1.27.3). If the tag already exists it
publishes into the existing release instead of failing, so a retry is safe.
release through `POST /api/v1/repos/{owner}/{repo}/releases`, attaches the APK
through `POST …/releases/{id}/assets` — which is the upload route Gitea's
Swagger declares for this instance (1.27.3) — and then reads the release back
and corrects its state if it did not stick. That last step is not decoration:
this instance has been observed to accept `draft: false` at create time and
store the release as a draft after the attachment lands, and a draft is
invisible to the updater, so the script verifies rather than assumes. If the
tag already exists it publishes into the existing release instead of failing,
so a retry is safe.
## What the in-app updater reads
@@ -108,8 +113,8 @@ publishes into the existing release instead of failing, so a retry is safe.
| Channel | Endpoint |
| --- | --- |
| production | `GET /api/v1/repos/devadmin/PilgrimsOfKatan/releases/latest` |
| dev | `GET /api/v1/repos/devadmin/PilgrimsOfKatan/releases?limit=30` |
| production | `GET /api/v1/repos/devadmin/ProLibertateGames/releases/latest` |
| dev | `GET /api/v1/repos/devadmin/ProLibertateGames/releases?limit=30` |
- `/releases/latest` is documented by Gitea as *the most recent non-prerelease,
non-draft release* — the same rule GitHub implements, so the production
@@ -121,7 +126,7 @@ publishes into the existing release instead of failing, so a retry is safe.
`prerelease`, `draft`, and assets carrying `browser_download_url`. Gitea says
`limit` where GitHub says `per_page`.
- Each asset must be named so it contains its tag —
`PilgrimsOfKatan-v1.4.1001.apk` for tag `v1.4.1001` — because the updater
`ProLibertateGames-v1.4.1001.apk` for tag `v1.4.1001` — because the updater
picks the versioned APK out of the release's assets by that rule.
`BuildConfig.OTA_ENABLED` is a kill switch, not a channel decision: it is
+29 -4
View File
@@ -11,7 +11,7 @@
# Environment:
# GITEA_TOKEN required — a token that may create releases on the repo.
# GITEA_API API base, default https://git.wallacenet.ca/api/v1
# GITEA_REPO owner/name, default devadmin/PilgrimsOfKatan
# GITEA_REPO owner/name, default devadmin/ProLibertateGames
#
# The four arguments can also arrive as RELEASE_TAG, RELEASE_APK,
# RELEASE_KIND and RELEASE_TARGET, which is how the workflows pass them.
@@ -34,7 +34,7 @@ esac
[ -f "$apk" ] || { echo "publish-release: no such APK: ${apk:-<unset>}" >&2; exit 2; }
[ -n "${GITEA_TOKEN:-}" ] || { echo "publish-release: GITEA_TOKEN is not set" >&2; exit 2; }
api="${GITEA_API:-${GITHUB_API_URL:-https://git.wallacenet.ca/api/v1}}/repos/${GITEA_REPO:-${GITHUB_REPOSITORY:-devadmin/PilgrimsOfKatan}}"
api="${GITEA_API:-${GITHUB_API_URL:-https://git.wallacenet.ca/api/v1}}/repos/${GITEA_REPO:-${GITHUB_REPOSITORY:-devadmin/ProLibertateGames}}"
tmp="$(mktemp -d)"
trap 'rm -rf "$tmp"' EXIT INT TERM
@@ -43,7 +43,7 @@ trap 'rm -rf "$tmp"' EXIT INT TERM
# updater picks the versioned APK out of a release's assets by finding the one
# whose name contains the tag, so an asset called app-debug.apk would only be
# found by the fallback rule.
apk_name="PilgrimsOfKatan-${tag}.apk"
apk_name="ProLibertateGames-${tag}.apk"
# The tag and the file name are both derived from gradle.properties and a run
# number, so neither can carry a quote or a backslash into this JSON.
@@ -56,7 +56,7 @@ else
fi
cat > "$tmp/release.json" <<JSON
{"tag_name":"${tag}","target_commitish":"${target}","name":"Pilgrims of Katan ${tag}","body":"${note}","prerelease":${prerelease},"draft":false}
{"tag_name":"${tag}","target_commitish":"${target}","name":"Pro Libertate Games ${tag}","body":"${note}","prerelease":${prerelease},"draft":false}
JSON
code="$(curl -sS -o "$tmp/create.json" -w '%{http_code}' -X POST \
@@ -86,6 +86,10 @@ fi
id="$(grep -o '"id":[0-9]*' "$tmp/create.json" | head -n1 | tr -dc '0-9' || true)"
[ -n "$id" ] || { echo "publish-release: no release id in response" >&2; cat "$tmp/create.json" >&2; echo >&2; exit 1; }
# Attach the APK first, then check the stored state: uploading has been
# observed to leave a freshly created release as a draft, whatever the create
# response said, so the check has to be the last thing that happens rather
# than a step in the middle.
code="$(curl -sS -o "$tmp/asset.json" -w '%{http_code}' -X POST \
-H "Authorization: token ${GITEA_TOKEN}" \
-F "attachment=@${apk};filename=${apk_name}" \
@@ -98,4 +102,25 @@ if [ "$code" != "201" ] && [ "$code" != "200" ]; then
exit 1
fi
# Read back what the database actually stored before declaring success. A draft
# is invisible to the updater — exactly what a channel must not silently become
# — and this instance has been seen to report one as published at create time
# and store it as a draft by the time it is read.
curl -sS -o "$tmp/stored.json" -H "Authorization: token ${GITEA_TOKEN}" "$api/releases/${id}"
if grep -q '"draft":true' "$tmp/stored.json" ||
! grep -q "\"prerelease\":${prerelease}" "$tmp/stored.json"; then
state="$(curl -sS -o "$tmp/fix.json" -w '%{http_code}' -X PATCH \
-H "Authorization: token ${GITEA_TOKEN}" \
-H "Content-Type: application/json" \
-d "{\"draft\":false,\"prerelease\":${prerelease}}" \
"$api/releases/${id}")"
if [ "$state" != "200" ]; then
echo "publish-release: release ${tag} is stored with the wrong state (HTTP ${state})" >&2
cat "$tmp/fix.json" >&2
echo >&2
exit 1
fi
echo "publish-release: corrected the stored state of ${tag} (it was saved as a draft)"
fi
echo "publish-release: ${tag} -> release ${id} on ${api#*/repos/}, asset ${apk_name}"
+1 -1
View File
@@ -14,5 +14,5 @@ dependencyResolutionManagement {
}
}
rootProject.name = "PilgrimsOfKatan"
rootProject.name = "ProLibertateGames"
include(":app")