Skip to content

MiNT Multiplayer v0.6.0-dev

Pre-release
Pre-release

Choose a tag to compare

@DaVillka DaVillka released this 08 Oct 18:05

MiNT Multiplayer v0.6.0

Version 0.6.0 introduces a launcher API for third-party applications, a new entity outline system, 3D UI improvements, expanded client/server events, resource build improvements, and client performance fixes.

Highlights

  • Launcher API: Control settings, updates, server resource preparation, and game launch through a JSON-based stdin/stdout interface.
  • Entity outlines: Highlight players, vehicles, and other entities with contour, glow, or fill effects.
  • 3D UI improvements: Added vehicle rendering support and scene snapshots that return a GTA texture.
  • Expanded APIs: New waypoint, attachment, model-change, weapon-damage, and damage-reporting events.
  • Performance and stability: Reduced per-frame work and fixed issues affecting audio, startup, and vehicle synchronization.

Launcher

Launcher architecture

The launcher browser has been moved out of the game client core to make the client easier to extend.

Launcher API for custom applications

The launcher now supports an API mode for external applications. Communication uses JSON messages over standard input and standard output (stdin/stdout).

Start the launcher in API mode:

Launcher.exe --api

In this mode, the launcher runs without showing its window and waits for commands.

Ready event

When initialization is complete, the launcher emits:

{"event":"ready","commands":["settings.get","settings.set","servers.list","update","prepare","launch","cancel","trust.confirm","shutdown"]}

Each request contains:

Field Description
id A positive integer that uniquely identifies the request.
command The name of the command to execute.
Additional fields Command-specific parameters.

Available commands

Command Description
settings.get Read settings from client.toml.
settings.set Update the specified launcher settings.
servers.list Retrieve the server list in result.servers.
update Check for and install MiNT updates without starting GTA.
prepare Verify and download resources for a selected server, without updating MiNT or launching the game. GTA does not need to be installed.
launch Update MiNT, prepare server resources, and launch GTA.
cancel Request cancellation of an active operation.
trust.confirm Respond to a server-key-change confirmation request.
shutdown Cancel the active operation, wait for cleanup, and stop the launcher API.

Request examples

Each line below represents an individual JSON request.

Read settings:

{"id":1,"command":"settings.get"}

Update settings:

{"id":2,"command":"settings.set","settings":{"playerName":"Player","voiceVolume":80}}

Supported settings.set fields:

  • playerName
  • gameDirectory
  • voiceVolume
  • microphoneVolume
  • microphoneEnabled
  • discordEnabled

Volume settings accept integer values from 0 to 100.

List servers:

{"id":3,"command":"servers.list"}

Update MiNT:

{"id":4,"command":"update"}

Prepare server resources:

{"id":5,"command":"prepare","server":"127.0.0.1:22005"}

prepare does not update MiNT, does not require GTA to be installed, and does not start the game.

Launch GTA:

{"id":6,"command":"launch","server":"127.0.0.1:22005"}

Running prepare first is optional. If suitable resources have already been prepared, launch reuses the existing cache.

Optional launch request fields:

Field Description
gameDirectory Path to the GTA installation. If omitted, the launcher uses the saved path and automatic detection. If GTA is not found, an error is returned without opening a folder selection dialog.
offline Skip the MiNT update step. Server resource verification still runs.
{"id":7,"command":"launch","server":"127.0.0.1:22005","gameDirectory":"D:\\Games\\GTA V","offline":true}

Cancel an operation:

{"id":8,"command":"cancel","targetId":7}

Confirm a server key change:

{"id":9,"command":"trust.confirm","targetId":7,"requestId":"1","accepted":true}

targetId identifies the operation awaiting confirmation. requestId is the string supplied by the trust.required event. Set accepted to true or false according to the user's choice.

Shut down the launcher API:

{"id":10,"command":"shutdown"}

Responses and events

Each request receives one final response with the same id.

Successful response:

{"id":5,"ok":true,"result":{"status":"ready"}}

Error response:

{"id":5,"ok":false,"error":{"code":"PREPARATION_FAILED","message":"Description of the error"}}

Use error.code for programmatic error handling and error.message for a human-readable description.

Typical results:

Operation Result
MiNT update status: "ready", or "skipped" when the update is skipped in offline mode.
prepare status: "ready"
launch status: "handedOff" after the first successful connection.
cancel status: "cancelling"; the cancelled operation reports its final result separately after cleanup.
trust.confirm status: "accepted", indicating the decision has been received for processing.
shutdown status: "stopped", after which the process exits.

