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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 53 additions & 0 deletions .github/workflows/macos-dmg.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
name: macOS build and package

on:
push:
paths-ignore: ['**.md']
pull_request:
paths: ['source/**', 'port/**', 'tools/**', 'configure.py', '.github/workflows/macos-dmg.yml']
workflow_dispatch:

permissions:
contents: read

concurrency:
group: macos-${{ github.ref }}
cancel-in-progress: true

jobs:
build:
runs-on: macos-26
timeout-minutes: 45
env:
HALO_MACOS_LLVM_BIN: /opt/homebrew/opt/llvm@22/bin
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: Public toolchain
run: |
test "$(uname -m)" = arm64
brew install llvm@22 lld@22 sdl3 ninja
printf '%s\n' /opt/homebrew/opt/lld@22/bin >> "$GITHUB_PATH"
python3 tools/macos_setup.py
- name: Build app
run: >-
python3 tools/macos_build.py --repository "$GITHUB_REPOSITORY"
--build-number "$GITHUB_RUN_NUMBER.$GITHUB_RUN_ATTEMPT"
- name: Create and verify ad-hoc DMG
run: |
python3 tools/macos_release.py local-dmg --output dist/macos/Halo-CE-Universal-macos-arm64.dmg
hdiutil verify dist/macos/Halo-CE-Universal-macos-arm64.dmg
dmg_mount="$(mktemp -d "$RUNNER_TEMP/halo-dmg.XXXXXX")"
hdiutil attach dist/macos/Halo-CE-Universal-macos-arm64.dmg -mountpoint "$dmg_mount" -nobrowse -readonly
trap 'hdiutil detach "$dmg_mount"' EXIT
codesign --verify --deep --strict "$dmg_mount/Halo CE Universal.app"
lipo "$dmg_mount/Halo CE Universal.app/Contents/MacOS/halo" -verify_arch arm64
- uses: actions/upload-artifact@v4
with:
name: halo-macos-arm64-dmg
path: dist/macos/Halo-CE-Universal-macos-arm64.dmg
if-no-files-found: error
compression-level: 0
retention-days: 14
16 changes: 13 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
# Halo: Combat Evolved for Linux, Windows and Android
# Halo: Combat Evolved for Linux, Windows, Android and macOS

