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
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.
The bundle of files to upload (only primary file is used)
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:
- Fetch version types and versions from CurseForge API
- Map Minecraft versions to CurseForge version IDs
- Add mod loaders, environment, and Java version
For Hytale Projects:
String endpoint = "https://legacy.curseforge.com";
List<Integer> gameVersions = Collections.singletonList(14284);
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
Optional Properties
CURSEFORGE_TYPE
String
default:"minecraft"
Project type: “minecraft” or “hytale”
Release type: “release”, “beta”, or “alpha”
Space-separated Minecraft version patterns (e.g., “1.20 1.20.1”)
Space-separated mod loaders (e.g., “fabric forge”)
Space-separated environments: “client”, “server”, or both
Java version (e.g., “17”, “21”)
JSON array of project relations/dependencies
Mark for manual release review: “true” or “false”
Common Properties
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;
}