Skip to main content
The CurseForgePlatform class implements the Platform interface to upload mod files to CurseForge for both Minecraft and Hytale projects.

Package

me.hsgamer.mcreleaser.curseforge.CurseForgePlatform

Class Declaration

public class CurseForgePlatform implements Platform {
    private final Logger logger = LoggerFactory.getLogger(getClass());
}

Constructor

CurseForgePlatform()

Creates a new CurseForge platform instance and inherits game version configuration from common properties if not set.
public CurseForgePlatform() {
    if (CurseForgePropertyKey.GAME_VERSIONS.isAbsent() && 
        CommonPropertyKey.GAME_VERSIONS.isPresent()) {
        CurseForgePropertyKey.GAME_VERSIONS.setValue(
            CommonPropertyKey.GAME_VERSIONS.getValue()
        );
    }
}

Methods

createUploadRunnable

Creates a batch runnable that uploads files to CurseForge.
fileBundle
FileBundle
required
The bundle of files to upload (only primary file is used)
runnable
Optional<BatchRunnable>
Returns a BatchRunnable if all required properties are present, otherwise empty
@Override
public Optional<BatchRunnable> createUploadRunnable(FileBundle fileBundle) {
    if (PropertyKeyUtil.isAbsentAndAnnounce(logger, 
        CurseForgePropertyKey.TOKEN, 
        CurseForgePropertyKey.PROJECT)) {
        return Optional.empty();
    }
    // Create upload tasks...
}

Upload Process

The upload is executed in four phases:

Phase 1: Client Initialization

CloseableHttpClient client = HttpClients.createMinimal();

Phase 2: Version Setup

Different setup based on project type: For Minecraft Projects:
  1. Fetch version types and versions from CurseForge API
  2. Map Minecraft versions to CurseForge version IDs
  3. Add mod loaders, environment, and Java version
For Hytale Projects:
String endpoint = "https://legacy.curseforge.com";
List<Integer> gameVersions = Collections.singletonList(14284);

Phase 3: Metadata Preparation

Builds the upload metadata:
JsonObject metadata = new JsonObject();
metadata.addProperty("changelog", CommonPropertyKey.DESCRIPTION.getValue());
metadata.addProperty("changelogType", "markdown");
metadata.addProperty("displayName", CommonPropertyKey.NAME.getValue());
metadata.addProperty("releaseType", 
    CurseForgePropertyKey.RELEASE_TYPE.getValue("release").toLowerCase());

JsonArray gameVersionsArray = new JsonArray();
for (Integer gameVersion : gameVersions) {
    gameVersionsArray.add(gameVersion);
}
metadata.add("gameVersions", gameVersionsArray);

Phase 4: File Upload

MultipartEntityBuilder builder = MultipartEntityBuilder.create()
    .addTextBody("metadata", gson.toJson(metadata), ContentType.APPLICATION_JSON)
    .addBinaryBody("file", fileBundle.primaryFile());

HttpPost request = new HttpPost(endpoint + "/api/projects/" + project + "/upload-file");
request.setHeader("X-Api-Token", CurseForgePropertyKey.TOKEN.getValue());
request.setEntity(builder.build());

Required Properties

CURSEFORGE_TOKEN
String
required
CurseForge API token
CURSEFORGE_PROJECT
String
required
CurseForge project ID

Optional Properties

CURSEFORGE_TYPE
String
default:"minecraft"
Project type: “minecraft” or “hytale”
CURSEFORGE_RELEASE_TYPE
String
default:"release"
Release type: “release”, “beta”, or “alpha”
CURSEFORGE_GAME_VERSIONS
String
Space-separated Minecraft version patterns (e.g., “1.20 1.20.1”)
CURSEFORGE_MOD_LOADERS
String
Space-separated mod loaders (e.g., “fabric forge”)
CURSEFORGE_ENVIRONMENT
String
Space-separated environments: “client”, “server”, or both
CURSEFORGE_JAVA_VERSION
String
Java version (e.g., “17”, “21”)
CURSEFORGE_RELATIONS
String
JSON array of project relations/dependencies
CURSEFORGE_MANUAL
String
Mark for manual release review: “true” or “false”

Common Properties

NAME
String
required
Version display name
VERSION
String
required
Version number
DESCRIPTION
String
required
Changelog (supports Markdown)

Configuration Examples

Basic Fabric Mod

export CURSEFORGE_TOKEN="your-curseforge-token"
export CURSEFORGE_PROJECT="123456"
export CURSEFORGE_GAME_VERSIONS="1.20.1 1.20.2"
export CURSEFORGE_MOD_LOADERS="fabric"
export CURSEFORGE_ENVIRONMENT="client server"