[![Join our Discord](https://invidget.switchblade.xyz/9gqcHyr5km)](https://discord.gg/9gqcHyr5km)

This project is a port of the Halo: Combat Evolved decompilation to Linux,
Windows and Android. The decompilation is of the Xbox build 2342
Windows, Android and Apple Silicon macOS. The decompilation is of the Xbox build 2342
(`cachebeta.exe`, SHA-256
`4cc87b45f721270392a96f1674ed2b5cd4a7bb4355faeab4531d1cf1884d9520`).

Expand All @@ -22,12 +22,17 @@ builds of the latest release:
| Linux | [halo-linux-release.zip](https://github.com/cybersecurity/halo-ce-universal/releases/latest/download/halo-linux-release.zip) | [halo-linux-debug.zip](https://github.com/cybersecurity/halo-ce-universal/releases/latest/download/halo-linux-debug.zip) |
| Windows | [halo-windows-release.zip](https://github.com/cybersecurity/halo-ce-universal/releases/latest/download/halo-windows-release.zip) | [halo-windows-debug.zip](https://github.com/cybersecurity/halo-ce-universal/releases/latest/download/halo-windows-debug.zip) |
| Android | [halo-android-release.zip](https://github.com/cybersecurity/halo-ce-universal/releases/latest/download/halo-android-release.zip) | [halo-android-debug.zip](https://github.com/cybersecurity/halo-ce-universal/releases/latest/download/halo-android-debug.zip) |
| macOS (Apple Silicon) | [Mac app and DMG builds](../../actions/workflows/macos-dmg.yml) | [Build locally](port/macos/README.md#build) |

For Mac, open a successful **macOS build and package** workflow run and download its DMG
artifact, then drag the app to Applications. The app asks for your own game data
on first launch. Automated artifacts are ad-hoc signed and are not notarized.

Use the release build to play. The debug build stops at the first failed
assertion and writes it to the log. Use the debug build to find and report
problems.

The game updates itself. At start-up it looks for a newer release, and asks
On Linux, Windows and Android, the game updates itself. At start-up it looks for a newer release, and asks
if you want to install it. Refer to "Updates" in
[port/linux/README.md](port/linux/README.md#updates).

Expand All @@ -51,6 +56,9 @@ so players of the two versions can play together.
On Linux and Windows, the game puts `maps/` next to the executable. On
Android, copy the disc image to the phone first. The app puts `maps/` in its
data folder. Refer to [port/android/README.md](port/android/README.md).
On Mac, the native chooser can import a disc image or select an extracted game
folder. Imported data and saves stay in Application Support, outside the app.
See [the Mac instructions](port/macos/README.md#install-and-game-data).

## Platforms

Expand All @@ -61,6 +69,7 @@ Each platform has its own instructions:
| Linux (32-bit x86 executable, OpenGL 4.5, SDL3) | [port/linux/README.md](port/linux/README.md) |
| Windows (32-bit x86 executable, OpenGL 4.5, SDL3) | [port/windows/README.md](port/windows/README.md) |
| Android (arm64 app, OpenGL ES 3, SDL3) | [port/android/README.md](port/android/README.md) |
| macOS (Apple Silicon app, ANGLE/Metal, SDL3) | [port/macos/README.md](port/macos/README.md) |

The Linux README also gives the controls, the settings and the multiplayer
functions. These are almost the same on all platforms.
Expand Down Expand Up @@ -95,6 +104,7 @@ To build the game:
| `ninja linux` | `build/linux/halo` |
| `ninja windows` (on Windows) | `build/windows/halo.exe` and `SDL3.dll` |
| `ninja android_apk` | `port/android/app/build/outputs/apk/debug/app-debug.apk` |
| `python3 tools/macos_build.py` (after [Mac setup](port/macos/README.md#build)) | `build/macos/Halo CE Universal.app` |

If you enter `ninja` without a target, ninja builds the game for the
computer that you use.
Expand Down
16 changes: 13 additions & 3 deletions configure.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#!/usr/bin/env python3

# root build script: writes build.ninja for the native ports (Linux, Windows,
# Android)
# Android, macOS)

import argparse
import io
Expand Down Expand Up @@ -73,6 +73,11 @@
type=str,
help="clang with the arm64_32 target for the Android guest (default: clang)",
)
parser.add_argument("--macos", action="store_true", help="generate the rebased Apple Silicon guest target")
parser.add_argument("--android-guest-only", action="store_true", help="build the portable ARM image without an Android host")
parser.add_argument("--android-guest-llvm-bin", type=Path, help="directory containing llvm-ar and ld.lld")
parser.add_argument("--android-guest-gl-include", type=Path, help="directory containing Khronos GLES headers")
parser.add_argument("--android-guest-builtins", type=Path, help="optional AArch64 compiler builtins archive")
args = parser.parse_args()

# the settings the builds read
Expand All @@ -86,7 +91,12 @@
port_pgo=args.pgo,
port_pgo_profile=args.pgo_profile,
android_ndk=args.android_ndk,
android_guest_cc=args.android_guest_cc,
android_guest_cc="tools/macos_guest_cc.py" if args.macos else args.android_guest_cc,
android_guest_only=args.android_guest_only or args.macos,
android_guest_llvm_bin=args.android_guest_llvm_bin,
android_guest_gl_include=args.android_guest_gl_include,
android_guest_builtins=args.android_guest_builtins,
macos=args.macos,
)


Expand Down Expand Up @@ -135,7 +145,7 @@ def is_windows() -> bool:

# the build for this computer, where it could be generated (the Windows
# build is left out when SDL cannot be fetched, for instance)
default = "windows" if is_windows() else "linux"
default = "macos_guest" if args.macos else "windows" if is_windows() else "linux"
if f"\nbuild {default}: " in out.getvalue():
n.comment("Default rule: the build for this computer")
n.default(default)
Expand Down
8 changes: 8 additions & 0 deletions port/android/guest/runtime/guest_host.h
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,15 @@ long long host_sdl_ticks(void);
long long host_sdl_thread_id(void);
unsigned int host_sdl_create_window(const char *title, int width, int height, long long flags);
void host_sdl_window_size_in_pixels(unsigned int window, int *width, int *height);
void host_sdl_window_size(unsigned int window, int *width, int *height);
void host_sdl_warp_mouse(unsigned int window, float x, float y);
int host_sdl_set_relative_mouse(unsigned int window, int enabled);
#ifdef HALO_MACOS
/* Native display actions and the PC menu share the guest's display.mode. */
int host_sdl_display_mode(void);
int host_sdl_apply_display(int mode, int width, int height);
void host_sdl_request_quit(void);
#endif
int host_sdl_gl_set_attribute(int attribute, int value);
unsigned int host_sdl_gl_create_context(unsigned int window);
int host_sdl_gl_make_current(unsigned int window, unsigned int context);
Expand Down
15 changes: 15 additions & 0 deletions port/android/guest/runtime/guest_sdl.c
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,21 @@ bool SDL_SetWindowRelativeMouseMode(SDL_Window *window, bool enabled)
return host_sdl_set_relative_mouse((unsigned int)window, enabled) != 0;
}

bool SDL_GetWindowSize(SDL_Window *window, int *width, int *height)
{
int w = 0, h = 0;

host_sdl_window_size((unsigned int)window, &w, &h);
if (width) *width = w;
if (height) *height = h;
return w > 0 && h > 0;
}

void SDL_WarpMouseInWindow(SDL_Window *window, float x, float y)
{
host_sdl_warp_mouse((unsigned int)window, x, y);
}

bool SDL_GL_SetAttribute(SDL_GLAttr attribute, int value)
{
return host_sdl_gl_set_attribute((int)attribute, value) != 0;
Expand Down
7 changes: 7 additions & 0 deletions port/android/host/host.h
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ port/android/include/halo_android_abi.h for the guest contract.
#ifndef __HALO_ANDROID_HOST_H
#define __HALO_ANDROID_HOST_H

#ifdef HALO_MACOS
#include "../../macos/host/host.h"
#else

#include <stdarg.h>
#include <stddef.h>
#include <stdint.h>
Expand Down Expand Up @@ -93,4 +97,7 @@ void *host_resolve_import(const char *name);

void *host_gl_resolve(const char *name);

#define guest_code_pointer(address) ((void *)(uintptr_t)(address))

#endif /* HALO_MACOS */
#endif
13 changes: 12 additions & 1 deletion port/android/host/host_gl.c
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@ already have host types by then. Only strings need copying back.

void *host_gl_resolve(const char *name)
{
#ifdef HALO_MACOS
/* Darwin packs stack arguments differently from the guest's Linux ABI. */
extern void *host_gl_bridge(const char *name);
void *function = host_gl_bridge(name);
return function ? function : (void *)eglGetProcAddress(name);
#else
static void *library;
void *function = NULL;

Expand All @@ -25,6 +31,7 @@ void *host_gl_resolve(const char *name)
if (!function)
function = (void *)eglGetProcAddress(name);
return function;
#endif
}

void host_gl_get_string(uint32_t name, int index, char *buffer, uint32_t size)
Expand Down Expand Up @@ -124,7 +131,11 @@ because host_gl_wait_frame releases the ring slot first. */
void host_gl_buffer_write(uint32_t target, uint32_t offset, uint32_t size, const void *data)
{
void *mapping = glMapBufferRange(target, offset, size,
GL_MAP_WRITE_BIT | GL_MAP_UNSYNCHRONIZED_BIT);
GL_MAP_WRITE_BIT | GL_MAP_UNSYNCHRONIZED_BIT
#ifdef HALO_MACOS
| GL_MAP_INVALIDATE_RANGE_BIT
#endif
);

if (!mapping)
{
Expand Down
29 changes: 29 additions & 0 deletions port/android/host/host_sdl.c
Original file line number Diff line number Diff line change
Expand Up @@ -83,10 +83,12 @@ static void *handle_get(uint32_t handle, int type)

/* ---------- general */

#ifndef HALO_MACOS
int host_sdl_init(uint32_t flags)
{
return SDL_Init((SDL_InitFlags)flags);
}
#endif

int host_sdl_set_hint(const char *name, const char *value)
{
Expand Down Expand Up @@ -120,10 +122,12 @@ int64_t host_sdl_thread_id(void)

/* ---------- video */

#ifndef HALO_MACOS
uint32_t host_sdl_create_window(const char *title, int width, int height, int64_t flags)
{
return handle_new(_handle_window, SDL_CreateWindow(title, width, height, (SDL_WindowFlags)flags));
}
#endif

void host_sdl_window_size_in_pixels(uint32_t window, int *width, int *height)
{
Expand All @@ -135,13 +139,29 @@ void host_sdl_window_size_in_pixels(uint32_t window, int *width, int *height)
SDL_GetWindowSizeInPixels(object, width, height);
}

#ifndef HALO_MACOS
int host_sdl_set_relative_mouse(uint32_t window, int enabled)
{
SDL_Window *object = handle_get(window, _handle_window);

return object ? SDL_SetWindowRelativeMouseMode(object, enabled != 0) : 0;
}
#endif

void host_sdl_window_size(uint32_t window, int *width, int *height)
{
SDL_Window *object = handle_get(window, _handle_window);
*width = *height = 0;
if (object) SDL_GetWindowSize(object, width, height);
}

void host_sdl_warp_mouse(uint32_t window, float x, float y)
{
SDL_Window *object = handle_get(window, _handle_window);
if (object) SDL_WarpMouseInWindow(object, x, y);
}

#ifndef HALO_MACOS
int host_sdl_gl_set_attribute(int attribute, int value)
{
return SDL_GL_SetAttribute((SDL_GLAttr)attribute, value);
Expand Down Expand Up @@ -170,9 +190,11 @@ int host_sdl_gl_swap_window(uint32_t window)

return object ? SDL_GL_SwapWindow(object) : 0;
}
#endif

/* ---------- events */

#ifndef HALO_MACOS
int host_sdl_poll_event(void *event)
{
SDL_Event host_event;
Expand All @@ -184,6 +206,7 @@ int host_sdl_poll_event(void *event)
memcpy(event, &host_event, sizeof(host_event));
return 1;
}
#endif

/* ---------- gamepads */

Expand Down Expand Up @@ -342,6 +365,8 @@ uint32_t host_sdl_open_audio_stream(uint32_t device, const void *spec, uint32_t
struct audio_binding *binding = SDL_calloc(1, sizeof(*binding));
SDL_AudioStream *stream;

if (!binding)
return 0;
binding->callback = callback;
binding->userdata = userdata;
pthread_mutex_init(&binding->lock, NULL);
Expand Down Expand Up @@ -394,14 +419,18 @@ void host_sdl_get_clipboard_text(char *buffer, uint32_t size)
SDL_free(text);
}

#ifndef HALO_MACOS
int host_sdl_show_toast(const char *message, int duration, int gravity, int x, int y)
{
return SDL_ShowAndroidToast(message, duration, gravity, x, y) ? 1 : 0;
}
#endif

/* ---------- a message for the player (a host of another network version) */

#ifndef HALO_MACOS
int host_sdl_show_simple_message_box(uint32_t flags, const char *title, const char *message)
{
return SDL_ShowSimpleMessageBox((SDL_MessageBoxFlags)flags, title, message, NULL) ? 1 : 0;
}
#endif
10 changes: 7 additions & 3 deletions port/android/host/host_thread.c
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,11 @@ static int on_guest_stack(void)
{
uint64_t sp = (uint64_t)__builtin_frame_address(0);

#ifdef HALO_MACOS
return (sp & ~UINT64_C(0xffffffff)) == HALO_MACOS_BIAS;
#else
return sp < 0x100000000ULL;
#endif
}

/* ---------- calling into the guest */
Expand All @@ -70,13 +74,13 @@ uint32_t host_call_guest(uint32_t function, uint32_t a, uint32_t b, uint32_t c,
if (!on_guest_stack())
host_fatal("guest code called on a thread without a guest stack");
if (!guest_tp)
((guest_function)(uintptr_t)host_image.header->thread_attach)(0, 0, 0, 0);
return ((guest_function)(uintptr_t)function)(a, b, c, d);
((guest_function)guest_code_pointer(host_image.header->thread_attach))(0, 0, 0, 0);
return ((guest_function)guest_code_pointer(function))(a, b, c, d);
}

void host_run_guest_main(uint32_t boot)
{
((void (*)(uint32_t))(uintptr_t)host_image.header->start)(boot);
((void (*)(uint32_t))guest_code_pointer(host_image.header->start))(boot);
host_fatal("the guest returned from __guest_start");
}

Expand Down
2 changes: 2 additions & 0 deletions port/android/host_imports.list
Original file line number Diff line number Diff line change
Expand Up @@ -52,3 +52,5 @@ host_gl_wait_frame
host_android_path

host_errno
host_sdl_window_size
host_sdl_warp_mouse
5 changes: 5 additions & 0 deletions port/android/include/halo_android_abi.h
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,12 @@ keeps its heaps low in the address space and fills it upwards */
#define HALO_GUEST_WINDOW_SIZE 0x08000000u

#define HALO_GUEST_MAGIC 0x4f4c4148u /* 'HALO' */
#ifdef HALO_MACOS
/* Darwin rebases guest pointers; Android hosts must reject this image. */
#define HALO_GUEST_ABI_VERSION 0x10001
#else
#define HALO_GUEST_ABI_VERSION 1
#endif

/* at HALO_GUEST_IMAGE_BASE */
struct halo_guest_header
Expand Down
Loading