Intermediate events include an event property and the id of the associated request.

Progress events report update, resource preparation, or launch progress:

  • stage: update, prepare, or launch.
  • phase: the current activity within that stage.
  • completedBytes and totalBytes: byte progress for the current phase. totalBytes: null means the total is unknown.
  • Launch events may provide a message instead of byte counters.
{"id":5,"event":"progress","stage":"prepare","phase":"resources","completedBytes":5000,"totalBytes":10000}

Server trust events: A trust.required event indicates that a server key has changed and a user decision is required. It includes requestId, server, previousKey, and candidateKey. Show the user the server and both keys, then send trust.confirm with their decision.

Concurrency: Only one operation can run at a time. While it is active, regular commands return BUSY; cancel, trust.confirm, and shutdown remain available. After a final response, the launcher API continues accepting commands. Determine an operation's result from its JSON response, not from the launcher's process exit code.

Python integration example

The following sample starts the launcher API, lists available servers, prepares a selected server, launches GTA, and shuts down the API. Update LAUNCHER to match the installation path.

import itertools
import json
import subprocess


LAUNCHER = r"D:\MiNT\Launcher.exe"


def main():
    with subprocess.Popen(
        [LAUNCHER, "--api"],
        stdin=subprocess.PIPE,
        stdout=subprocess.PIPE,
        creationflags=subprocess.CREATE_NO_WINDOW,
    ) as process:
        ids = itertools.count(1)

        def send(command, **arguments):
            request_id = next(ids)
            payload = {"id": request_id, "command": command, **arguments}
            process.stdin.write(
                (json.dumps(payload, ensure_ascii=False) + "\n").encode("utf-8")
            )
            process.stdin.flush()
            return request_id

        def receive():
            line = process.stdout.readline()
            if not line:
                raise RuntimeError("MiNT API closed the connection before responding")
            return json.loads(line.decode("utf-8"))

        def request(command, **arguments):
            request_id = send(command, **arguments)
            while True:
                message = receive()
                if "ok" in message:
                    if not message["ok"]:
                        error = message["error"]
                        raise RuntimeError(f"{error['code']}: {error['message']}")
                    if message["id"] == request_id:
                        return message["result"]
                    # Responses to trust.confirm also pass through this loop.
                elif message.get("event") == "progress":
                    stage = message.get("stage", "")
                    if "message" in message:
                        print(stage, message["message"])
                    else:
                        done = message.get("completedBytes", 0)
                        total = message.get("totalBytes")
                        print(
                            stage,
                            message.get("phase", ""),
                            f"{done}/{total or '?'} bytes",
                        )
                elif message.get("event") == "trust.required":
                    print("Server key has changed:", message["server"])
                    print("Previous key:", message["previousKey"])
                    print("New key:", message["candidateKey"])
                    accepted = (
                        input("Accept the new key? [yes/no]: ")
                        .strip()
                        .lower()
                        == "yes"
                    )
                    send(
                        "trust.confirm",
                        targetId=message["id"],
                        requestId=message["requestId"],
                        accepted=accepted,
                    )

        try:
            ready = receive()
            if ready.get("event") != "ready":
                raise RuntimeError(f"API is not ready: {ready}")

            try:
                for server in request("servers.list")["servers"]:
                    host = server["host"]
                    address = f"[{host}]" if ":" in host else host
                    print(f"{server['name']} — {address}:{server['port']}")
            except RuntimeError as error:
                print("Server list is unavailable:", error)

            endpoint = input("Server address (host:port): ").strip()
            request("prepare", server=endpoint)
            print("Resources are ready. Launching GTA...")
            result = request("launch", server=endpoint)
            print("Launch result:", result["status"])
            request("shutdown")
        finally:
            try:
                process.stdin.close()
            except BrokenPipeError:
                pass
            for _ in process.stdout:
                pass
            process.wait()


if __name__ == "__main__":
    try:
        main()
    except KeyboardInterrupt:
        print("Operation stopped.")
    except (OSError, RuntimeError) as error:
        print("Error:", error)

Console Commands and Configuration

New console commands

Command Description
mic-volume Adjust microphone volume.
voice-volume Adjust the volume of other players' voice chat.
netgraph Display FPS, ping, and frame time.