export NAME="MyMod v1.0.0"
export VERSION="1.0.0"
export DESCRIPTION="## Changes\n\n- Initial release"
export FILES="build/libs/*.jar"

java -jar mcreleaser-cli.jar

Forge Mod with Java Version

export CURSEFORGE_MOD_LOADERS="forge"
export CURSEFORGE_JAVA_VERSION="17"
export CURSEFORGE_GAME_VERSIONS="1.20"

Beta Release

export CURSEFORGE_RELEASE_TYPE="beta"
export NAME="MyMod v2.0.0-beta"

With Dependencies

export CURSEFORGE_RELATIONS='[
  {
    "slug": "jei",
    "type": "requiredDependency"
  },
  {
    "slug": "config-api",
    "type": "optionalDependency"
  },
  {
    "slug": "old-mod",
    "type": "incompatible"
  }
]'
Relation types:
  • requiredDependency - Must be installed
  • optionalDependency - Recommended but not required
  • incompatible - Conflicts with this mod
  • embeddedLibrary - Included in the mod file
  • tool - Development tool

Manual Review

export CURSEFORGE_MANUAL="true"
# File will require manual approval before publishing

Hytale Project

export CURSEFORGE_TYPE="hytale"
export CURSEFORGE_PROJECT="789012"
# No game versions needed for Hytale

Client-Only Mod

export CURSEFORGE_ENVIRONMENT="client"

Multi-Loader Support

export CURSEFORGE_MOD_LOADERS="fabric forge quilt"
export CURSEFORGE_GAME_VERSIONS="1.20 1.20.1"

Property Keys Source

Defined in CurseForgePropertyKey:
public interface CurseForgePropertyKey {
    PropertyPrefix CURSEFORGE = new PropertyPrefix("curseforge");
    PropertyKey TOKEN = CURSEFORGE.key("token");
    PropertyKey PROJECT = CURSEFORGE.key("project");
    PropertyKey TYPE = CURSEFORGE.key("type");
    PropertyKey RELEASE_TYPE = CURSEFORGE.key("releaseType");
    PropertyKey GAME_VERSIONS = CURSEFORGE.key("gameVersions");
    PropertyKey RELATIONS = CURSEFORGE.key("relations");
    PropertyKey MOD_LOADERS = CURSEFORGE.key("modLoaders");
    PropertyKey JAVA_VERSION = CURSEFORGE.key("javaVersion");
    PropertyKey ENVIRONMENT = CURSEFORGE.key("environment");
    PropertyKey MANUAL = CURSEFORGE.key("manual");
}

Supported Project Types

Minecraft

export CURSEFORGE_TYPE="minecraft"
Endpoint: https://minecraft.curseforge.com Features:
  • Automatic version ID resolution
  • Mod loader support
  • Environment specification
  • Java version tagging

Hytale

export CURSEFORGE_TYPE="hytale"
Endpoint: https://legacy.curseforge.com Features:
  • Fixed game version (14284)
  • Simplified configuration

Release Types

  • release - Stable release (default)
  • beta - Beta version
  • alpha - Alpha/development version

Environment Values

  • client - Client-side only
  • server - Server-side only
  • client server - Both client and server

Version Resolution

For Minecraft projects, version IDs are resolved through the CurseForge API:
BiConsumer<String, String> addToGameVersions = (versionName, typeSlug) -> {
    // Fetch version types
    Set<Integer> versionTypeIds = versionTypes.asList().stream()
        .map(JsonElement::getAsJsonObject)
        .filter(versionType -> matchesTypeSlug(versionType, typeSlug))
        .map(versionType -> versionType.get("id").getAsInt())
        .collect(Collectors.toSet());
    
    // Find matching versions
    versions.asList().stream()
        .map(JsonElement::getAsJsonObject)
        .filter(version -> version.get("name").getAsString().equalsIgnoreCase(versionName))
        .filter(version -> versionTypeIds.contains(version.get("gameVersionTypeID").getAsInt()))
        .forEach(version -> gameVersions.add(version.get("id").getAsInt()));
};

File Handling

Important: CurseForge only uploads the primary file:
.addBinaryBody("file", fileBundle.primaryFile())
Secondary files are ignored.

Error Handling

Invalid Project Type

if (!type.equalsIgnoreCase("minecraft") && !type.equalsIgnoreCase("hytale")) {
    logger.error("Unknown platform type. Supported types: MINECRAFT, HYTALE");
    return Optional.empty();
}

API Errors

if (response.getCode() != 200) {
    String responseBody = EntityUtils.toString(response.getEntity());
    logger.error("Failed to upload version: {} - {}", 
        response.getCode(), responseBody);
    return false;
}

Build docs developers (and LLMs) love