Launcher app: home-screen PKG to start, check and stop Control4Free

An OpenOrbis app that sends the bundled payload to GoldHEN's PayLoader,
shows its address and a QR code, and stops it through the loopback API.
It draws its screen in software in the controller page's style
(stb_truetype with Roboto), and its icon with the same code.

The package (title ID CFRE00001) carries OpenOrbis's stub libc.prx and
libSceFios2.prx in sce_module/, as the toolchain asks.

The README covers GoldHEN AutoRun, the launcher, the page and the build.
This commit is contained in:
MoHadiShibli committed 2026-10-04 19:53:45 +03:00
1 parent 6d931a05ce
commit b3863fa5f1
22 files changed
+8544

No files matched your search

+155
View File
@@ -0,0 +1,155 @@
# Control4Free
A GoldHEN payload for controlling the PS4 from a phone or PC, using touch,
keyboard, or an Xbox/other controller exposed by the browser's Gamepad API.
Virtual controllers use the PS4's native user-selection screen, so they work on
the home screen, at sign-in and in games.
**Status: the browser controller works on the development console; the launcher
app and GoldHEN AutoRun start are ready for their first console test.** Native
user sign-in, menu control and gameplay have been confirmed by the tester. Four
virtual slots are exposed; the console's device limits apply.
## Requirements
- A PS4 running GoldHEN. Development console: firmware 10.01, GoldHEN 2.4b18.10.
Starting Control4Free from GoldHEN itself (LaunchPad or AutoRun) needs 2.4b18.10
or later; the launcher app and PC sending need PayLoader.
- GoldHEN's klog server, which 2.4b18 always runs. Device creation relies on
kernel-log events to recover the virtual device's handle.
- A phone or PC on the same network.
- Turn off other GoldHEN controller plugins in games you play with Control4Free. A
plugin that takes over a signed-in user's controller can stop games from reading
the virtual controllers.
## Start it with GoldHEN (AutoRun)
GoldHEN 2.4b18.10 can run payloads on its own, from its **Payloader LaunchPad**, and
automatically each time GoldHEN starts (**AutoRun**). Set this up once and no PC or
PayLoader is needed to start Control4Free.
1. Put `control4free.elf` in `/data/payloads/` on the PS4, for example through
GoldHEN's FTP server (port 2121). GoldHEN also lists payloads in a `Payloads`
folder on a USB drive, but keep the AutoRun copy on the console.
2. In GoldHEN, open **Payloads > Payloader LaunchPad** and select
`control4free.elf` to start it now.
3. Press **Square** on it to add it to the AutoRun queue. GoldHEN then starts it
every time GoldHEN loads.
When you update Control4Free, replace `/data/payloads/control4free.elf` as well.
## Install the launcher app
The launcher is a home-screen app that shows whether Control4Free is running, its
address and a QR code for your phone, and can start or stop it.
1. Copy `build/Control4Free-0.2.0.pkg` to a USB drive and install it using the
PS4's Package Installer with GoldHEN active.
2. Open **Control4Free** from the home screen. If AutoRun already started it, the
app shows **Running**. Otherwise turn on GoldHEN's PayLoader and press
**Cross**.
3. Scan the QR code or enter the address on your phone. Select a controller there
and sign in through the PS4 screen.
4. Press **Circle** to close the app. Control4Free keeps running on its own. Reopen
the app to see the address again, or press **Square**, then **Cross**, to stop it
and disconnect all its controllers.
The app does not turn on GoldHEN or PayLoader, and does not start after a reboot by
itself; AutoRun does that.
**Upgrading from the original test payload:** disconnect controllers and use
**Stop Control4Free** in the phone menu before starting from the launcher. The
old payload has no launcher-management API, so the app refuses to start a second
copy over it. Rebooting the console also clears that old instance.
## Use
1. Stop any older Control4Free payload before loading another. They all use port
4264.
2. Start Control4Free: from GoldHEN (above), from the launcher app, or by sending
`build/control4free.elf` from a PC to GoldHEN's PayLoader on port 9090 with a
payload sender. The payload serves its own page; no PC web server is needed.
3. Open `http://YOUR-PS4-IP:4264` in the phone or PC browser.
4. Select a free controller. This creates it and opens native PS4 user selection.
Use Left/Right and Cross to select a user or follow the PS4's guest flow.
**Do not press PS while the initial user-selection screen is open:** in the
hardware experiment this cancelled selection. PS works after sign-in.
5. Test the home screen and then a game before adding another controller.
For an Xbox or other physical controller, connect it to the phone/PC, press a
button so the browser detects it, then choose its virtual-controller slot under
connected controllers. Sign it in through the same PS4 screen. Add and sign in
controllers one at a time. Touch and keyboard can also operate the selected slot.
The corner menu provides local layout/keyboard preferences, **Disconnect
controller**, and **Stop Control4Free**. PS and Share send actual controller
buttons. The PS4 assigns users; there are no pad or user settings on the page.
Some browsers restrict Gamepad API access on an HTTP page. If the page reports
that restriction, try opening a saved copy of `client/index.html` locally and
entering `YOUR-PS4-IP:4264` in its connection settings. Browser support for local
files varies. Keep the controller page in the foreground while playing.
## Connection behavior
- Opening the page does not create a controller. Creation follows an explicit
controller selection; the payload never presses sign-in buttons automatically.
- Each virtual slot has one browser owner. Multiple local inputs may share that
browser's selected slot, or use separate slots.
- A lost connection releases held buttons. After 3 seconds without updates,
inputs become neutral; after 15 seconds the controller is removed. A short
disconnect leaves a 15-second window to select that controller again.
- Explicit disconnect removes the controller immediately. Stopping the payload
removes all controllers and restores the saved host credentials. Browser Stop
refuses while another browser owns a controller. The launcher's confirmed Stop
can disconnect all controllers through its loopback-only API.
- If the kernel log is not available yet when Control4Free starts (AutoRun can
start it early), it connects to it when the first controller is created.
- There is no time limit. Rebooting the console ends it; AutoRun, the launcher or
a PC starts it again.
## Build
The [ps4-payload-sdk](https://github.com/ps4-payload-dev/sdk) toolchain is pinned
in the Docker image:
```sh
docker build -t control4free-build docker/
docker run --rm -v "$PWD:/src" -w /src control4free-build make
```
Output: `build/control4free.elf`, with the compressed page embedded. `C4F_STAGE=0`
is the default browser build. Stages 1-8 remain available as diagnostics, with
separate `control4free-stageN.elf` outputs.
To build the installable launcher after the payload image is available:
```sh
docker build -t control4free-launcher-build -f docker/Dockerfile.launcher docker/
docker run --rm -v "$PWD:/src" -w /src control4free-launcher-build bash -lc 'make && make -C launcher'
```
Output: `build/Control4Free-0.2.0.pkg` (title ID `CFRE00001`). On Windows,
`tools/build-launcher.ps1` runs both image builds and the package build. It does
not send anything to a console. Packaging uses OpenOrbis and LibOrbisPkg. The
launcher draws its screen and its icon in software, in the controller page's style.
## Current limits
Rumble, lightbar feedback and motion input are not implemented. Rest-mode
recovery and multiple native user sessions are unverified. User-assignment status
comes from kernel-log events and may lag or miss an event; check the TV.
Logs are written to `/data/control4free/spike.log` on the console.
## Credits and license
The VDA code is ported from [seregonwar/SplashDown](https://github.com/seregonwar/SplashDown).
The build uses John Törnblom's [ps4-payload-sdk](https://github.com/ps4-payload-dev/sdk)
and the [OpenOrbis PS4 Toolchain](https://github.com/OpenOrbis/OpenOrbis-PS4-Toolchain).
[Ghostcontrol](https://github.com/srbraboo/Ghostcontrol-PS5-USB-Controller-Patcher)
was a research reference. JSON parsing uses [jsmn](https://github.com/zserge/jsmn);
the launcher uses [QR Code generator](https://github.com/nayuki/QR-Code-generator),
[stb_truetype](https://github.com/nothings/stb) and the
[Roboto](https://github.com/googlefonts/roboto) font.
GPL-3.0; see [LICENSE](LICENSE) and [THIRD_PARTY.md](THIRD_PARTY.md).
+33
View File
@@ -0,0 +1,33 @@
# Third-party material
- The virtual-device implementation derives from seregonwar/SplashDown's
`psbutton.c` (https://github.com/seregonwar/SplashDown), GPL-3.0.
- `vendor/jsmn/jsmn.h` is from https://github.com/zserge/jsmn, copyright Serge
Zaitsev, under the MIT license included in `vendor/jsmn/LICENSE`.
Retrieved 2026-10-04; SHA-256:
`c04533e9181e1e33baceb0f55ac449b05145bb936e8c68cc77dfe0d8277514fb`.
- The toolchain uses https://github.com/ps4-payload-dev/sdk; its version is pinned
in `docker/Dockerfile` and it retains its own licensing.
- The native launcher uses the OpenOrbis PS4 Toolchain and its software-display
initialization pattern (https://github.com/OpenOrbis/OpenOrbis-PS4-Toolchain,
GPL-3.0). The toolchain supplies `sce_sys/about/right.sprx` and its stub
`sce_module/libc.prx` and `sce_module/libSceFios2.prx`; LibOrbisPkg produces the
package. `docker/Dockerfile.launcher` pins the OpenOrbis image by digest.
- `vendor/qrcodegen/qrcodegen.c` and `.h` are Project Nayuki's QR generator v1.8.0
(https://github.com/nayuki/QR-Code-generator/tree/v1.8.0/c), MIT. The full license
is retained at the top of each file. SHA-256 for `.c`:
`300eff07ee25baaa7578f20284411638154716379437391e7e689c0e6ce81403`;
for `.h`: `e82df4bff37d18b5863b9e7486fe6bda1b6cda8c3b9ecebfec473907265cb589`.
- `vendor/stb/stb_truetype.h` is Sean Barrett's stb_truetype v1.26
(https://github.com/nothings/stb), public domain or MIT, as chosen at the end of
the file. Retrieved 2026-10-04; SHA-256:
`ecd30b05e0dd4fea3a13c26810dd9e1992dc379049482c393d5a19e6b5090aab`.
- `vendor/roboto/` holds Roboto Light and Regular v2.138
(https://github.com/googlefonts/roboto), Apache License 2.0
(`vendor/roboto/LICENSE`). The launcher draws its text with them. SHA-256:
`Roboto-Light.ttf` `a08729d794801eaa158c53b1558f4cc351c3b1791eb3d22faf7058b2b7df9c8c`,
`Roboto-Regular.ttf` `f3edb8058e523f5612bfd99d0745e661568ad85e1b6217bc62f786fabae624c6`.
These license notices are also included inside the package.
The HTTP/WebSocket transport in `src/net.c` is a new implementation.
+14
View File
@@ -0,0 +1,14 @@
# Reuse the pinned OpenOrbis release from the existing plugin toolchain image.
ARG OPENORBIS_IMAGE=xfangfang/goldhen_plugin@sha256:e3d2acad69a1905472bd9d73e373d1d0fb6d3ca293d9f9a6ba957b9b2f49fffc
FROM ${OPENORBIS_IMAGE} AS openorbis
FROM ubuntu:20.04 AS pkgcompat
RUN apt-get update && apt-get install -y --no-install-recommends libssl1.1 \
&& rm -rf /var/lib/apt/lists/*
FROM control4free-build:latest
COPY --from=openorbis /opt/OpenOrbis/PS4Toolchain /opt/OpenOrbis/PS4Toolchain
# The SDK's self-contained LibOrbisPkg runtime expects OpenSSL 1.1.
COPY --from=pkgcompat /usr/lib/x86_64-linux-gnu/libssl.so.1.1 /usr/lib/x86_64-linux-gnu/
COPY --from=pkgcompat /usr/lib/x86_64-linux-gnu/libcrypto.so.1.1 /usr/lib/x86_64-linux-gnu/
ENV OO_PS4_TOOLCHAIN=/opt/OpenOrbis/PS4Toolchain
ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1
WORKDIR /src
+36
View File
@@ -0,0 +1,36 @@
ifndef OO_PS4_TOOLCHAIN
$(error OO_PS4_TOOLCHAIN is undefined; use the launcher Docker image)
endif
SDK := $(OO_PS4_TOOLCHAIN)
OUT := ../build/launcher
PKGDIR := $(OUT)/pkg
CC := clang-18
CXX := clang++-18
LD := ld.lld-18
CFLAGS := --target=x86_64-pc-freebsd12-elf -fPIC -funwind-tables -O2 -Wall -Wextra -Werror -MMD -MP -D_DEFAULT_SOURCE -isysroot $(SDK) -isystem $(SDK)/include -I../vendor/jsmn -I../vendor/qrcodegen -I../vendor/stb
OBJECTS := $(OUT)/main.o $(OUT)/screen.o $(OUT)/draw.o $(OUT)/service.o $(OUT)/qrcodegen.o $(OUT)/stb_truetype.o $(OUT)/fonts.o
LIBS := -lc -lkernel -lSceVideoOut -lScePad -lSceUserService -lSceSystemService -lSceNetCtl
FONTS := ../vendor/roboto/Roboto-Light.ttf ../vendor/roboto/Roboto-Regular.ttf
.PHONY: all package
.DEFAULT_GOAL := all
-include $(OBJECTS:.o=.d)
all: package
$(OUT):
mkdir -p $@
$(OUT)/%.o: %.c | $(OUT)
$(CC) $(CFLAGS) -std=gnu11 -c $< -o $@
$(OUT)/main.o: main.cpp | $(OUT)
$(CXX) $(CFLAGS) -std=c++17 -fno-exceptions -fno-rtti -c $< -o $@
$(OUT)/qrcodegen.o: ../vendor/qrcodegen/qrcodegen.c | $(OUT)
$(CC) $(CFLAGS) -std=gnu11 -c $< -o $@
# Vendor code, built with its warnings off.
$(OUT)/stb_truetype.o: stb_truetype.c | $(OUT)
$(CC) $(CFLAGS) -std=gnu11 -w -c $< -o $@
$(OUT)/fonts.c: $(FONTS) ../tools/embed_file.py | $(OUT)
python3 ../tools/embed_file.py $@ c4fFontLight=../vendor/roboto/Roboto-Light.ttf c4fFontRegular=../vendor/roboto/Roboto-Regular.ttf
$(OUT)/fonts.o: $(OUT)/fonts.c
$(CC) $(CFLAGS) -std=gnu11 -c $< -o $@
$(OUT)/launcher.elf: $(OBJECTS)
$(LD) -m elf_x86_64 -pie --script $(SDK)/link.x --eh-frame-hdr -o $@ $(OBJECTS) -L$(SDK)/lib $(LIBS) $(SDK)/lib/crt1.o
package: $(OUT)/launcher.elf ../build/control4free.elf
python3 ../tools/package_launcher.py --sdk $(SDK)
+348
View File
@@ -0,0 +1,348 @@
/* Software drawing for the native launcher. Shape coverage comes from signed
* distances, text from stb_truetype; everything is blended in 8-bit ARGB. */
#include "draw.h"
#include <math.h>
#include <stdlib.h>
#include <string.h>
#include "stb_truetype.h"
extern const unsigned char c4fFontLight[], c4fFontRegular[];
typedef struct { unsigned char *bitmap; int w, h, x0, y0, ready; float advance; } C4fGlyph;
typedef struct { int font, size; float scale; C4fGlyph glyph[95]; } C4fFace;
static stbtt_fontinfo c4fFonts[C4F_FONT_COUNT];
static C4fFace c4fFaces[24];
static int c4fFaceCount;
static uint8_t c4fTextGamma[256];
int c4fDrawInit(void)
{
const unsigned char *data[C4F_FONT_COUNT] = { c4fFontLight, c4fFontRegular };
for (int i = 0; i < C4F_FONT_COUNT; i++)
if (!stbtt_InitFont(&c4fFonts[i], data[i], stbtt_GetFontOffsetForIndex(data[i], 0))) return -1;
/* Light strokes on a dark background read thinner than a browser draws
* them; lift partial coverage a little. */
for (int i = 0; i < 256; i++) c4fTextGamma[i] = (uint8_t)(powf(i / 255.0f, 0.8f) * 255.0f + 0.5f);
return 0;
}
static float c4fClamp01(float v) { return v < 0 ? 0 : v > 1 ? 1 : v; }
static int c4fMinI(int a, int b) { return a < b ? a : b; }
static int c4fMaxI(int a, int b) { return a > b ? a : b; }
void c4fBlend(C4fCanvas *c, int x, int y, C4fColor color, float coverage)
{
if ((unsigned)x >= (unsigned)c->w || (unsigned)y >= (unsigned)c->h) return;
int a = (int)(coverage * color.a + 0.5f);
if (a <= 0) return;
uint32_t *p = &c->px[(size_t)y * c->w + x];
if (a >= 255) { *p = 0xff000000u | (uint32_t)color.r << 16 | (uint32_t)color.g << 8 | color.b; return; }
uint32_t d = *p;
unsigned inv = 255u - (unsigned)a;
unsigned r = (((d >> 16) & 255u) * inv + color.r * (unsigned)a + 127u) / 255u;
unsigned g = (((d >> 8) & 255u) * inv + color.g * (unsigned)a + 127u) / 255u;
unsigned b = ((d & 255u) * inv + color.b * (unsigned)a + 127u) / 255u;
*p = 0xff000000u | r << 16 | g << 8 | b;
}
void c4fFillRect(C4fCanvas *c, int x, int y, int w, int h, C4fColor color)
{
for (int py = c4fMaxI(y, 0); py < c4fMinI(y + h, c->h); py++)
for (int px = c4fMaxI(x, 0); px < c4fMinI(x + w, c->w); px++) c4fBlend(c, px, py, color, 1);
}
typedef struct { int x0, y0, x1, y1; } C4fBox;
static int c4fClip(const C4fCanvas *c, float x0, float y0, float x1, float y1, C4fBox *b)
{
b->x0 = c4fMaxI((int)floorf(x0), 0);
b->y0 = c4fMaxI((int)floorf(y0), 0);
b->x1 = c4fMinI((int)ceilf(x1), c->w);
b->y1 = c4fMinI((int)ceilf(y1), c->h);
return b->x0 < b->x1 && b->y0 < b->y1;
}
/* Signed distance to a rounded box (centre, half size, corner radius). */
float c4fRoundBoxDistance(float px, float py, float cx, float cy, float hx, float hy, float r)
{
float qx = fabsf(px - cx) - (hx - r), qy = fabsf(py - cy) - (hy - r);
float ox = qx > 0 ? qx : 0, oy = qy > 0 ? qy : 0, inner = qx > qy ? qx : qy;
return sqrtf(ox * ox + oy * oy) + (inner < 0 ? inner : 0) - r;
}
typedef struct { float cx, cy, hx, hy, r; } C4fShape;
static C4fShape c4fShape(float x, float y, float w, float h, float r)
{
C4fShape s = { x + w / 2, y + h / 2, w / 2, h / 2, r };
if (s.r > s.hx) s.r = s.hx;
if (s.r > s.hy) s.r = s.hy;
if (s.r < 0) s.r = 0;
return s;
}
static float c4fShapeDistance(const C4fShape *s, int px, int py)
{
return c4fRoundBoxDistance(px + 0.5f, py + 0.5f, s->cx, s->cy, s->hx, s->hy, s->r);
}
void c4fFillRoundRect(C4fCanvas *c, float x, float y, float w, float h, float r, C4fColor fill)
{
C4fShape s = c4fShape(x, y, w, h, r);
C4fBox b;
if (!c4fClip(c, x - 1, y - 1, x + w + 1, y + h + 1, &b)) return;
for (int py = b.y0; py < b.y1; py++)
for (int px = b.x0; px < b.x1; px++) {
float cov = c4fClamp01(0.5f - c4fShapeDistance(&s, px, py));
if (cov > 0) c4fBlend(c, px, py, fill, cov);
}
}
void c4fInsetRoundRect(C4fCanvas *c, float x, float y, float w, float h, float r, float width, C4fColor line)
{
C4fShape s = c4fShape(x, y, w, h, r);
C4fBox b;
if (!c4fClip(c, x - 1, y - 1, x + w + 1, y + h + 1, &b)) return;
for (int py = b.y0; py < b.y1; py++)
for (int px = b.x0; px < b.x1; px++) {
float d = c4fShapeDistance(&s, px, py);
float cov = c4fClamp01(0.5f - d) - c4fClamp01(0.5f - (d + width));
if (cov > 0) c4fBlend(c, px, py, line, cov);
}
}
void c4fSheenRoundRect(C4fCanvas *c, float x, float y, float w, float h, float r, float angle,
C4fColor color, float a0, float a1, float stop)
{
C4fShape s = c4fShape(x, y, w, h, r);
C4fBox b;
/* CSS gradient angles: 0deg points up, clockwise. */
float rad = angle * 3.14159265f / 180.0f, dx = sinf(rad), dy = -cosf(rad);
float length = fabsf(w * dx) + fabsf(h * dy);
if (!c4fClip(c, x - 1, y - 1, x + w + 1, y + h + 1, &b)) return;
for (int py = b.y0; py < b.y1; py++)
for (int px = b.x0; px < b.x1; px++) {
float cov = c4fClamp01(0.5f - c4fShapeDistance(&s, px, py));
if (cov <= 0) continue;
float t = ((px + 0.5f - s.cx) * dx + (py + 0.5f - s.cy) * dy) / length + 0.5f;
float a = t >= stop ? a1 : t <= 0 ? a0 : a0 + (a1 - a0) * (t / stop);
c4fBlend(c, px, py, color, cov * a);
}
}
void c4fBandRoundRect(C4fCanvas *c, float x, float y, float w, float h, float r, float height, C4fColor color)
{
C4fShape s = c4fShape(x, y, w, h, r);
C4fBox b;
if (!c4fClip(c, x - 1, y - 1, x + w + 1, y + height + 1, &b)) return;
for (int py = b.y0; py < b.y1; py++) {
float band = c4fClamp01(fminf(py + 1.0f, y + height) - fmaxf((float)py, y));
if (band <= 0) continue;
for (int px = b.x0; px < b.x1; px++) {
float cov = c4fClamp01(0.5f - c4fShapeDistance(&s, px, py)) * band;
if (cov > 0) c4fBlend(c, px, py, color, cov);
}
}
}
/* A blurred edge: the normal CDF, approximated by a logistic curve. */
static float c4fSoftEdge(float distance, float sigma)
{
return 1.0f / (1.0f + expf(1.702f * distance / sigma));
}
void c4fShadowRoundRect(C4fCanvas *c, float x, float y, float w, float h, float r,
float dx, float dy, float blur, float spread, C4fColor color)
{
float sigma = blur > 1 ? blur / 2 : 0.5f, reach = 3 * sigma;
C4fShape own = c4fShape(x, y, w, h, r);
C4fShape shadow = c4fShape(x + dx - spread, y + dy - spread, w + 2 * spread, h + 2 * spread, r + spread);
C4fBox b;
if (shadow.hx <= 0 || shadow.hy <= 0) return;
if (!c4fClip(c, fminf(x, x + dx - spread) - reach, fminf(y, y + dy - spread) - reach,
fmaxf(x + w, x + dx + w + spread) + reach, fmaxf(y + h, y + dy + h + spread) + reach, &b)) return;
for (int py = b.y0; py < b.y1; py++)
for (int px = b.x0; px < b.x1; px++) {
/* An outer box-shadow is never painted under the element itself. */
float inside = c4fClamp01(0.5f - c4fShapeDistance(&own, px, py));
if (inside >= 1) continue;
float d = c4fShapeDistance(&shadow, px, py);
if (d > reach) continue;
c4fBlend(c, px, py, color, c4fSoftEdge(d, sigma) * (1 - inside));
}
}
void c4fFillCircle(C4fCanvas *c, float cx, float cy, float r, C4fColor fill)
{
C4fBox b;
if (!c4fClip(c, cx - r - 1, cy - r - 1, cx + r + 1, cy + r + 1, &b)) return;
for (int py = b.y0; py < b.y1; py++)
for (int px = b.x0; px < b.x1; px++) {
float cov = c4fClamp01(0.5f - (hypotf(px + 0.5f - cx, py + 0.5f - cy) - r));
if (cov > 0) c4fBlend(c, px, py, fill, cov);
}
}
void c4fRing(C4fCanvas *c, float cx, float cy, float r, float width, C4fColor line)
{
C4fBox b;
if (!c4fClip(c, cx - r - 1, cy - r - 1, cx + r + 1, cy + r + 1, &b)) return;
for (int py = b.y0; py < b.y1; py++)
for (int px = b.x0; px < b.x1; px++) {
float d = hypotf(px + 0.5f - cx, py + 0.5f - cy) - r;
float cov = c4fClamp01(0.5f - d) - c4fClamp01(0.5f - (d + width));
if (cov > 0) c4fBlend(c, px, py, line, cov);
}
}
void c4fGlowCircle(C4fCanvas *c, float cx, float cy, float r, float blur, float spread, C4fColor color)
{
float sigma = blur > 1 ? blur / 2 : 0.5f, reach = r + spread + 3 * sigma;
C4fBox b;
if (!c4fClip(c, cx - reach, cy - reach, cx + reach, cy + reach, &b)) return;
for (int py = b.y0; py < b.y1; py++)
for (int px = b.x0; px < b.x1; px++) {
float distance = hypotf(px + 0.5f - cx, py + 0.5f - cy);
float inside = c4fClamp01(0.5f - (distance - r));
if (inside >= 1) continue;
c4fBlend(c, px, py, color, c4fSoftEdge(distance - (r + spread), sigma) * (1 - inside));
}
}
void c4fSegment(C4fCanvas *c, float x0, float y0, float x1, float y1, float width, C4fColor color)
{
float hw = width / 2, vx = x1 - x0, vy = y1 - y0, length2 = vx * vx + vy * vy;
C4fBox b;
if (!c4fClip(c, fminf(x0, x1) - hw - 1, fminf(y0, y1) - hw - 1, fmaxf(x0, x1) + hw + 1, fmaxf(y0, y1) + hw + 1, &b)) return;
for (int py = b.y0; py < b.y1; py++)
for (int px = b.x0; px < b.x1; px++) {
float fx = px + 0.5f - x0, fy = py + 0.5f - y0;
float t = length2 > 0 ? c4fClamp01((fx * vx + fy * vy) / length2) : 0;
float cov = c4fClamp01(0.5f - (hypotf(fx - t * vx, fy - t * vy) - hw));
if (cov > 0) c4fBlend(c, px, py, color, cov);
}
}
void c4fSquareOutline(C4fCanvas *c, float cx, float cy, float half, float width, C4fColor color)
{
float reach = half + width;
C4fBox b;
if (!c4fClip(c, cx - reach - 1, cy - reach - 1, cx + reach + 1, cy + reach + 1, &b)) return;
for (int py = b.y0; py < b.y1; py++)
for (int px = b.x0; px < b.x1; px++) {
float d = c4fRoundBoxDistance(px + 0.5f, py + 0.5f, cx, cy, half, half, width * 0.4f);
float cov = c4fClamp01(0.5f - (fabsf(d) - width / 2));
if (cov > 0) c4fBlend(c, px, py, color, cov);
}
}
/* ---- text ---- */
static C4fFace *c4fFace(int font, float size)
{
int pixels = (int)(size + 0.5f);
for (int i = 0; i < c4fFaceCount; i++)
if (c4fFaces[i].font == font && c4fFaces[i].size == pixels) return &c4fFaces[i];
if (font < 0 || font >= C4F_FONT_COUNT || c4fFaceCount == (int)(sizeof(c4fFaces) / sizeof(c4fFaces[0]))) return NULL;
C4fFace *face = &c4fFaces[c4fFaceCount++];
memset(face, 0, sizeof(*face));
face->font = font;
face->size = pixels;
/* CSS font-size is the em size. */
face->scale = stbtt_ScaleForMappingEmToPixels(&c4fFonts[font], (float)pixels);
return face;
}
static int c4fCodepoint(unsigned char ch) { return ch < 32 || ch > 126 ? '?' : ch; }
static C4fGlyph *c4fGlyph(C4fFace *face, int ch)
{
C4fGlyph *g = &face->glyph[ch - 32];
if (!g->ready) {
const stbtt_fontinfo *font = &c4fFonts[face->font];
int advance, bearing, x0, y0, x1, y1;
stbtt_GetCodepointHMetrics(font, ch, &advance, &bearing);
stbtt_GetCodepointBitmapBox(font, ch, face->scale, face->scale, &x0, &y0, &x1, &y1);
g->advance = advance * face->scale;
g->x0 = x0;
g->y0 = y0;
g->w = x1 - x0;
g->h = y1 - y0;
if (g->w > 0 && g->h > 0 && (g->bitmap = malloc((size_t)g->w * (size_t)g->h)))
stbtt_MakeCodepointBitmap(font, g->bitmap, g->w, g->h, g->w, face->scale, face->scale, ch);
g->ready = 1;
}
return g;
}
static float c4fTextWidthN(int font, float size, const char *text, int length)
{
C4fFace *face = c4fFace(font, size);
float width = 0;
int previous = 0;
if (!face) return 0;
for (int i = 0; i < length && text[i]; i++) {
int ch = c4fCodepoint((unsigned char)text[i]);
if (previous) width += stbtt_GetCodepointKernAdvance(&c4fFonts[font], previous, ch) * face->scale;
width += c4fGlyph(face, ch)->advance;
previous = ch;
}
return width;
}
float c4fTextWidth(int font, float size, const char *text)
{
return c4fTextWidthN(font, size, text, (int)strlen(text));
}
float c4fTextN(C4fCanvas *c, int font, float size, float x, float y, C4fColor color, const char *text, int length)
{
C4fFace *face = c4fFace(font, size);
float pen = x;
int previous = 0, baseline = (int)floorf(y + 0.5f);
if (!face) return 0;
for (int i = 0; i < length && text[i]; i++) {
int ch = c4fCodepoint((unsigned char)text[i]);
if (previous) pen += stbtt_GetCodepointKernAdvance(&c4fFonts[font], previous, ch) * face->scale;
C4fGlyph *g = c4fGlyph(face, ch);
if (g->bitmap) {
int gx = (int)floorf(pen + 0.5f) + g->x0, gy = baseline + g->y0;
for (int row = 0; row < g->h; row++)
for (int col = 0; col < g->w; col++) {
unsigned value = g->bitmap[row * g->w + col];
if (value) c4fBlend(c, gx + col, gy + row, color, c4fTextGamma[value] / 255.0f);
}
}
pen += g->advance;
previous = ch;
}
return pen - x;
}
float c4fText(C4fCanvas *c, int font, float size, float x, float y, C4fColor color, const char *text)
{
return c4fTextN(c, font, size, x, y, color, text, (int)strlen(text));
}
int c4fTextWrapped(C4fCanvas *c, int font, float size, float x, float y, float maxWidth,
float lineHeight, int maxLines, C4fColor color, const char *text)
{
int lines = 0;
while (*text && lines < maxLines) {
while (*text == ' ') text++;
if (!*text) break;
int fit = 0, end = 0;
for (;;) {
while (text[end] && text[end] != ' ') end++;
if (fit && c4fTextWidthN(font, size, text, end) > maxWidth) break;
fit = end;
if (!text[end]) break;
end++;
}
if (c) c4fTextN(c, font, size, x, y + lines * lineHeight, color, text, fit);
lines++;
text += fit;
}
return lines;
}
+52
View File
@@ -0,0 +1,52 @@
#ifndef C4F_LAUNCHER_DRAW_H
#define C4F_LAUNCHER_DRAW_H
/* Software drawing into a cached ARGB buffer: anti-aliased shapes and text.
* Blending reads the destination, so never draw straight into the
* write-combined framebuffer. */
#include <stdint.h>
typedef struct { uint32_t *px; int w, h; } C4fCanvas;
typedef struct { uint8_t r, g, b, a; } C4fColor;
#define C4F_RGBA(r, g, b, a) ((C4fColor){ (r), (g), (b), (uint8_t)((a) * 255.0f + 0.5f) })
enum { C4F_FONT_LIGHT, C4F_FONT_REGULAR, C4F_FONT_COUNT };
int c4fDrawInit(void);
/* Signed distance to a rounded box: centre, half size, corner radius. */
float c4fRoundBoxDistance(float px, float py, float cx, float cy, float hx, float hy, float r);
void c4fBlend(C4fCanvas *c, int x, int y, C4fColor color, float coverage);
void c4fFillRect(C4fCanvas *c, int x, int y, int w, int h, C4fColor color);
/* Rounded rectangles; x/y/w/h/r in pixels, fractions allowed. */
void c4fFillRoundRect(C4fCanvas *c, float x, float y, float w, float h, float r, C4fColor fill);
/* A CSS "inset 0 0 0 <width>px" line along the inside edge. */
void c4fInsetRoundRect(C4fCanvas *c, float x, float y, float w, float h, float r, float width, C4fColor line);
/* A linear gradient clipped to the shape: alpha a0 at the start, a1 from
* `stop` on, along a CSS angle in degrees. */
void c4fSheenRoundRect(C4fCanvas *c, float x, float y, float w, float h, float r, float angle,
C4fColor color, float a0, float a1, float stop);
/* The part of the shape between top and top+height (a tile's colour bar). */
void c4fBandRoundRect(C4fCanvas *c, float x, float y, float w, float h, float r, float height, C4fColor color);
/* A CSS outer box-shadow: drawn only outside the shape itself. */
void c4fShadowRoundRect(C4fCanvas *c, float x, float y, float w, float h, float r,
float dx, float dy, float blur, float spread, C4fColor color);
void c4fFillCircle(C4fCanvas *c, float cx, float cy, float r, C4fColor fill);
void c4fRing(C4fCanvas *c, float cx, float cy, float r, float width, C4fColor line);
/* Soft halo around a circle, like box-shadow 0 0 <blur>px <spread>px. */
void c4fGlowCircle(C4fCanvas *c, float cx, float cy, float r, float blur, float spread, C4fColor color);
/* A line with round caps. */
void c4fSegment(C4fCanvas *c, float x0, float y0, float x1, float y1, float width, C4fColor color);
void c4fSquareOutline(C4fCanvas *c, float cx, float cy, float half, float width, C4fColor color);
/* Text on a baseline. Sizes are CSS font-size in pixels. ASCII only. */
float c4fText(C4fCanvas *c, int font, float size, float x, float y, C4fColor color, const char *text);
float c4fTextN(C4fCanvas *c, int font, float size, float x, float y, C4fColor color, const char *text, int length);
float c4fTextWidth(int font, float size, const char *text);
/* Word-wrapped text; returns the number of lines (drawn only if c is set). */
int c4fTextWrapped(C4fCanvas *c, int font, float size, float x, float y, float maxWidth,
float lineHeight, int maxLines, C4fColor color, const char *text);
#endif
+192
View File
@@ -0,0 +1,192 @@
/* Native PS4 launcher. The controller service runs outside this application. */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
#include <unistd.h>
#include <orbis/libkernel.h>
#include <orbis/NetCtl.h>
#include <orbis/Pad.h>
#include <orbis/SystemService.h>
#include <orbis/UserService.h>
#include <orbis/VideoOut.h>
#include "screen.h"
#include "service.h"
static pthread_mutex_t c4fMutex = PTHREAD_MUTEX_INITIALIZER;
static C4fLauncherScreen c4fScreen;
static int c4fCommand, c4fQuit;
static void *c4fWorker(void *)
{
uint64_t nextCheck = 0;
int first = 1;
for (;;) {
pthread_mutex_lock(&c4fMutex);
int command = c4fCommand, quit = c4fQuit;
c4fCommand = 0;
pthread_mutex_unlock(&c4fMutex);
if (quit) return NULL;
if (command || c4fLauncherTimeMs() >= nextCheck) {
char message[160] = {0}, address[64] = {0};
int result = 0;
if (command == 1) result = c4fLauncherStart("/app0/assets/control4free.elf", message, sizeof(message));
if (command == 2) result = c4fLauncherStop(message, sizeof(message));
C4fServiceStatus status;
int running = c4fLauncherProbe(&status);
OrbisNetCtlInfo info;
memset(&info, 0, sizeof(info));
if (sceNetCtlGetInfo(ORBIS_NET_CTL_INFO_IP_ADDRESS, &info) == 0 &&
info.ip_address[0] && strcmp(info.ip_address, "0.0.0.0"))
snprintf(address, sizeof(address), "http://%.15s:4264", info.ip_address);
pthread_mutex_lock(&c4fMutex);
if (result == -2) c4fScreen.locked = 1;
if (command) snprintf(c4fScreen.message, sizeof(c4fScreen.message), "%s", message);
else if (!c4fScreen.locked && (first || running != c4fScreen.running)) {
snprintf(c4fScreen.message, sizeof(c4fScreen.message), "%s",
running == 1 ? "Ready. Scan the code or open the address on your phone." :
running == 0 ? "Not running. Press Cross to start it." :
"Something else answers on port 4264. Stop the old copy from its phone page.");
}
c4fScreen.running = running;
c4fScreen.controllers = status.controllers;
snprintf(c4fScreen.address, sizeof(c4fScreen.address), "%s", address);
/* A queued click while a periodic probe was in flight remains busy. */
if (!c4fCommand) c4fScreen.busy = 0;
pthread_mutex_unlock(&c4fMutex);
first = 0; nextCheck = c4fLauncherTimeMs() + 2000;
}
usleep(100000);
}
}
static int c4fVideo = -1;
static OrbisKernelEqueue c4fFlipQueue;
static int c4fHasQueue, c4fBufferRegistration = -1;
static void *c4fVideoMemory;
static off_t c4fVideoOffset;
static size_t c4fVideoSize;
static void *c4fBuffers[2];
static int c4fInitVideo(void)
{
c4fVideo = sceVideoOutOpen(ORBIS_VIDEO_USER_MAIN, ORBIS_VIDEO_OUT_BUS_MAIN, 0, NULL);
if (c4fVideo < 0) return -1;
if (sceKernelCreateEqueue(&c4fFlipQueue, "Control4Free screen") < 0) return -1;
c4fHasQueue = 1;
if (sceVideoOutAddFlipEvent(c4fFlipQueue, c4fVideo, NULL) < 0) return -1;
const size_t frameSize = C4F_SCREEN_WIDTH * C4F_SCREEN_HEIGHT * sizeof(uint32_t);
const size_t alignment = 0x200000;
size_t size = (frameSize * 2 + alignment - 1) & ~(alignment - 1);
if (sceKernelAllocateDirectMemory(0, sceKernelGetDirectMemorySize(), size, alignment, 3, &c4fVideoOffset) < 0) return -1;
c4fVideoSize = size;
if (sceKernelMapDirectMemory(&c4fVideoMemory, size, 0x33, 0, c4fVideoOffset, alignment) < 0) return -1;
c4fBuffers[0] = c4fVideoMemory;
c4fBuffers[1] = (char *)c4fVideoMemory + frameSize;
OrbisVideoOutBufferAttribute attr;
memset(&attr, 0, sizeof(attr));
/* Same linear ARGB format as the OpenOrbis software-rendering sample. */
sceVideoOutSetBufferAttribute(&attr, 0x80000000, 1, 0, C4F_SCREEN_WIDTH, C4F_SCREEN_HEIGHT, C4F_SCREEN_WIDTH);
c4fBufferRegistration = sceVideoOutRegisterBuffers(c4fVideo, 0, c4fBuffers, 2, &attr);
if (c4fBufferRegistration < 0) return -1;
return sceVideoOutSetFlipRate(c4fVideo, 1); /* 30 Hz is enough for a launcher. */
}
static void c4fCloseVideo(void)
{
if (c4fVideo >= 0) {
if (c4fBufferRegistration >= 0) sceVideoOutUnregisterBuffers(c4fVideo, c4fBufferRegistration);
sceVideoOutClose(c4fVideo);
}
if (c4fHasQueue) sceKernelDeleteEqueue(c4fFlipQueue);
if (c4fVideoMemory) sceKernelMunmap(c4fVideoMemory, c4fVideoSize);
if (c4fVideoSize) sceKernelReleaseDirectMemory(c4fVideoOffset, c4fVideoSize);
}
int main(void)
{
const size_t frameBytes = (size_t)C4F_SCREEN_WIDTH * C4F_SCREEN_HEIGHT * sizeof(uint32_t);
setvbuf(stdout, NULL, _IONBF, 0);
printf("[c4f-launcher] start " C4F_LAUNCHER_VERSION "\n");
/* Frames are composed in cached memory: blending reads pixels back, which
* is very slow from the write-combined framebuffers. */
uint32_t *canvas = (uint32_t *)malloc(frameBytes);
if (!canvas || c4fScreenInit() < 0 || c4fInitVideo() < 0) {
printf("[c4f-launcher] display initialization failed\n");
c4fCloseVideo(); free(canvas); return 1;
}
sceNetCtlInit();
OrbisUserServiceInitializeParams params = {};
params.priority = ORBIS_KERNEL_PRIO_FIFO_LOWEST;
sceUserServiceInitialize(&params);
int32_t user = -1, pad = -1;
if (scePadInit() == 0 && sceUserServiceGetInitialUser(&user) == 0)
pad = scePadOpen(user, ORBIS_PAD_PORT_TYPE_STANDARD, 0, NULL);
c4fScreen.running = -1; c4fScreen.busy = 1;
snprintf(c4fScreen.message, sizeof(c4fScreen.message), "Checking Control4Free...");
pthread_t worker;
int workerStarted = pthread_create(&worker, NULL, c4fWorker, NULL) == 0;
if (!workerStarted || pad < 0) {
pthread_mutex_lock(&c4fMutex);
c4fScreen.locked = 1;
snprintf(c4fScreen.message, sizeof(c4fScreen.message), "Could not initialize launcher. Close with PS and retry.");
pthread_mutex_unlock(&c4fMutex);
}
sceSystemServiceHideSplashScreen();
uint32_t previous = 0;
int buffer = 0, done = 0, drawn = 0, shown[2] = { 0, 0 };
int64_t frame = 1;
C4fLauncherScreen drawnState;
memset(&drawnState, 0, sizeof(drawnState));
while (!done) {
OrbisPadData input;
memset(&input, 0, sizeof(input));
uint32_t buttons = pad >= 0 && scePadReadState(pad, &input) == 0 && input.connected ? input.buttons : 0;
uint32_t pressed = buttons & ~previous;
previous = buttons;
pthread_mutex_lock(&c4fMutex);
if (!c4fScreen.busy) {
if (pressed & ORBIS_PAD_BUTTON_CIRCLE) {
if (c4fScreen.confirmStop) c4fScreen.confirmStop = 0;
else done = 1;
} else if (pressed & ORBIS_PAD_BUTTON_CROSS) {
if (c4fScreen.confirmStop) {
c4fCommand = 2; c4fScreen.confirmStop = 0; c4fScreen.busy = 1;
snprintf(c4fScreen.message, sizeof(c4fScreen.message), "Stopping and releasing controllers...");
} else if (!c4fScreen.locked && c4fScreen.running != 1) {
c4fCommand = 1; c4fScreen.busy = 1;
snprintf(c4fScreen.message, sizeof(c4fScreen.message), "Starting Control4Free. Please wait...");
}
} else if ((pressed & ORBIS_PAD_BUTTON_SQUARE) && c4fScreen.running == 1) c4fScreen.confirmStop = 1;
}
C4fLauncherScreen snapshot = c4fScreen;
pthread_mutex_unlock(&c4fMutex);
/* Redraw only when something changed, then copy it to each buffer once. */
if (!drawn || memcmp(&snapshot, &drawnState, sizeof(snapshot))) {
c4fDrawLauncher(canvas, &snapshot);
drawnState = snapshot; drawn = 1; shown[0] = shown[1] = 0;
}
if (!shown[buffer]) { memcpy(c4fBuffers[buffer], canvas, frameBytes); shown[buffer] = 1; }
if (sceVideoOutSubmitFlip(c4fVideo, buffer, ORBIS_VIDEO_OUT_FLIP_VSYNC, frame) < 0) break;
for (;;) {
OrbisVideoOutFlipStatus status;
memset(&status, 0, sizeof(status));
if (sceVideoOutGetFlipStatus(c4fVideo, &status) < 0) { done = 1; break; }
if (status.flipArg >= frame) break;
OrbisKernelEvent event;
int count;
OrbisKernelUseconds timeout = 100000;
sceKernelWaitEqueue(c4fFlipQueue, &event, 1, &count, &timeout);
}
buffer ^= 1; frame++;
}
pthread_mutex_lock(&c4fMutex); c4fQuit = 1; pthread_mutex_unlock(&c4fMutex);
if (workerStarted) pthread_join(worker, NULL);
if (pad >= 0) scePadClose(pad);
sceNetCtlTerm();
c4fCloseVideo();
free(canvas);
/* Closing this app intentionally does not stop the external payload. */
sceSystemServiceLoadExec("exit", NULL);
return 0;
}
+560
View File
@@ -0,0 +1,560 @@
/* The launcher screen, drawn in the controller page's style (client/index.html):
* the same blue backdrop with light waves, tiles, panels and colours, at 1.5x
* the page's CSS sizes for a 1080p TV. */
#include "screen.h"
#include <math.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "draw.h"
#include "qrcodegen.h"
/* Page CSS pixels to TV pixels. */
#define C4F_S 1.5f
/* Layout, in TV pixels. */
#define C4F_LEFT 150.0f
#define C4F_RIGHT 1770.0f
#define C4F_ROW_TOP 336.0f
#define C4F_ROW_HEIGHT 444.0f
#define C4F_ROW_GAP 30.0f
#define C4F_QR_WIDTH 520.0f
#define C4F_MAIN_WIDTH (C4F_RIGHT - C4F_LEFT - C4F_ROW_GAP - C4F_QR_WIDTH)
#define C4F_QR_LEFT (C4F_LEFT + C4F_MAIN_WIDTH + C4F_ROW_GAP)
#define C4F_PANEL_TOP (C4F_ROW_TOP + C4F_ROW_HEIGHT + 28)
#define C4F_PANEL_HEIGHT 128.0f
/* The page's palette (:root). */
#define C4F_TEXT C4F_RGBA(244, 247, 255, 1)
#define C4F_MUTED C4F_RGBA(226, 236, 255, .72f)
#define C4F_FAINT C4F_RGBA(226, 236, 255, .46f)
#define C4F_LINE C4F_RGBA(255, 255, 255, .16f)
#define C4F_LINE_SOFT C4F_RGBA(255, 255, 255, .09f)
#define C4F_PANEL C4F_RGBA(3, 16, 44, .55f)
#define C4F_PANEL_STRONG C4F_RGBA(2, 11, 31, .86f)
#define C4F_TILE C4F_RGBA(12, 40, 98, .5f)
#define C4F_ACCENT C4F_RGBA(58, 140, 255, 1)
#define C4F_OK C4F_RGBA(69, 224, 138, 1)
#define C4F_WARN C4F_RGBA(255, 208, 77, 1)
#define C4F_BAD C4F_RGBA(255, 107, 125, 1)
#define C4F_CIRCLE_KEY C4F_RGBA(255, 102, 128, 1)
#define C4F_CROSS_KEY C4F_RGBA(134, 182, 255, 1)
#define C4F_SQUARE_KEY C4F_RGBA(255, 146, 220, 1)
#define C4F_WHITE C4F_RGBA(255, 255, 255, 1)
static uint32_t *c4fBackdrop;
static float c4fClamp(float v) { return v < 0 ? 0 : v > 1 ? 1 : v; }
/* ---- backdrop (.backdrop, .waves, .sparkles) ---- */
static float c4fNoise(int x, int y)
{
uint32_t h = (uint32_t)x * 374761393u + (uint32_t)y * 668265263u;
h = (h ^ (h >> 13)) * 1274126177u;
return (float)((h ^ (h >> 16)) & 0xffffu) / 65536.0f - 0.5f;
}
static void c4fMix(float rgb[3], float r, float g, float b, float a)
{
rgb[0] += (r - rgb[0]) * a;
rgb[1] += (g - rgb[1]) * a;
rgb[2] += (b - rgb[2]) * a;
}
static void c4fPaintGradient(C4fCanvas *c)
{
static const float stops[4][4] = {
{ 0.00f, 0x02, 0x0f, 0x2b }, { 0.45f, 0x06, 0x27, 0x66 },
{ 0.72f, 0x0b, 0x3f, 0x9c }, { 1.00f, 0x04, 0x19, 0x4a },
};
const float w = (float)c->w, h = (float)c->h;
/* linear-gradient(165deg): CSS angles point up at 0 and turn clockwise. */
const float dx = 0.258819f, dy = 0.965926f, length = w * dx + h * dy;
for (int y = 0; y < c->h; y++)
for (int x = 0; x < c->w; x++) {
float px = x + 0.5f, py = y + 0.5f, rgb[3];
float t = c4fClamp(((px - w / 2) * dx + (py - h / 2) * dy) / length + 0.5f);
int i = t > stops[2][0] ? 2 : t > stops[1][0] ? 1 : 0;
float f = (t - stops[i][0]) / (stops[i + 1][0] - stops[i][0]);
for (int k = 0; k < 3; k++) rgb[k] = stops[i][k + 1] + (stops[i + 1][k + 1] - stops[i][k + 1]) * f;
/* radial-gradient(80% 60% at 0% -10%, rgba(25, 90, 200, .55), transparent 60%) */
float ex = px / (0.8f * w), ey = (py + 0.1f * h) / (0.6f * h);
c4fMix(rgb, 25, 90, 200, 0.55f * c4fClamp(1 - sqrtf(ex * ex + ey * ey) / 0.6f));
/* radial-gradient(120% 70% at 75% 115%, rgba(70, 150, 255, .5), transparent 62%), on top */
ex = (px - 0.75f * w) / (1.2f * w);
ey = (py - 1.15f * h) / (0.7f * h);
c4fMix(rgb, 70, 150, 255, 0.5f * c4fClamp(1 - sqrtf(ex * ex + ey * ey) / 0.62f));
/* Dither, so the dark gradient does not band on a TV. */
float noise = c4fNoise(x, y);
uint32_t out = 0xff000000u;
for (int k = 0; k < 3; k++) {
int v = (int)floorf(rgb[k] + noise + 0.5f);
out |= (uint32_t)(v < 0 ? 0 : v > 255 ? 255 : v) << (16 - 8 * k);
}
c->px[(size_t)y * c->w + x] = out;
}
}
/* The page's wave paths (viewBox 0 0 3200 400): the start y, then four cubic
* segments (c1x c1y c2x c2y x y) with every "S" expanded. */
static const float c4fWaveA[25] = { 230, 200, 150, 400, 310, 800, 230, 1200, 150, 1400, 150, 1600, 230,
1800, 150, 2000, 310, 2400, 230, 2800, 150, 3000, 150, 3200, 230 };
static const float c4fWaveB[25] = { 260, 250, 200, 550, 330, 800, 260, 1050, 190, 1350, 190, 1600, 260,
1850, 200, 2150, 330, 2400, 260, 2650, 190, 2950, 190, 3200, 260 };
static const float c4fWaveC[25] = { 200, 300, 110, 500, 300, 800, 200, 1100, 100, 1300, 100, 1600, 200,
1900, 110, 2100, 300, 2400, 200, 2700, 100, 2900, 100, 3200, 200 };
static const float c4fWaveD[25] = { 215, 300, 140, 520, 290, 800, 215, 1080, 140, 1320, 130, 1600, 215,
1900, 140, 2120, 290, 2400, 215, 2680, 140, 2920, 130, 3200, 215 };
static const float c4fWaveE[25] = { 240, 280, 170, 540, 330, 800, 240, 1060, 150, 1330, 160, 1600, 240,
1880, 170, 2140, 330, 2400, 240, 2660, 150, 2930, 160, 3200, 240 };
/* Gradient stops: offset, r, g, b, alpha. */
typedef float C4fWaveStops[3][5];
static const C4fWaveStops c4fSlowStops = { { 0, 255, 255, 255, 0 }, { .5f, 188, 216, 255, .55f }, { 1, 255, 255, 255, 0 } };
static const C4fWaveStops c4fMainStops = { { 0, 127, 178, 255, .1f }, { .5f, 255, 255, 255, .5f }, { 1, 127, 178, 255, .1f } };
static C4fColor c4fWaveColor(const C4fWaveStops stops, float t, float opacity)
{
const float *a = stops[0], *b = stops[1];
t = c4fClamp(t);
if (t > stops[1][0]) { a = stops[1]; b = stops[2]; }
float f = (t - a[0]) / (b[0] - a[0]);
/* Interpolate premultiplied, as browsers do. */
float alpha = a[4] + (b[4] - a[4]) * f, rgb[3];
for (int k = 0; k < 3; k++) {
float v = a[k + 1] * a[4] + (b[k + 1] * b[4] - a[k + 1] * a[4]) * f;
rgb[k] = alpha > 0 ? v / alpha : a[k + 1];
}
return (C4fColor){ (uint8_t)rgb[0], (uint8_t)rgb[1], (uint8_t)rgb[2], (uint8_t)(alpha * opacity * 255 + 0.5f) };
}
static void c4fPaintWave(C4fCanvas *c, const float path[25], float offset, float top, float sx, float sy,
float stroke, float opacity, const C4fWaveStops stops)
{
enum { STEPS = 48, POINTS = 4 * STEPS + 1 };
float px[POINTS], py[POINTS], x0 = 0, y0 = path[0];
int n = 1;
px[0] = offset;
py[0] = top + y0 * sy;
for (int s = 0; s < 4; s++) {
const float *k = path + 1 + s * 6;
for (int i = 1; i <= STEPS; i++) {
float t = (float)i / STEPS, u = 1 - t;
float bx = u * u * u * x0 + 3 * u * u * t * k[0] + 3 * u * t * t * k[2] + t * t * t * k[4];
float by = u * u * u * y0 + 3 * u * u * t * k[1] + 3 * u * t * t * k[3] + t * t * t * k[5];
px[n] = offset + bx * sx;
py[n] = top + by * sy;
n++;
}
x0 = k[4];
y0 = k[5];
}
/* The paths only ever move right, so each column crosses a path once. */
float half = fmaxf(stroke * sy / 2, 0.35f);
int seg = 0;
for (int x = 0; x < c->w; x++) {
float fx = x + 0.5f;
if (fx < px[0] || fx > px[n - 1]) continue;
while (seg < n - 2 && px[seg + 1] < fx) seg++;
float span = px[seg + 1] - px[seg];
float slope = span > 0 ? (py[seg + 1] - py[seg]) / span : 0;
float yc = py[seg] + slope * (fx - px[seg]), norm = sqrtf(1 + slope * slope);
C4fColor color = c4fWaveColor(stops, (fx - offset) / (sx * 3200), opacity);
for (int y = (int)floorf(yc - (half + 1) * norm); y <= (int)ceilf(yc + (half + 1) * norm); y++) {
float cov = c4fClamp(half + 0.5f - fabsf(y + 0.5f - yc) / norm);
if (cov > 0) c4fBlend(c, x, y, color, cov);
}
}
}
static void c4fPaintBackdrop(C4fCanvas *c, float scale)
{
const float w = (float)c->w, h = (float)c->h, sx = 2 * w / 3200;
c4fPaintGradient(c);
/* A still from the drifting animation, the brightest part mid-screen.
* .waves.slow: bottom 14%, height 50%, opacity .7. */
float top = 0.36f * h, sy = 0.5f * h / 400, offset = -0.70f * w;
c4fPaintWave(c, c4fWaveA, offset, top, sx, sy, 1.2f, 0.7f, c4fSlowStops);
c4fPaintWave(c, c4fWaveB, offset, top, sx, sy, 2.0f, 0.7f * 0.6f, c4fSlowStops);
/* .waves: bottom 4%, height 62%. */
top = 0.34f * h;
sy = 0.62f * h / 400;
offset = -0.38f * w;
c4fPaintWave(c, c4fWaveC, offset, top, sx, sy, 1.5f, 1.0f, c4fMainStops);
c4fPaintWave(c, c4fWaveD, offset, top, sx, sy, 1.0f, 0.7f, c4fMainStops);
c4fPaintWave(c, c4fWaveE, offset, top, sx, sy, 3.0f, 0.35f, c4fMainStops);
/* .sparkles spans -10% to 110% of the height. */
static const float dots[8][4] = {
{ .12f, .22f, 1.5f, .70f }, { .28f, .64f, 1.0f, .55f }, { .47f, .38f, 1.5f, .60f }, { .63f, .76f, 1.0f, .50f },
{ .78f, .30f, 2.0f, .45f }, { .88f, .58f, 1.0f, .60f }, { .36f, .88f, 1.5f, .50f }, { .92f, .12f, 1.0f, .55f },
};
for (int i = 0; i < 8; i++) {
float cx = dots[i][0] * w, cy = -0.1f * h + dots[i][1] * 1.2f * h, r = fmaxf(dots[i][2] * scale, 1.5f);
for (int y = (int)floorf(cy - r); y <= (int)ceilf(cy + r); y++)
for (int x = (int)floorf(cx - r); x <= (int)ceilf(cx + r); x++) {
float d = hypotf(x + 0.5f - cx, y + 0.5f - cy);
if (d < r) c4fBlend(c, x, y, C4F_RGBA(255, 255, 255, dots[i][3]), 1 - d / r);
}
}
}
static int c4fClampI(int v, int lo, int hi) { return v < lo ? lo : v > hi ? hi : v; }
/* One box-blur pass along rows or columns of an RGB buffer, edges clamped. */
static void c4fBoxPass(const uint8_t *src, uint8_t *dst, int w, int h, int radius, int horizontal)
{
const int n = horizontal ? w : h, lines = horizontal ? h : w, divisor = 2 * radius + 1;
const int step = horizontal ? 3 : w * 3, lineStep = horizontal ? w * 3 : 3;
for (int line = 0; line < lines; line++) {
const uint8_t *s = src + (size_t)line * lineStep;
uint8_t *d = dst + (size_t)line * lineStep;
for (int k = 0; k < 3; k++) {
int sum = 0;
for (int i = -radius; i <= radius; i++) sum += s[c4fClampI(i, 0, n - 1) * step + k];
for (int i = 0; i < n; i++) {
d[i * step + k] = (uint8_t)((sum + divisor / 2) / divisor);
sum += s[c4fClampI(i + radius + 1, 0, n - 1) * step + k] - s[c4fClampI(i - radius, 0, n - 1) * step + k];
}
}
}
}
/* backdrop-filter: blur(sigma) under a tile. The backdrop never changes, so
* this is done once, clipped to the tile's rounded shape. */
static void c4fBlurUnder(C4fCanvas *c, float x, float y, float w, float h, float r, float sigma)
{
/* Three box passes of this radius approximate a Gaussian of that sigma. */
const int radius = (int)((sqrtf(4 * sigma * sigma + 1) - 1) / 2 + 0.5f), margin = 3 * radius;
const int x0 = c4fClampI((int)floorf(x) - margin, 0, c->w), y0 = c4fClampI((int)floorf(y) - margin, 0, c->h);
const int x1 = c4fClampI((int)ceilf(x + w) + margin, 0, c->w), y1 = c4fClampI((int)ceilf(y + h) + margin, 0, c->h);
const int bw = x1 - x0, bh = y1 - y0;
if (bw <= 0 || bh <= 0 || radius < 1) return;
uint8_t *buffer = malloc((size_t)bw * bh * 3), *scratch = malloc((size_t)bw * bh * 3);
if (buffer && scratch) {
for (int py = 0; py < bh; py++)
for (int px = 0; px < bw; px++) {
uint32_t v = c->px[(size_t)(y0 + py) * c->w + x0 + px];
uint8_t *o = buffer + ((size_t)py * bw + px) * 3;
o[0] = (uint8_t)(v >> 16); o[1] = (uint8_t)(v >> 8); o[2] = (uint8_t)v;
}
for (int pass = 0; pass < 3; pass++) {
c4fBoxPass(buffer, scratch, bw, bh, radius, 1);
c4fBoxPass(scratch, buffer, bw, bh, radius, 0);
}
const float cx = x + w / 2, cy = y + h / 2;
for (int py = 0; py < bh; py++)
for (int px = 0; px < bw; px++) {
int gx = x0 + px, gy = y0 + py;
float cov = c4fClamp(0.5f - c4fRoundBoxDistance(gx + 0.5f, gy + 0.5f, cx, cy, w / 2, h / 2, r));
if (cov <= 0) continue;
uint32_t *p = &c->px[(size_t)gy * c->w + gx], out = 0xff000000u;
const uint8_t *b = buffer + ((size_t)py * bw + px) * 3;
float noise = c4fNoise(gx, gy);
for (int k = 0; k < 3; k++) {
float old = (float)((*p >> (16 - 8 * k)) & 255u);
int v = (int)floorf(old + (b[k] - old) * cov + noise + 0.5f);
out |= (uint32_t)c4fClampI(v, 0, 255) << (16 - 8 * k);
}
*p = out;
}
}
free(buffer);
free(scratch);
}
/* ---- pieces of the page ---- */
static float c4fCapsuleDistance(float x, float y, float ax, float ay, float bx, float by, float r)
{
float vx = bx - ax, vy = by - ay, fx = x - ax, fy = y - ay;
float t = c4fClamp((fx * vx + fy * vy) / (vx * vx + vy * vy));
return hypotf(fx - t * vx, fy - t * vy) - r;
}
/* The page's controller mark (.app-mark, viewBox 0 0 32 32) from simple shapes. */
static float c4fMarkDistance(float x, float y)
{
float body = c4fRoundBoxDistance(x, y, 16, 15, 12.9f, 5, 5);
float grips = fminf(c4fCapsuleDistance(x, y, 6.4f, 16.5f, 7.6f, 21.2f, 3.3f),
c4fCapsuleDistance(x, y, 25.6f, 16.5f, 24.4f, 21.2f, 3.3f));
float dpad = fminf(c4fRoundBoxDistance(x, y, 11, 17, 1, 3, 0.3f), c4fRoundBoxDistance(x, y, 11, 17, 3, 1, 0.3f));
float buttons = fminf(hypotf(x - 21.5f, y - 15.5f), hypotf(x - 19, y - 19)) - 1.5f;
return fmaxf(fminf(body, grips), -fminf(dpad, buttons));
}
static void c4fMark(C4fCanvas *c, float cx, float cy, float unit, C4fColor color)
{
for (int y = (int)floorf(cy - 8 * unit); y <= (int)ceilf(cy + 8 * unit); y++)
for (int x = (int)floorf(cx - 14 * unit); x <= (int)ceilf(cx + 14 * unit); x++) {
float d = c4fMarkDistance(16 + (x + 0.5f - cx) / unit, 17.3f + (y + 0.5f - cy) / unit) * unit;
float cov = c4fClamp(0.5f - d);
if (cov > 0) c4fBlend(c, x, y, color, cov);
}
}
static void c4fTextCentered(C4fCanvas *c, int font, float size, float cx, float y, C4fColor color, const char *text)
{
c4fText(c, font, size, cx - c4fTextWidth(font, size, text) / 2, y, color, text);
}
/* .tile: --tile with a light sheen, a 1px line, a drop shadow and the colour bar. */
static void c4fTile(C4fCanvas *c, float x, float y, float w, float h, C4fColor bar)
{
const float r = 6 * C4F_S;
c4fShadowRoundRect(c, x, y, w, h, r, 0, 16 * C4F_S, 34 * C4F_S, -18 * C4F_S, C4F_RGBA(0, 0, 0, .9f));
c4fFillRoundRect(c, x, y, w, h, r, C4F_TILE);
c4fSheenRoundRect(c, x, y, w, h, r, 160, C4F_WHITE, .13f, .02f, .55f);
c4fInsetRoundRect(c, x, y, w, h, r, 1 * C4F_S, C4F_LINE);
bar.a = (uint8_t)(bar.a * .9f);
c4fBandRoundRect(c, x, y, w, h, r, 3 * C4F_S, bar);
}
/* .panel */
static void c4fPanel(C4fCanvas *c, float x, float y, float w, float h, C4fColor fill, C4fColor line)
{
c4fFillRoundRect(c, x, y, w, h, 6 * C4F_S, fill);
c4fInsetRoundRect(c, x, y, w, h, 6 * C4F_S, 1 * C4F_S, line);
}
/* .ring: the numbered circle on a controller tile. */
static void c4fRingBadge(C4fCanvas *c, float cx, float cy, C4fColor color, const char *label)
{
const float r = 26 * C4F_S, hx = cx, hy = cy - 0.3f * r, far = hypotf(r, 1.3f * r);
c4fGlowCircle(c, cx, cy, r, 18 * C4F_S, -4 * C4F_S, color);
/* radial-gradient(circle at 50% 35%, rgba(255, 255, 255, .18), rgba(255, 255, 255, .03)) */
for (int y = (int)floorf(cy - r - 1); y <= (int)ceilf(cy + r + 1); y++)
for (int x = (int)floorf(cx - r - 1); x <= (int)ceilf(cx + r + 1); x++) {
float inside = c4fClamp(0.5f - (hypotf(x + 0.5f - cx, y + 0.5f - cy) - r));
if (inside <= 0) continue;
float t = c4fClamp(hypotf(x + 0.5f - hx, y + 0.5f - hy) / far);
c4fBlend(c, x, y, C4F_RGBA(255, 255, 255, .18f + (.03f - .18f) * t), inside);
}
c4fRing(c, cx, cy, r, 3 * C4F_S, color);
c4fTextCentered(c, C4F_FONT_LIGHT, 24 * C4F_S, cx, cy + 24 * C4F_S * 0.36f, C4F_TEXT, label);
}
/* .dot, with the glow the page gives a live connection. */
static void c4fDot(C4fCanvas *c, float cx, float cy, C4fColor color, int glow)
{
if (glow) c4fGlowCircle(c, cx, cy, 4 * C4F_S, 10 * C4F_S, 0, color);
c4fFillCircle(c, cx, cy, 4 * C4F_S, color);
}
/* Button guide in the PS4's manner, with the page's face-button colours. */
enum { C4F_KEY_CROSS, C4F_KEY_CIRCLE, C4F_KEY_SQUARE };
typedef struct { int key; const char *label; } C4fHint;
#define C4F_HINT_RADIUS 22.0f
#define C4F_HINT_GAP 14.0f
#define C4F_HINT_SPACING 40.0f
#define C4F_HINT_SIZE 26.0f
static void c4fHints(C4fCanvas *c, float right, float cy, const C4fHint *hints, int count)
{
float x = right;
for (int i = 0; i < count; i++)
x -= (i ? C4F_HINT_SPACING : 0) + 2 * C4F_HINT_RADIUS + C4F_HINT_GAP +
c4fTextWidth(C4F_FONT_REGULAR, C4F_HINT_SIZE, hints[i].label);
for (int i = 0; i < count; i++) {
float cx = x + C4F_HINT_RADIUS;
c4fFillCircle(c, cx, cy, C4F_HINT_RADIUS, C4F_RGBA(255, 255, 255, .08f));
c4fRing(c, cx, cy, C4F_HINT_RADIUS, 1 * C4F_S, C4F_LINE_SOFT);
if (hints[i].key == C4F_KEY_CROSS) {
c4fSegment(c, cx - 8.5f, cy - 8.5f, cx + 8.5f, cy + 8.5f, 3.6f, C4F_CROSS_KEY);
c4fSegment(c, cx - 8.5f, cy + 8.5f, cx + 8.5f, cy - 8.5f, 3.6f, C4F_CROSS_KEY);
} else if (hints[i].key == C4F_KEY_CIRCLE) {
c4fRing(c, cx, cy, 10.5f, 3.6f, C4F_CIRCLE_KEY);
} else {
c4fSquareOutline(c, cx, cy, 8.5f, 3.4f, C4F_SQUARE_KEY);
}
x += 2 * C4F_HINT_RADIUS + C4F_HINT_GAP;
x += c4fText(c, C4F_FONT_REGULAR, C4F_HINT_SIZE, x, cy + C4F_HINT_SIZE * 0.36f, C4F_TEXT, hints[i].label);
x += C4F_HINT_SPACING;
}
}
static void c4fQrCard(C4fCanvas *c, float x, float y, float size, const char *address)
{
static char lastAddress[64];
static uint8_t qr[qrcodegen_BUFFER_LEN_FOR_VERSION(5)];
static int qrSize;
if (strcmp(lastAddress, address)) {
uint8_t temp[sizeof(qr)];
qrSize = qrcodegen_encodeText(address, temp, qr, qrcodegen_Ecc_MEDIUM, 1, 5, qrcodegen_Mask_AUTO, true)
? qrcodegen_getSize(qr) : 0;
snprintf(lastAddress, sizeof(lastAddress), "%s", address);
}
c4fFillRoundRect(c, x, y, size, size, 6 * C4F_S, C4F_WHITE);
if (!qrSize) return;
/* Whole pixels per module and a four-module quiet zone keep it scannable. */
int scale = (int)(size / (float)(qrSize + 8)), drawn = qrSize * scale;
int left = (int)(x + (size - drawn) / 2), top = (int)(y + (size - drawn) / 2);
for (int row = 0; row < qrSize; row++)
for (int col = 0; col < qrSize; col++)
if (qrcodegen_getModule(qr, col, row))
c4fFillRect(c, left + col * scale, top + row * scale, scale, scale, C4F_RGBA(4, 14, 40, 1));
}
/* ---- the screen ---- */
typedef struct {
const char *pill, *title, *subtitle, *meta;
C4fColor dot, bar;
int glow;
} C4fLook;
static C4fLook c4fLookFor(const C4fLauncherScreen *s, char *meta, size_t metaSize)
{
C4fLook look = { "Not responding", "Check Control4Free",
"Another program answers on port 4264, or nothing replied in time. Stop the old copy from "
"its phone page, or restart the PS4.",
"Not responding", C4F_BAD, C4F_BAD, 0 };
if (s->locked) {
look.pill = look.meta = "Restart needed";
look.title = "Restart the PS4";
look.subtitle = "Control4Free was sent but never answered. Restart the console before you try again, so "
"two copies never run at once.";
} else if (s->busy) {
look = (C4fLook){ "Working", "Please wait", s->message, "Checking", C4F_WARN, C4F_ACCENT, 0 };
} else if (s->running == 1) {
if (s->controllers == 1) snprintf(meta, metaSize, "1 controller connected");
else snprintf(meta, metaSize, "%d controllers connected", s->controllers);
look = (C4fLook){ "Running", "Control4Free is running",
"Open the address or scan the code on your phone or PC. Pick a controller there, then "
"choose its user on the PS4.",
meta, C4F_OK, C4F_ACCENT, 1 };
} else if (s->running == 0) {
look = (C4fLook){ "Stopped", "Start Control4Free",
"Press Cross with GoldHEN's PayLoader turned on, or start it from GoldHEN's Payloader "
"LaunchPad. Add it to AutoRun there and it starts with GoldHEN.",
"Not running", C4F_FAINT, C4F_FAINT, 0 };
}
return look;
}
void c4fDrawLauncher(uint32_t *pixels, const C4fLauncherScreen *s)
{
C4fCanvas c = { pixels, C4F_SCREEN_WIDTH, C4F_SCREEN_HEIGHT };
const float left = C4F_LEFT, right = C4F_RIGHT, top = C4F_ROW_TOP, height = C4F_ROW_HEIGHT;
const float qrWidth = C4F_QR_WIDTH, mainWidth = C4F_MAIN_WIDTH, pad = 16 * C4F_S;
char meta[48], count[8];
C4fLook look = c4fLookFor(s, meta, sizeof(meta));
int live = s->running == 1 && !s->locked;
if (c4fBackdrop) memcpy(pixels, c4fBackdrop, (size_t)c.w * (size_t)c.h * sizeof(uint32_t));
else memset(pixels, 0, (size_t)c.w * (size_t)c.h * sizeof(uint32_t));
/* .topbar: the app mark, the name and the state. */
c4fMark(&c, 64 + 13 * C4F_S, 54, 26 * C4F_S / 32, C4F_WHITE);
float x = 64 + 26 * C4F_S + 10 * C4F_S;
x += c4fText(&c, C4F_FONT_LIGHT, 17 * C4F_S, x, 63, C4F_TEXT, "Control4Free") + 14 * C4F_S;
c4fDot(&c, x + 4 * C4F_S, 55, look.dot, look.glow);
c4fText(&c, C4F_FONT_LIGHT, 13 * C4F_S, x + 16 * C4F_S, 62, C4F_MUTED, look.pill);
/* .screen-title and .screen-sub */
c4fText(&c, C4F_FONT_LIGHT, 40 * C4F_S, left, 196, C4F_TEXT, look.title);
c4fTextWrapped(&c, C4F_FONT_LIGHT, 26, left, 250, 1500, 39, 2, C4F_MUTED, look.subtitle);
/* Where to connect, as a controller tile. */
c4fTile(&c, left, top, mainWidth, height, look.bar);
if (s->running == 1) snprintf(count, sizeof(count), "%d", s->controllers);
else snprintf(count, sizeof(count), "-");
c4fRingBadge(&c, left + pad + 26 * C4F_S, top + pad + 26 * C4F_S + 4, look.bar, count);
float textX = left + pad + 52 * C4F_S + 22;
c4fText(&c, C4F_FONT_REGULAR, 30, textX, top + 66, C4F_TEXT, "Open on your phone or PC");
c4fText(&c, C4F_FONT_LIGHT, 23, textX, top + 102, C4F_MUTED, look.meta);
float fieldX = left + pad, fieldY = top + 150, fieldWidth = mainWidth - 2 * pad, fieldHeight = 88;
c4fFillRoundRect(&c, fieldX, fieldY, fieldWidth, fieldHeight, 4 * C4F_S, C4F_RGBA(0, 0, 0, .3f));
c4fInsetRoundRect(&c, fieldX, fieldY, fieldWidth, fieldHeight, 4 * C4F_S, 1 * C4F_S, C4F_LINE);
if (s->address[0])
c4fText(&c, C4F_FONT_REGULAR, 40, fieldX + 28, fieldY + fieldHeight / 2 + 14, live ? C4F_TEXT : C4F_MUTED, s->address);
else
c4fText(&c, C4F_FONT_LIGHT, 28, fieldX + 28, fieldY + fieldHeight / 2 + 10, C4F_MUTED,
"Connect the PS4 to your home network");
c4fTextWrapped(&c, C4F_FONT_LIGHT, 24, fieldX, fieldY + fieldHeight + 44, fieldWidth, 36, 2, C4F_MUTED,
"Use the same network as the PS4. Touch, a keyboard and controllers connected to the phone "
"or PC all work.");
/* The last result along the bottom of the tile. */
float lineY = top + height - 104;
c4fFillRoundRect(&c, fieldX, lineY, fieldWidth, 1 * C4F_S, 0, C4F_LINE_SOFT);
c4fDot(&c, fieldX + 4 * C4F_S, lineY + 44, look.dot, look.glow);
c4fTextWrapped(&c, C4F_FONT_LIGHT, 24, fieldX + 24, lineY + 52, fieldWidth - 24, 34, 2, C4F_TEXT,
s->busy ? "Working..." : s->message);
/* The QR code tile. */
float qrX = C4F_QR_LEFT, card = 340, cardX = qrX + (qrWidth - card) / 2, cardY = top + 34;
c4fTile(&c, qrX, top, qrWidth, height, look.bar);
if (live && s->address[0]) {
c4fQrCard(&c, cardX, cardY, card, s->address);
c4fTextCentered(&c, C4F_FONT_LIGHT, 24, qrX + qrWidth / 2, cardY + card + 50, C4F_MUTED,
"Scan with your phone's camera");
} else {
c4fFillRoundRect(&c, cardX, cardY, card, card, 6 * C4F_S, C4F_RGBA(0, 0, 0, .3f));
c4fInsetRoundRect(&c, cardX, cardY, card, card, 6 * C4F_S, 1 * C4F_S, C4F_LINE_SOFT);
c4fMark(&c, cardX + card / 2, cardY + card / 2, 5, C4F_FAINT);
c4fTextCentered(&c, C4F_FONT_LIGHT, 24, qrX + qrWidth / 2, cardY + card + 50, C4F_MUTED,
"The code appears once it runs");
}
/* .panel with the one thing worth knowing. */
float panelY = C4F_PANEL_TOP;
c4fPanel(&c, left, panelY, right - left, C4F_PANEL_HEIGHT, C4F_PANEL, C4F_LINE_SOFT);
c4fText(&c, C4F_FONT_LIGHT, 19 * C4F_S, left + 20 * C4F_S, panelY + 52, C4F_TEXT,
"Closing this app leaves your controllers running");
c4fTextWrapped(&c, C4F_FONT_LIGHT, 23, left + 20 * C4F_S, panelY + 94, right - left - 40 * C4F_S, 34, 1, C4F_MUTED,
"Open it again any time to see the address or to stop Control4Free. Turn off other "
"controller plugins in your games.");
/* .home-foot and the button guide. */
c4fText(&c, C4F_FONT_LIGHT, 20, left, 1030, C4F_FAINT, "Control4Free " C4F_LAUNCHER_VERSION);
if (!s->confirmStop && !s->busy) {
C4fHint hints[2];
int n = 0;
if (!s->locked) {
hints[n].key = live ? C4F_KEY_SQUARE : C4F_KEY_CROSS;
hints[n++].label = live ? "Stop" : "Start";
}
hints[n].key = C4F_KEY_CIRCLE;
hints[n++].label = "Close";
c4fHints(&c, right, 1022, hints, n);
}
if (s->confirmStop) {
const float dw = 980, dh = 330, dx = (c.w - dw) / 2, dy = (c.h - dh) / 2;
static const C4fHint hints[2] = { { C4F_KEY_CROSS, "Stop" }, { C4F_KEY_CIRCLE, "Cancel" } };
c4fFillRect(&c, 0, 0, c.w, c.h, C4F_RGBA(2, 11, 31, .6f));
c4fShadowRoundRect(&c, dx, dy, dw, dh, 6 * C4F_S, 0, 16 * C4F_S, 34 * C4F_S, -18 * C4F_S, C4F_RGBA(0, 0, 0, .9f));
/* A little more opaque than --panel-strong, so the tiles do not ghost through. */
c4fPanel(&c, dx, dy, dw, dh, C4F_RGBA(2, 11, 31, .94f), C4F_LINE);
c4fText(&c, C4F_FONT_LIGHT, 48, dx + 48, dy + 96, C4F_TEXT, "Stop Control4Free?");
c4fTextWrapped(&c, C4F_FONT_LIGHT, 26, dx + 48, dy + 152, dw - 96, 39, 2, C4F_MUTED,
"Every controller disconnects. You can start Control4Free again from this screen.");
c4fHints(&c, dx + dw - 48, dy + dh - 58, hints, 2);
}
}
void c4fDrawIcon(uint32_t *pixels, int size)
{
C4fCanvas c = { pixels, size, size };
c4fPaintBackdrop(&c, size / 360.0f);
c4fMark(&c, size / 2.0f, size * 0.40f, size * 0.60f / 26, C4F_WHITE);
c4fTextCentered(&c, C4F_FONT_LIGHT, size * 0.105f, size / 2.0f, size * 0.80f, C4F_TEXT, "Control4Free");
}
int c4fScreenInit(void)
{
if (c4fDrawInit()) return -1;
if (!c4fBackdrop) {
c4fBackdrop = malloc((size_t)C4F_SCREEN_WIDTH * C4F_SCREEN_HEIGHT * sizeof(uint32_t));
if (!c4fBackdrop) return -1;
C4fCanvas c = { c4fBackdrop, C4F_SCREEN_WIDTH, C4F_SCREEN_HEIGHT };
c4fPaintBackdrop(&c, C4F_S);
/* .tile has backdrop-filter: blur(6px), .panel blur(10px). */
c4fBlurUnder(&c, C4F_LEFT, C4F_ROW_TOP, C4F_MAIN_WIDTH, C4F_ROW_HEIGHT, 6 * C4F_S, 6 * C4F_S);
c4fBlurUnder(&c, C4F_QR_LEFT, C4F_ROW_TOP, C4F_QR_WIDTH, C4F_ROW_HEIGHT, 6 * C4F_S, 6 * C4F_S);
c4fBlurUnder(&c, C4F_LEFT, C4F_PANEL_TOP, C4F_RIGHT - C4F_LEFT, C4F_PANEL_HEIGHT, 6 * C4F_S, 10 * C4F_S);
}
return 0;
}
+24
View File
@@ -0,0 +1,24 @@
#ifndef C4F_LAUNCHER_SCREEN_H
#define C4F_LAUNCHER_SCREEN_H
#include <stdint.h>
#define C4F_LAUNCHER_VERSION "0.2.0"
#define C4F_SCREEN_WIDTH 1920
#define C4F_SCREEN_HEIGHT 1080
typedef struct {
int running, busy, controllers, confirmStop, locked;
char address[64], message[160];
} C4fLauncherScreen;
#ifdef __cplusplus
extern "C" {
#endif
/* Loads the fonts and paints the backdrop once. 0 on success. */
int c4fScreenInit(void);
/* Draws a whole frame. Blending reads pixels back, so pass cached memory,
* never the write-combined framebuffer. */
void c4fDrawLauncher(uint32_t *pixels, const C4fLauncherScreen *state);
/* The package icon (size x size), in the same style. */
void c4fDrawIcon(uint32_t *pixels, int size);
#ifdef __cplusplus
}
#endif
#endif
+202
View File
@@ -0,0 +1,202 @@
/* Local-only launcher transport. No controller creation or input injection. */
#include "service.h"
#include <arpa/inet.h>
#include <errno.h>
#include <fcntl.h>
#include <netinet/in.h>
#include <stdio.h>
#include <string.h>
#include <sys/select.h>
#include <sys/socket.h>
#include <time.h>
#include <unistd.h>
#define JSMN_STATIC
#define JSMN_STRICT
#include "jsmn.h"
#ifndef C4F_SERVICE_PORT
#define C4F_SERVICE_PORT 4264
#endif
#ifndef C4F_PAYLOADER_PORT
#define C4F_PAYLOADER_PORT 9090
#endif
uint64_t c4fLauncherTimeMs(void)
{
struct timespec t;
clock_gettime(CLOCK_MONOTONIC, &t);
return (uint64_t)t.tv_sec * 1000 + t.tv_nsec / 1000000;
}
static int c4fWaitSocket(int fd, int writeReady, uint64_t deadline)
{
for (;;) {
uint64_t now = c4fLauncherTimeMs();
if (now >= deadline) { errno = ETIMEDOUT; return -1; }
fd_set set; FD_ZERO(&set); FD_SET(fd, &set);
unsigned remaining = (unsigned)(deadline - now);
struct timeval tv = { remaining / 1000, (remaining % 1000) * 1000 };
int ret = select(fd + 1, writeReady ? NULL : &set, writeReady ? &set : NULL, NULL, &tv);
if (ret > 0) return 0;
if (ret < 0 && errno == EINTR) continue;
if (ret == 0) errno = ETIMEDOUT;
return -1;
}
}
static int c4fConnect(int port)
{
int fd = socket(AF_INET, SOCK_STREAM, 0);
if (fd < 0) return -1;
int savedError;
if (fd >= (int)FD_SETSIZE || fcntl(fd, F_SETFL, O_NONBLOCK) < 0) goto fail;
#ifdef SO_NOSIGPIPE
int one = 1;
setsockopt(fd, SOL_SOCKET, SO_NOSIGPIPE, &one, sizeof(one));
#endif
struct sockaddr_in addr;
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET; addr.sin_port = htons(port);
addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
if (connect(fd, (struct sockaddr *)&addr, sizeof(addr)) == 0) return fd;
if (errno != EINPROGRESS) goto fail;
if (c4fWaitSocket(fd, 1, c4fLauncherTimeMs() + 700)) goto fail;
int error = 0; socklen_t size = sizeof(error);
if (getsockopt(fd, SOL_SOCKET, SO_ERROR, &error, &size) < 0) goto fail;
if (error) { errno = error; goto fail; }
return fd;
fail:
savedError = errno; close(fd); errno = savedError; return -1;
}
static int c4fSendAll(int fd, const void *data, size_t size, uint64_t deadline)
{
const char *p = data;
while (size) {
if (c4fWaitSocket(fd, 1, deadline)) return -1;
int flags = 0;
#ifdef MSG_NOSIGNAL
flags = MSG_NOSIGNAL;
#endif
ssize_t n = send(fd, p, size, flags);
if (n < 0 && (errno == EINTR || errno == EAGAIN || errno == EWOULDBLOCK)) continue;
if (n <= 0) return -1;
p += n; size -= (size_t)n;
}
return 0;
}
static int c4fHttp(const char *method, const char *path, char *body, size_t capacity)
{
int fd = c4fConnect(C4F_SERVICE_PORT);
if (fd < 0) return errno == ECONNREFUSED ? 0 : -1;
char request[256], response[2048];
int n = snprintf(request, sizeof(request), "%s %s HTTP/1.1\r\nHost: 127.0.0.1:%d\r\nX-Control4Free-Launcher: 1\r\nContent-Length: 0\r\nConnection: close\r\n\r\n", method, path, C4F_SERVICE_PORT);
uint64_t deadline = c4fLauncherTimeMs() + 1500;
if (c4fSendAll(fd, request, (size_t)n, deadline)) { close(fd); return -1; }
size_t used = 0;
int complete = 0;
while (used < sizeof(response) - 1) {
if (c4fWaitSocket(fd, 0, deadline)) break;
ssize_t count = recv(fd, response + used, sizeof(response) - 1 - used, 0);
if (count < 0 && (errno == EINTR || errno == EAGAIN || errno == EWOULDBLOCK)) continue;
if (count == 0) { complete = 1; break; }
if (count < 0) break;
used += (size_t)count;
}
close(fd); response[used] = 0;
char *start = strstr(response, "\r\n\r\n");
if (!complete || strncmp(response, "HTTP/1.1 200 OK\r\n", 17) || !start) return -1;
start += 4;
size_t length = used - (size_t)(start - response);
if (!length || length >= capacity || memchr(start, 0, length)) return -1;
memcpy(body, start, length); body[length] = 0;
return 1;
}
static int c4fEquals(const char *json, const jsmntok_t *token, const char *value)
{
return token->end - token->start == (int)strlen(value) && !memcmp(json + token->start, value, strlen(value));
}
int c4fLauncherProbe(C4fServiceStatus *status)
{
char body[512]; jsmntok_t tokens[24]; jsmn_parser parser;
memset(status, 0, sizeof(*status));
int result = c4fHttp("GET", "/api/status", body, sizeof(body));
if (result <= 0) return result;
jsmn_init(&parser);
int count = jsmn_parse(&parser, body, strlen(body), tokens, 24);
if (count < 1 || tokens[0].type != JSMN_OBJECT || tokens[0].end != (int)strlen(body)) return -1;
int app = 0, api = 0, controllers = 0, stopping = 0;
for (int i = 1; i + 1 < count; i += 2) {
jsmntok_t *key = &tokens[i], *value = &tokens[i+1];
if (key->type != JSMN_STRING || (value->type != JSMN_STRING && value->type != JSMN_PRIMITIVE)) return -1;
if (c4fEquals(body, key, "application")) app = value->type == JSMN_STRING && c4fEquals(body, value, "Control4Free");
else if (c4fEquals(body, key, "api")) api = value->type == JSMN_PRIMITIVE && c4fEquals(body, value, "1");
else if (c4fEquals(body, key, "controllers")) {
if (value->type != JSMN_PRIMITIVE || value->end - value->start != 1 || body[value->start] < '0' || body[value->start] > '4') return -1;
status->controllers = body[value->start] - '0'; controllers = 1;
} else if (c4fEquals(body, key, "stopping")) {
if (value->type != JSMN_PRIMITIVE || (!c4fEquals(body, value, "true") && !c4fEquals(body, value, "false"))) return -1;
status->stopping = c4fEquals(body, value, "true"); stopping = 1;
} else if (c4fEquals(body, key, "version")) {
int size = value->end - value->start;
if (value->type != JSMN_STRING || size >= (int)sizeof(status->version)) return -1;
memcpy(status->version, body + value->start, (size_t)size);
}
}
return app && api && controllers && stopping ? 1 : -1;
}
int c4fLauncherStart(const char *payload, char *message, size_t size)
{
C4fServiceStatus status;
int probe = c4fLauncherProbe(&status);
if (probe == 1) { snprintf(message, size, "Already running. Scan the code to connect."); return 0; }
if (probe != 0) { snprintf(message, size, "Existing service detected. Stop it from the phone first."); return -1; }
FILE *file = fopen(payload, "rb");
if (!file) { snprintf(message, size, "Payload missing. Reinstall the Control4Free package."); return -1; }
unsigned char buffer[16384];
size_t first = fread(buffer, 1, sizeof(buffer), file);
if (first < 64 || memcmp(buffer, "\177ELF", 4) || buffer[4] != 2 || buffer[5] != 1) {
fclose(file); snprintf(message, size, "Invalid bundled payload. Reinstall this package."); return -1;
}
int fd = c4fConnect(C4F_PAYLOADER_PORT);
if (fd < 0) { fclose(file); snprintf(message, size, "Turn on GoldHEN's PayLoader, then press Cross again."); return -1; }
uint64_t deadline = c4fLauncherTimeMs() + 10000;
int failed = c4fSendAll(fd, buffer, first, deadline);
while (!failed) {
size_t count = fread(buffer, 1, sizeof(buffer), file);
if (!count) { failed = ferror(file); break; }
failed = c4fSendAll(fd, buffer, count, deadline);
}
fclose(file); shutdown(fd, SHUT_WR); close(fd);
if (failed) { snprintf(message, size, "Transfer interrupted. Restart the PS4 before retrying."); return -2; }
deadline = c4fLauncherTimeMs() + 20000;
while (c4fLauncherTimeMs() < deadline) {
if (c4fLauncherProbe(&status) == 1 && !status.stopping) {
snprintf(message, size, "Ready. Scan the code or open the address on your phone."); return 0;
}
usleep(250000);
}
snprintf(message, size, "Sent, but no reply. Restart the PS4 before retrying.");
return -2; /* Caller locks further sends until restart, avoiding duplicates. */
}
int c4fLauncherStop(char *message, size_t size)
{
C4fServiceStatus status;
int probe = c4fLauncherProbe(&status);
if (probe == 0) { snprintf(message, size, "Stopped. Press Cross to start it again."); return 0; }
if (probe != 1) { snprintf(message, size, "Older service detected. Stop it from the phone."); return -1; }
char reply[256];
int ret = c4fHttp("POST", "/api/stop", reply, sizeof(reply));
if (ret != 1) { snprintf(message, size, "Stop was not confirmed. Check the phone page."); return -1; }
uint64_t deadline = c4fLauncherTimeMs() + 5000;
while (c4fLauncherTimeMs() < deadline) {
if (c4fLauncherProbe(&status) == 0) { snprintf(message, size, "Stopped. Press Cross to start it again."); return 0; }
usleep(100000);
}
snprintf(message, size, "Still stopping. Wait before trying again."); return -1;
}
+20
View File
@@ -0,0 +1,20 @@
#ifndef C4F_LAUNCHER_SERVICE_H
#define C4F_LAUNCHER_SERVICE_H
#include <stddef.h>
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
enum { C4F_SERVICE_UNKNOWN = -1, C4F_SERVICE_DOWN = 0, C4F_SERVICE_RUNNING = 1 };
typedef struct { int controllers, stopping; char version[24]; } C4fServiceStatus;
uint64_t c4fLauncherTimeMs(void);
/* DOWN is returned only for a refused local connection. Ambiguous responses
* must not authorize starting another copy in the shared payload host. */
int c4fLauncherProbe(C4fServiceStatus *status);
int c4fLauncherStart(const char *payload, char *message, size_t size);
int c4fLauncherStop(char *message, size_t size);
#ifdef __cplusplus
}
#endif
#endif
+3
View File
@@ -0,0 +1,3 @@
/* stb_truetype's implementation, built on its own with warnings off (vendor code). */
#define STB_TRUETYPE_IMPLEMENTATION
#include "stb_truetype.h"
+15
View File
@@ -0,0 +1,15 @@
<# Build the payload and installable PS4 launcher locally. Requires Docker. #>
$ErrorActionPreference = 'Stop'
$projectRoot = (Resolve-Path (Join-Path $PSScriptRoot '..')).Path
Push-Location $projectRoot
try {
docker build -t control4free-build -f docker/Dockerfile docker
if ($LASTEXITCODE -ne 0) { throw 'Payload toolchain build failed.' }
docker build -t control4free-launcher-build -f docker/Dockerfile.launcher docker
if ($LASTEXITCODE -ne 0) { throw 'Launcher toolchain build failed.' }
docker run --rm --network none -v "${projectRoot}:/src" -w /src control4free-launcher-build bash -lc 'make && make -C launcher'
if ($LASTEXITCODE -ne 0) { throw 'Launcher package build failed.' }
Get-Item -LiteralPath (Join-Path $projectRoot 'build/Control4Free-0.2.0.pkg') | Select-Object FullName,Length
} finally {
Pop-Location
}
+15
View File
@@ -0,0 +1,15 @@
"""Embed binary files in a C source: embed_file.py out.c symbol=path [symbol=path ...]"""
import sys
from pathlib import Path
out, pairs = Path(sys.argv[1]), [arg.split('=', 1) for arg in sys.argv[2:]]
lines = ['/* Generated by tools/embed_file.py; do not edit. */', '#include <stddef.h>']
for symbol, path in pairs:
data = Path(path).read_bytes()
lines.append(f'/* {Path(path).name} */')
lines.append(f'__attribute__((aligned(4))) const unsigned char {symbol}[] = {{')
for i in range(0, len(data), 24):
lines.append(''.join(f'{b},' for b in data[i:i + 24]))
lines.append('};')
lines.append(f'const size_t {symbol}Size = {len(data)};')
out.write_text('\n'.join(lines) + '\n')
+56
View File
@@ -0,0 +1,56 @@
/* Renders launcher artwork on the build machine with the launcher's own drawing
* code: the package icon, and screen states for review. Writes raw RGBA.
* launcher_art icon <size> <out.rgba>
* launcher_art screen <running|stopped|busy|confirm|locked|unknown> <out.rgba> */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "screen.h"
static int c4fWrite(const char *path, const uint32_t *pixels, size_t count)
{
FILE *out = fopen(path, "wb");
if (!out) return 1;
for (size_t i = 0; i < count; i++) {
unsigned char rgba[4] = { (unsigned char)(pixels[i] >> 16), (unsigned char)(pixels[i] >> 8),
(unsigned char)pixels[i], (unsigned char)(pixels[i] >> 24) };
fwrite(rgba, 1, 4, out);
}
return fclose(out) != 0;
}
int main(int argc, char **argv)
{
if (argc != 4 || c4fScreenInit()) return 2;
if (!strcmp(argv[1], "icon")) {
int size = atoi(argv[2]);
uint32_t *pixels = size > 0 && size <= 4096 ? calloc((size_t)size * size, sizeof(uint32_t)) : NULL;
if (!pixels) return 2;
c4fDrawIcon(pixels, size);
return c4fWrite(argv[3], pixels, (size_t)size * size);
}
if (strcmp(argv[1], "screen")) return 2;
C4fLauncherScreen s;
memset(&s, 0, sizeof(s));
snprintf(s.address, sizeof(s.address), "http://192.168.1.20:4264");
const char *state = argv[2], *message = "";
if (!strcmp(state, "running") || !strcmp(state, "confirm")) {
s.running = 1; s.controllers = 2; s.confirmStop = !strcmp(state, "confirm");
message = "Ready. Scan the code or open the address on your phone.";
} else if (!strcmp(state, "stopped")) {
message = "Not running. Press Cross to start it.";
} else if (!strcmp(state, "busy")) {
s.running = -1; s.busy = 1; message = "Starting Control4Free. Please wait...";
} else if (!strcmp(state, "locked")) {
s.running = -1; s.locked = 1; message = "Sent, but no reply. Restart the PS4 before retrying.";
} else if (!strcmp(state, "unknown")) {
s.running = -1; message = "Existing service detected. Stop it from the phone first.";
} else {
return 2;
}
snprintf(s.message, sizeof(s.message), "%s", message);
uint32_t *pixels = calloc((size_t)C4F_SCREEN_WIDTH * C4F_SCREEN_HEIGHT, sizeof(uint32_t));
if (!pixels) return 2;
c4fDrawLauncher(pixels, &s);
return c4fWrite(argv[3], pixels, (size_t)C4F_SCREEN_WIDTH * C4F_SCREEN_HEIGHT);
}
+132
View File
@@ -0,0 +1,132 @@
"""Build the native launcher package using OpenOrbis/LibOrbisPkg only."""
import argparse
import binascii
import shutil
import struct
import subprocess
import zlib
import xml.etree.ElementTree as ET
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
OUT = ROOT / 'build/launcher'
STAGE = OUT / 'pkg'
ART = OUT / 'launcher-art'
FONTS = OUT / 'fonts.c'
TITLE_ID = 'CFRE00001' # 4 letters + 5 digits, the usual title ID format
CONTENT_ID = f'IV0000-{TITLE_ID}_00-CONTROL4FREE0000'
assert len(CONTENT_ID) == 36
def embed_fonts():
sources = [ROOT / 'vendor/roboto/Roboto-Light.ttf', ROOT / 'vendor/roboto/Roboto-Regular.ttf']
if FONTS.exists() and all(FONTS.stat().st_mtime >= s.stat().st_mtime for s in sources):
return
OUT.mkdir(parents=True, exist_ok=True)
subprocess.run(['python3', str(ROOT / 'tools/embed_file.py'), str(FONTS),
f'c4fFontLight={sources[0]}', f'c4fFontRegular={sources[1]}'], check=True)
def build_art_tool():
"""Host build of the launcher's drawing code, for the icon and previews."""
embed_fonts()
flags = ['clang-18', '-std=gnu11', '-O2', '-D_DEFAULT_SOURCE', f'-I{ROOT}/launcher',
f'-I{ROOT}/vendor/stb', f'-I{ROOT}/vendor/qrcodegen']
stb = OUT / 'stb_truetype-host.o'
subprocess.run([*flags, '-w', '-c', str(ROOT / 'launcher/stb_truetype.c'), '-o', str(stb)], check=True)
subprocess.run([*flags, '-Wall', '-Wextra', '-Werror', str(ROOT / 'tools/launcher_art.c'),
str(ROOT / 'launcher/screen.c'), str(ROOT / 'launcher/draw.c'),
str(ROOT / 'vendor/qrcodegen/qrcodegen.c'), str(FONTS), str(stb), '-lm', '-o', str(ART)],
check=True)
def write_png(path, width, height, rgba, alpha=False):
def chunk(kind, data):
return struct.pack('>I', len(data)) + kind + data + struct.pack('>I', binascii.crc32(kind + data) & 0xffffffff)
pixels = rgba if alpha else bytes(b for i, b in enumerate(rgba) if i % 4 != 3)
stride = width * (4 if alpha else 3)
scanlines = b''.join(b'\0' + pixels[y * stride:(y + 1) * stride] for y in range(height))
header = struct.pack('>IIBBBBB', width, height, 8, 6 if alpha else 2, 0, 0, 0)
Path(path).write_bytes(b'\x89PNG\r\n\x1a\n' + chunk(b'IHDR', header) +
chunk(b'IDAT', zlib.compress(scanlines, 9)) + chunk(b'IEND', b''))
def render(args, path, width, height):
"""Draw with the art tool (build_art_tool first) and save a PNG."""
raw = OUT / 'art.rgba'
subprocess.run([str(ART), *args, str(raw)], check=True)
write_png(path, width, height, raw.read_bytes())
raw.unlink()
def main():
parser = argparse.ArgumentParser()
parser.add_argument('--sdk', required=True, type=Path)
sdk = parser.parse_args().sdk
binary = sdk / 'bin/linux'
# Start clean, so nothing left over from an older build gets packaged.
shutil.rmtree(STAGE, ignore_errors=True)
for directory in ('sce_sys/about', 'sce_module', 'assets', 'assets/licenses'):
(STAGE / directory).mkdir(parents=True, exist_ok=True)
run = lambda *args: subprocess.run([str(arg) for arg in args], cwd=STAGE, check=True)
run(binary / 'create-fself', '-in=' + str(OUT / 'launcher.elf'), '-out=' + str(OUT / 'launcher.oelf'),
'--eboot', 'eboot.bin', '--paid', '0x3800000000000011')
shutil.copyfile(ROOT / 'build/control4free.elf', STAGE / 'assets/control4free.elf')
shutil.copyfile(sdk / 'samples/graphics/sce_sys/about/right.sprx', STAGE / 'sce_sys/about/right.sprx')
# OpenOrbis's stub modules. Its CHANGELOG (v0.5) says homebrew packages
# should carry them in sce_module/, and every sample does.
for name in ('libc.prx', 'libSceFios2.prx'):
shutil.copyfile(sdk / 'samples/graphics/sce_module' / name, STAGE / 'sce_module' / name)
licenses = STAGE / 'assets/licenses'
shutil.copyfile(ROOT / 'LICENSE', licenses / 'Control4Free-GPL-3.0.txt')
shutil.copyfile(ROOT / 'THIRD_PARTY.md', licenses / 'THIRD_PARTY.txt')
shutil.copyfile(ROOT / 'vendor/jsmn/LICENSE', licenses / 'jsmn-MIT.txt')
shutil.copyfile(ROOT / 'vendor/roboto/LICENSE', licenses / 'Roboto-Apache-2.0.txt')
qrcodegen = (ROOT / 'vendor/qrcodegen/qrcodegen.h').read_text()
(licenses / 'qrcodegen-MIT.txt').write_text(qrcodegen.split('*/', 1)[0] + '*/\n')
stb = (ROOT / 'vendor/stb/stb_truetype.h').read_text()
(licenses / 'stb_truetype.txt').write_text(stb[stb.index('This software is available under 2 licenses'):])
# The home-screen icon comes from the launcher's own drawing code.
build_art_tool()
render(['icon', '512'], STAGE / 'sce_sys/icon0.png', 512, 512)
pkgtool = binary / 'PkgTool.Core'
sfo = 'sce_sys/param.sfo'
run(pkgtool, 'sfo_new', sfo)
entries = {
'APP_TYPE': ('Integer', 4, '1'), 'APP_VER': ('Utf8', 8, '00.20'),
'ATTRIBUTE': ('Integer', 4, '0'), 'CATEGORY': ('Utf8', 4, 'gd'),
'CONTENT_ID': ('Utf8', 48, CONTENT_ID), 'DOWNLOAD_DATA_SIZE': ('Integer', 4, '0'),
'SYSTEM_VER': ('Integer', 4, '0'), 'TITLE': ('Utf8', 128, 'Control4Free'),
'TITLE_ID': ('Utf8', 12, TITLE_ID), 'VERSION': ('Utf8', 8, '00.20'),
}
for key, (kind, size, value) in entries.items():
run(pkgtool, 'sfo_setentry', sfo, key, '--type', kind, '--maxsize', size, '--value', value)
files = ['eboot.bin', 'sce_sys/param.sfo', 'sce_sys/icon0.png', 'sce_sys/about/right.sprx',
'sce_module/libc.prx', 'sce_module/libSceFios2.prx', 'assets/control4free.elf']
files += sorted(str(p.relative_to(STAGE)) for p in (STAGE / 'assets/licenses').iterdir())
project = STAGE / 'launcher.gp4'
run(binary / 'create-gp4', '-out', project, '--content-id=' + CONTENT_ID, '--files', ' '.join(files))
# create-gp4 ships a sample directory tree. Describe the actual package,
# including nested license files, and keep source paths relative to it.
tree = ET.parse(project)
rootdir = tree.getroot().find('rootdir')
rootdir.clear()
for filename in files:
parent = rootdir
for part in Path(filename).parts[:-1]:
child = next((p for p in parent if p.get('targ_name') == part), None)
if child is None:
child = ET.SubElement(parent, 'dir', targ_name=part)
parent = child
ET.indent(tree)
tree.write(project, encoding='utf-8', xml_declaration=True)
shutil.copyfile(project, OUT / 'launcher.gp4')
run(pkgtool, 'pkg_build', project, OUT)
generated = OUT / (CONTENT_ID + '.pkg')
target = ROOT / 'build/Control4Free-0.2.0.pkg'
shutil.copyfile(generated, target)
print(f'Built {target} ({target.stat().st_size:,} bytes)', flush=True)
if __name__ == '__main__':
main()
+1022
View File
File diff suppressed because it is too large. Load diff
+385
View File
@@ -0,0 +1,385 @@
/*
* QR Code generator library (C)
*
* Copyright (c) Project Nayuki. (MIT License)
* https://www.nayuki.io/page/qr-code-generator-library
*
* 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.
*/
#pragma once
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
/*
* This library creates QR Code symbols, which is a type of two-dimension barcode.
* Invented by Denso Wave and described in the ISO/IEC 18004 standard.
* A QR Code structure is an immutable square grid of dark and light cells.
* The library provides functions to create a QR Code from text or binary data.
* The library covers the QR Code Model 2 specification, supporting all versions (sizes)
* from 1 to 40, all 4 error correction levels, and 4 character encoding modes.
*
* Ways to create a QR Code object:
* - High level: Take the payload data and call qrcodegen_encodeText() or qrcodegen_encodeBinary().
* - Low level: Custom-make the list of segments and call
* qrcodegen_encodeSegments() or qrcodegen_encodeSegmentsAdvanced().
* (Note that all ways require supplying the desired error correction level and various byte buffers.)
*/
/*---- Enum and struct types----*/
/*
* The error correction level in a QR Code symbol.
*/
enum qrcodegen_Ecc {
// Must be declared in ascending order of error protection
// so that an internal qrcodegen function works properly
qrcodegen_Ecc_LOW = 0 , // The QR Code can tolerate about 7% erroneous codewords
qrcodegen_Ecc_MEDIUM , // The QR Code can tolerate about 15% erroneous codewords
qrcodegen_Ecc_QUARTILE, // The QR Code can tolerate about 25% erroneous codewords
qrcodegen_Ecc_HIGH , // The QR Code can tolerate about 30% erroneous codewords
};
/*
* The mask pattern used in a QR Code symbol.
*/
enum qrcodegen_Mask {
// A special value to tell the QR Code encoder to
// automatically select an appropriate mask pattern
qrcodegen_Mask_AUTO = -1,
// The eight actual mask patterns
qrcodegen_Mask_0 = 0,
qrcodegen_Mask_1,
qrcodegen_Mask_2,
qrcodegen_Mask_3,
qrcodegen_Mask_4,
qrcodegen_Mask_5,
qrcodegen_Mask_6,
qrcodegen_Mask_7,
};
/*
* Describes how a segment's data bits are interpreted.
*/
enum qrcodegen_Mode {
qrcodegen_Mode_NUMERIC = 0x1,
qrcodegen_Mode_ALPHANUMERIC = 0x2,
qrcodegen_Mode_BYTE = 0x4,
qrcodegen_Mode_KANJI = 0x8,
qrcodegen_Mode_ECI = 0x7,
};
/*
* A segment of character/binary/control data in a QR Code symbol.
* The mid-level way to create a segment is to take the payload data
* and call a factory function such as qrcodegen_makeNumeric().
* The low-level way to create a segment is to custom-make the bit buffer
* and initialize a qrcodegen_Segment struct with appropriate values.
* Even in the most favorable conditions, a QR Code can only hold 7089 characters of data.
* Any segment longer than this is meaningless for the purpose of generating QR Codes.
* Moreover, the maximum allowed bit length is 32767 because
* the largest QR Code (version 40) has 31329 modules.
*/
struct qrcodegen_Segment {
// The mode indicator of this segment.
enum qrcodegen_Mode mode;
// The length of this segment's unencoded data. Measured in characters for
// numeric/alphanumeric/kanji mode, bytes for byte mode, and 0 for ECI mode.
// Always zero or positive. Not the same as the data's bit length.
int numChars;
// The data bits of this segment, packed in bitwise big endian.
// Can be null if the bit length is zero.
uint8_t *data;
// The number of valid data bits used in the buffer. Requires
// 0 <= bitLength <= 32767, and bitLength <= (capacity of data array) * 8.
// The character count (numChars) must agree with the mode and the bit buffer length.
int bitLength;
};
/*---- Macro constants and functions ----*/
#define qrcodegen_VERSION_MIN 1 // The minimum version number supported in the QR Code Model 2 standard
#define qrcodegen_VERSION_MAX 40 // The maximum version number supported in the QR Code Model 2 standard
// Calculates the number of bytes needed to store any QR Code up to and including the given version number,
// as a compile-time constant. For example, 'uint8_t buffer[qrcodegen_BUFFER_LEN_FOR_VERSION(25)];'
// can store any single QR Code from version 1 to 25 (inclusive). The result fits in an int (or int16).
// Requires qrcodegen_VERSION_MIN <= n <= qrcodegen_VERSION_MAX.
#define qrcodegen_BUFFER_LEN_FOR_VERSION(n) ((((n) * 4 + 17) * ((n) * 4 + 17) + 7) / 8 + 1)
// The worst-case number of bytes needed to store one QR Code, up to and including
// version 40. This value equals 3918, which is just under 4 kilobytes.
// Use this more convenient value to avoid calculating tighter memory bounds for buffers.
#define qrcodegen_BUFFER_LEN_MAX qrcodegen_BUFFER_LEN_FOR_VERSION(qrcodegen_VERSION_MAX)
/*---- Functions (high level) to generate QR Codes ----*/
/*
* Encodes the given text string to a QR Code, returning true if successful.
* If the data is too long to fit in any version in the given range
* at the given ECC level, then false is returned.
*
* The input text must be encoded in UTF-8 and contain no NULs.
* Requires 1 <= minVersion <= maxVersion <= 40.
*
* The smallest possible QR Code version within the given range is automatically
* chosen for the output. Iff boostEcl is true, then the ECC level of the result
* may be higher than the ecl argument if it can be done without increasing the
* version. The mask is either between qrcodegen_Mask_0 to 7 to force that mask, or
* qrcodegen_Mask_AUTO to automatically choose an appropriate mask (which may be slow).
*
* About the arrays, letting len = qrcodegen_BUFFER_LEN_FOR_VERSION(maxVersion):
* - Before calling the function:
* - The array ranges tempBuffer[0 : len] and qrcode[0 : len] must allow
* reading and writing; hence each array must have a length of at least len.
* - The two ranges must not overlap (aliasing).
* - The initial state of both ranges can be uninitialized
* because the function always writes before reading.
* - After the function returns:
* - Both ranges have no guarantee on which elements are initialized and what values are stored.
* - tempBuffer contains no useful data and should be treated as entirely uninitialized.
* - If successful, qrcode can be passed into qrcodegen_getSize() and qrcodegen_getModule().
*
* If successful, the resulting QR Code may use numeric,
* alphanumeric, or byte mode to encode the text.
*
* In the most optimistic case, a QR Code at version 40 with low ECC
* can hold any UTF-8 string up to 2953 bytes, or any alphanumeric string
* up to 4296 characters, or any digit string up to 7089 characters.
* These numbers represent the hard upper limit of the QR Code standard.
*
* Please consult the QR Code specification for information on
* data capacities per version, ECC level, and text encoding mode.
*/
bool qrcodegen_encodeText(const char *text, uint8_t tempBuffer[], uint8_t qrcode[],
enum qrcodegen_Ecc ecl, int minVersion, int maxVersion, enum qrcodegen_Mask mask, bool boostEcl);
/*
* Encodes the given binary data to a QR Code, returning true if successful.
* If the data is too long to fit in any version in the given range
* at the given ECC level, then false is returned.
*
* Requires 1 <= minVersion <= maxVersion <= 40.
*
* The smallest possible QR Code version within the given range is automatically
* chosen for the output. Iff boostEcl is true, then the ECC level of the result
* may be higher than the ecl argument if it can be done without increasing the
* version. The mask is either between qrcodegen_Mask_0 to 7 to force that mask, or
* qrcodegen_Mask_AUTO to automatically choose an appropriate mask (which may be slow).
*
* About the arrays, letting len = qrcodegen_BUFFER_LEN_FOR_VERSION(maxVersion):
* - Before calling the function:
* - The array ranges dataAndTemp[0 : len] and qrcode[0 : len] must allow
* reading and writing; hence each array must have a length of at least len.
* - The two ranges must not overlap (aliasing).
* - The input array range dataAndTemp[0 : dataLen] should normally be
* valid UTF-8 text, but is not required by the QR Code standard.
* - The initial state of dataAndTemp[dataLen : len] and qrcode[0 : len]
* can be uninitialized because the function always writes before reading.
* - After the function returns:
* - Both ranges have no guarantee on which elements are initialized and what values are stored.
* - dataAndTemp contains no useful data and should be treated as entirely uninitialized.
* - If successful, qrcode can be passed into qrcodegen_getSize() and qrcodegen_getModule().
*
* If successful, the resulting QR Code will use byte mode to encode the data.
*
* In the most optimistic case, a QR Code at version 40 with low ECC can hold any byte
* sequence up to length 2953. This is the hard upper limit of the QR Code standard.
*
* Please consult the QR Code specification for information on
* data capacities per version, ECC level, and text encoding mode.
*/
bool qrcodegen_encodeBinary(uint8_t dataAndTemp[], size_t dataLen, uint8_t qrcode[],
enum qrcodegen_Ecc ecl, int minVersion, int maxVersion, enum qrcodegen_Mask mask, bool boostEcl);
/*---- Functions (low level) to generate QR Codes ----*/
/*
* Encodes the given segments to a QR Code, returning true if successful.
* If the data is too long to fit in any version at the given ECC level,
* then false is returned.
*
* The smallest possible QR Code version is automatically chosen for
* the output. The ECC level of the result may be higher than the
* ecl argument if it can be done without increasing the version.
*
* About the byte arrays, letting len = qrcodegen_BUFFER_LEN_FOR_VERSION(qrcodegen_VERSION_MAX):
* - Before calling the function:
* - The array ranges tempBuffer[0 : len] and qrcode[0 : len] must allow
* reading and writing; hence each array must have a length of at least len.
* - The two ranges must not overlap (aliasing).
* - The initial state of both ranges can be uninitialized
* because the function always writes before reading.
* - The input array segs can contain segments whose data buffers overlap with tempBuffer.
* - After the function returns:
* - Both ranges have no guarantee on which elements are initialized and what values are stored.
* - tempBuffer contains no useful data and should be treated as entirely uninitialized.
* - Any segment whose data buffer overlaps with tempBuffer[0 : len]
* must be treated as having invalid values in that array.
* - If successful, qrcode can be passed into qrcodegen_getSize() and qrcodegen_getModule().
*
* Please consult the QR Code specification for information on
* data capacities per version, ECC level, and text encoding mode.
*
* This function allows the user to create a custom sequence of segments that switches
* between modes (such as alphanumeric and byte) to encode text in less space.
* This is a low-level API; the high-level API is qrcodegen_encodeText() and qrcodegen_encodeBinary().
*/
bool qrcodegen_encodeSegments(const struct qrcodegen_Segment segs[], size_t len,
enum qrcodegen_Ecc ecl, uint8_t tempBuffer[], uint8_t qrcode[]);
/*
* Encodes the given segments to a QR Code, returning true if successful.
* If the data is too long to fit in any version in the given range
* at the given ECC level, then false is returned.
*
* Requires 1 <= minVersion <= maxVersion <= 40.
*
* The smallest possible QR Code version within the given range is automatically
* chosen for the output. Iff boostEcl is true, then the ECC level of the result
* may be higher than the ecl argument if it can be done without increasing the
* version. The mask is either between qrcodegen_Mask_0 to 7 to force that mask, or
* qrcodegen_Mask_AUTO to automatically choose an appropriate mask (which may be slow).
*
* About the byte arrays, letting len = qrcodegen_BUFFER_LEN_FOR_VERSION(qrcodegen_VERSION_MAX):
* - Before calling the function:
* - The array ranges tempBuffer[0 : len] and qrcode[0 : len] must allow
* reading and writing; hence each array must have a length of at least len.
* - The two ranges must not overlap (aliasing).
* - The initial state of both ranges can be uninitialized
* because the function always writes before reading.
* - The input array segs can contain segments whose data buffers overlap with tempBuffer.
* - After the function returns:
* - Both ranges have no guarantee on which elements are initialized and what values are stored.
* - tempBuffer contains no useful data and should be treated as entirely uninitialized.
* - Any segment whose data buffer overlaps with tempBuffer[0 : len]
* must be treated as having invalid values in that array.
* - If successful, qrcode can be passed into qrcodegen_getSize() and qrcodegen_getModule().
*
* Please consult the QR Code specification for information on
* data capacities per version, ECC level, and text encoding mode.
*
* This function allows the user to create a custom sequence of segments that switches
* between modes (such as alphanumeric and byte) to encode text in less space.
* This is a low-level API; the high-level API is qrcodegen_encodeText() and qrcodegen_encodeBinary().
*/
bool qrcodegen_encodeSegmentsAdvanced(const struct qrcodegen_Segment segs[], size_t len, enum qrcodegen_Ecc ecl,
int minVersion, int maxVersion, enum qrcodegen_Mask mask, bool boostEcl, uint8_t tempBuffer[], uint8_t qrcode[]);
/*
* Tests whether the given string can be encoded as a segment in numeric mode.
* A string is encodable iff each character is in the range 0 to 9.
*/
bool qrcodegen_isNumeric(const char *text);
/*
* Tests whether the given string can be encoded as a segment in alphanumeric mode.
* A string is encodable iff each character is in the following set: 0 to 9, A to Z
* (uppercase only), space, dollar, percent, asterisk, plus, hyphen, period, slash, colon.
*/
bool qrcodegen_isAlphanumeric(const char *text);
/*
* Returns the number of bytes (uint8_t) needed for the data buffer of a segment
* containing the given number of characters using the given mode. Notes:
* - Returns SIZE_MAX on failure, i.e. numChars > INT16_MAX or the internal
* calculation of the number of needed bits exceeds INT16_MAX (i.e. 32767).
* - Otherwise, all valid results are in the range [0, ceil(INT16_MAX / 8)], i.e. at most 4096.
* - It is okay for the user to allocate more bytes for the buffer than needed.
* - For byte mode, numChars measures the number of bytes, not Unicode code points.
* - For ECI mode, numChars must be 0, and the worst-case number of bytes is returned.
* An actual ECI segment can have shorter data. For non-ECI modes, the result is exact.
*/
size_t qrcodegen_calcSegmentBufferSize(enum qrcodegen_Mode mode, size_t numChars);
/*
* Returns a segment representing the given binary data encoded in
* byte mode. All input byte arrays are acceptable. Any text string
* can be converted to UTF-8 bytes and encoded as a byte mode segment.
*/
struct qrcodegen_Segment qrcodegen_makeBytes(const uint8_t data[], size_t len, uint8_t buf[]);
/*
* Returns a segment representing the given string of decimal digits encoded in numeric mode.
*/
struct qrcodegen_Segment qrcodegen_makeNumeric(const char *digits, uint8_t buf[]);
/*
* Returns a segment representing the given text string encoded in alphanumeric mode.
* The characters allowed are: 0 to 9, A to Z (uppercase only), space,
* dollar, percent, asterisk, plus, hyphen, period, slash, colon.
*/
struct qrcodegen_Segment qrcodegen_makeAlphanumeric(const char *text, uint8_t buf[]);
/*
* Returns a segment representing an Extended Channel Interpretation
* (ECI) designator with the given assignment value.
*/
struct qrcodegen_Segment qrcodegen_makeEci(long assignVal, uint8_t buf[]);
/*---- Functions to extract raw data from QR Codes ----*/
/*
* Returns the side length of the given QR Code, assuming that encoding succeeded.
* The result is in the range [21, 177]. Note that the length of the array buffer
* is related to the side length - every 'uint8_t qrcode[]' must have length at least
* qrcodegen_BUFFER_LEN_FOR_VERSION(version), which equals ceil(size^2 / 8 + 1).
*/
int qrcodegen_getSize(const uint8_t qrcode[]);
/*
* Returns the color of the module (pixel) at the given coordinates, which is false
* for light or true for dark. The top left corner has the coordinates (x=0, y=0).
* If the given coordinates are out of bounds, then false (light) is returned.
*/
bool qrcodegen_getModule(const uint8_t qrcode[], int x, int y);
#ifdef __cplusplus
}
#endif
+201
View File
@@ -0,0 +1,201 @@
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.
BIN
View File
Binary file not shown.
Binary file not shown.
+5079
View File
File diff suppressed because it is too large. Load diff