Expanded game configuration

The [pools] section in gameConfig has been expanded from 6 to 21 properties.


Entity Outline System

Added an API for highlighting entities, with three rendering styles:

Option Values Description
style contour, glow, fill Sharp outer outline, soft outer glow, or filled silhouette. Default: contour.
mode visible, xray Respect depth testing or render through other geometry. Default: xray.
color { r, g, b, a } RGBA components from 0 to 255, including alpha. Default: (0, 255, 255, 230).
width 1–8 Outline thickness or glow radius in pixels. Default: 2. Ignored for fill.

TypeScript API

export interface IOutlineOptions {
    /** Rendering style. Default: 'contour'. */
    style?: 'contour' | 'glow' | 'fill';

    /** Visibility mode. Default: 'xray'. */
    mode?: 'visible' | 'xray';

    /** RGBA values from 0 to 255. Default: (0, 255, 255, 230). */
    color?: IRGBA;

    /** Thickness/radius in pixels (1–8). Default: 2. Ignored for 'fill'. */
    width?: number;
}

export class Outline {
    constructor(options?: IOutlineOptions);

    readonly valid: boolean;
    readonly size: number;

    /** Replaces options; omitted properties reset to defaults. */
    options: IOutlineOptions;

    /** Accepts an Entity or GTA script ID, not a network ID. */
    add(target: Entity | number): boolean;
    remove(target: Entity | number): boolean;
    has(target: Entity | number): boolean;

    /** Removes all targets while preserving the group and its options. */
    clear(): void;

    /** Destroys the group and releases its slot. */
    destroy(): void;
}

/** Highlights one target. A subsequent call replaces the previous target. */
export function setOutline(
    target: Entity | number,
    options?: IOutlineOptions,
): boolean;

/** Clears the current resource's single-target outline. */
export function clearOutline(): void;

Usage example

Press F6 to highlight the local player and their vehicle (if present), or F9 to clear the highlights.

import * as mint from 'mint-client';

const selected = new mint.Outline({
    style: 'glow',
    mode: 'visible',
    width: 6,
    color: new mint.RGBA(0, 255, 180, 220),
});

mint.on('keyup', key => {
    if (key === mint.KeyCode.F6) {
        selected.clear();

        if (!selected.add(mint.Player.local)) {
            mint.logWarning('Failed to add the player to the outline.');
        }

        const vehicle = mint.Player.local.vehicle;

        if (vehicle && !selected.add(vehicle)) {
            mint.logWarning('Failed to add the vehicle to the outline.');
        }
    } else if (key === mint.KeyCode.F9) {
        selected.clear();
    }
});

Options can be updated at runtime:

selected.options = {
    ...selected.options,
    mode: 'xray',
};

3D UI Improvements

UI3DScene now supports vehicle rendering. A new snapshot method also returns a GTA texture containing the rendered scene.

Render a 3D scene

import * as mint from 'mint-client';

const scene = new mint.Utils.UI3DScene('PAUSE_SINGLE_RIGHT', {
    overlay: true,
});

scene.setPed(0, mint.Player.local, {
    pos: { x: 0, y: 0.1, z: 0 },
    rot: { x: 0, y: 0, z: 15 },
    lightIntensity: 0.8,
});

const tick = mint.everyTick(() => scene.render());

mint.on('resourceStop', name => {
    if (name !== mint.resourceName) return;

    mint.clearEveryTick(tick);
    scene.destroy();
});

Capture and display a scene snapshot

The following example captures a texture at the current screen aspect ratio, limiting the maximum dimension to 2048 pixels, then displays it using drawSprite.

import * as mint from 'mint-client';
import * as native from 'natives';

const scene = new mint.Utils.UI3DScene('PAUSE_SINGLE_RIGHT');

scene.setPed(0, mint.Player.local, {
    rot: { x: 0, y: 0, z: 15 },
    lightIntensity: 0.8,
});

const screen = mint.getScreenResolution();
const scale = Math.min(1, 2048 / Math.max(screen.x, screen.y));

let image;

try {
    image = await scene.snapshot({
        size: {
            x: Math.max(1, Math.round(screen.x * scale)),
            y: Math.max(1, Math.round(screen.y * scale)),
        },
        timeout: 5000,
    });
} finally {
    scene.destroy(); // The captured texture remains valid.
}

const tick = mint.everyTick(() => {
    if (!image.valid) return;

    native.drawSprite(
        image.textureDict, image.textureName,
        0.5, 0.5, 1, 1,
        0, 255, 255, 255, 255,
        false, false,
    );
});

mint.on('resourceStop', name => {
    if (name !== mint.resourceName) return;

    mint.clearEveryTick(tick);
    image.destroy();
});

API and Event Updates

Waypoint events

Added events for creating, removing, and reaching map waypoints:

waypointCreated: (position: Vector3) => void;
waypointRemoved: (position: Vector3) => void;
waypointReached: (position: Vector3) => void;

Entity attachment events

Added events for attaching and detaching entities, including vehicle-related attachment types:

entityAttached: (
    parent: Entity,
    child: Entity,
    kind: shared.AttachmentKind
) => void;

entityDetached: (
    parent: Entity,
    child: Entity,
    kind: shared.AttachmentKind
) => void;

export enum AttachmentKind {
    GenericPhysical = 0, // Standard physical attachment
    TrailerHitch = 1,    // Trailer
    TowArm = 2,          // Tow truck
    CargobobHook = 3,    // Cargobob hook
    CargobobMagnet = 4,  // Cargobob magnet
}

Added TypeScript declarations for vehicle attachment events:

vehicleAttach: (target: Vehicle, child: Vehicle) => void;
vehicleDetach: (target: Vehicle, child: Vehicle) => void;

Entity model changes

Added the entityModelChange event for client/server model-change notifications:

entityModelChange: (
    entity: Entity,
    oldModel: number,
    newModel: number
) => void;

Client-side weaponDamage event

weaponDamage: (
    target: Entity,
    weaponHash: number,
    damage: number,
    offset: Vector3,
    bodyPart: BodyPart,
    sourceEntity: Entity
) => boolean | number | void;

Updated server-side weaponDamage event

weaponDamage: (
    source: Player,
    target: Entity,
    weaponHash: number,
    damage: number,
    shotOffset: shared.IVector3,
    bodyPart: shared.BodyPart,
    sourceEntity: Entity
) => boolean | number | void;

Parameter descriptions:

Parameter Description
source Player who sent the hit request to the server.
target Entity receiving the hit.
weaponHash Weapon hash.
damage Incoming damage value; the final HP reduction is determined by GTA's damage calculations.
offset / shotOffset Hit location in the target's local coordinates.
bodyPart Value from BodyPart, e.g. Head = 20, Unknown = -1.
sourceEntity Entity that caused the hit.

Server API change: The previous boneId parameter has been replaced with BodyPart, and sourceEntity has been added.

Server-side damage events

Added events reporting damage that was actually applied:

playerDamage: (
    victim: Player,
    attacker: Entity | null,
    healthDamage: number,
    armourDamage: number,
    weaponHash: number
) => void;

pedDamage: (
    ped: Ped,
    attacker: Entity | null,
    healthDamage: number,
    armourDamage: number,
    weapon: number
) => void | boolean;

vehicleDamage: (
    vehicle: Vehicle,
    attacker: Entity | null,
    bodyHealthDamage: number,
    additionalBodyHealthDamage: number,
    engineHealthDamage: number,
    petrolTankDamage: number,
    weaponHash: number
) => void;

Call native functions by hash

Added invoke for calling GTA native functions by hash:

export function invoke(
    hash: string | bigint,
    ...args: unknown[]
): unknown;

Example:

import * as native from 'natives';

const gameTimer = native.invoke('0x9CD27B0045628463');

New statSetInt native method

export function statSetInt(
    statName: number,
    value: number,
    save: boolean
): boolean;

Resource Build and Game Data

  • Standalone streaming assets can now be packaged without placeholder metadata.
  • Fixed an issue affecting small resource packages.
  • Added and integrated game-data.json, containing information about built-in vehicles, characters, weapons, clothing, model skeletons, color palettes, and wheel categories.

Performance, Synchronization, and Fixes

  • Reduced unnecessary entity-state reads and per-frame processing to improve FPS.
  • Fixed an audio-related cause of game freezes.
  • Fixed the client hanging during startup.
  • Fixed disconnections when spawning vehicles with invalid model hashes.
  • Increased controller limits to match the number of entities within the streaming area.
  • Removed the disconnect console command.

Compatibility Notes

  • Server-side weaponDamage: Replace the former boneId handling with BodyPart and account for the new sourceEntity parameter.
  • Console: The disconnect command is no longer available.