Skip to content
Merged
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package net.ornithemc.osl.core.api.util;

/**
* Namespaced identifiers are two-part strings that uniquely point to content in Minecraft.
* The two parts are the namespace and the identifier. They can be combined into a single
* string representation as namespace:identifier (the namespace, followed by the identifier,
* separated by a colon).
* <p>
* A namespace is a domain for content. It is used not to point to specific content, but to
* differentiate between different content sources or publishers. The use of namespaces can
* prevent conflicts between mods, resource packs, or data packs, in cases where the same
* identifier is used.
* <p>
* The identifier is a unique name for content within a namespace. It should be descriptive
* to avoid naming conflicts with other content. The preferred format is snake_case.
* <p>
* Namespaces may only contain alphanumeric characters [a-zA-Z0-9] and special characters
* [-._]. Identifiers may also contain the special character [/].
*/
public interface NamespacedIdentifier {

/**
* The separator between the namespace and identifier in the {@code String}
* representation of a {@code NamespacedIdentifier}.
*/
static char SEPARATOR = ':';

/**
* @return the namespace of this {@code NamespacedIdentifier}.
*/
String namespace();

/**
* @return the identifier of this {@code NamespacedIdentifier}.
*/
String identifier();

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
package net.ornithemc.osl.core.api.util;

import java.util.Comparator;

import net.ornithemc.osl.core.impl.util.NamespacedIdentifierException;
import net.ornithemc.osl.core.impl.util.NamespacedIdentifierParseException;
import net.ornithemc.osl.core.impl.util.NamespacedIdentifierImpl;

/**
* Utility methods for creating and validating {@link NamespacedIdentifier}s.
*/
public final class NamespacedIdentifiers {

/**
* The {@code minecraft} namespace is used for Vanilla resources and ids.
*/
public static final String MINECRAFT_NAMESPACE = "minecraft";
/**
* The default namespace of {@code NamespacedIdentifier}s.
* It is recommended to use a custom namespace for your own identifiers.
*/
public static final String DEFAULT_NAMESPACE = MINECRAFT_NAMESPACE;

/**
* The maximum length of a {@code NamespacedIdentifier}'s namespace string.
*/
public static final int MAX_LENGTH_NAMESPACE = Integer.MAX_VALUE;
/**
* The maximum length of a {@code NamespacedIdentifier} identifier string.
*/
public static final int MAX_LENGTH_IDENTIFIER = Integer.MAX_VALUE;

/**
* A comparator for {@code NamespacedIdentifier}s, comparing first by identifier, then by namespace.
*/
public static final Comparator<NamespacedIdentifier> COMPARATOR = (a, b) -> {
int c = a.identifier().compareTo(b.identifier());
if (c == 0) {
c = a.namespace().compareTo(b.namespace());
}

return c;
};

/**
* Construct and validate a {@code NamespacedIdentifier} with the default namespace and the given identifier.
*
* @return a {@code NamespacedIdentifier} with the default namespace and the given identifier.
* @throws NamespacedIdentifierException
* if the given identifier is invalid.
*/
public static NamespacedIdentifier from(String identifier) {
return from(DEFAULT_NAMESPACE, identifier);
}

/**
* Construct and validate a {@code NamespacedIdentifier} from the given namespace and identifier.
*
* @return a {@code NamespacedIdentifier} with the given namespace and identifier.
* @throws NamespacedIdentifierException
* if the given namespace or identifier is invalid.
*/
public static NamespacedIdentifier from(String namespace, String identifier) {
return new NamespacedIdentifierImpl(
validateNamespace(namespace),
validateIdentifier(identifier)
);
}

/**
* Parse a {@code NamespacedIdentifier} from the given {@code String}.
* The returned identifier is always valid. If no valid identifier can
* be parsed from the given string, an exception is thrown.
*
* @return the {@code NamespacedIdentifier}} represented by the {@code String}.
* @throws NamespacedIdentifierParseException
* if no valid {@code NamespacedIdentifier} can be parsed from the given {@code String}.
*/
public static NamespacedIdentifier parse(String s) {
int i = s.indexOf(NamespacedIdentifier.SEPARATOR);

try {
if (i < 0) {
return from(s.substring(i + 1));
} else if (i > 0) {
return from(s.substring(0, i), s.substring(i + 1));
} else {
throw NamespacedIdentifierParseException.invalid(s, "badly formatted");
}
} catch (NamespacedIdentifierException e) {
throw NamespacedIdentifierParseException.invalid(s, e);
}
}

/**
* Check whether the given {@code NamespacedIdentifier} is valid, or throw an exception.
*/
public static NamespacedIdentifier validate(NamespacedIdentifier id) {
try {
validateNamespace(id.namespace());
validateIdentifier(id.identifier());

return id;
} catch (NamespacedIdentifierException e) {
throw NamespacedIdentifierException.invalid(id, e);
}
}

/**
* Check that the given namespace is valid for a {@code NamespacedIdentifier}.
*/
public static String validateNamespace(String namespace) {
if (namespace == null || namespace.isEmpty()) {
throw NamespacedIdentifierException.invalidNamespace(namespace, "null or empty");
}
if (namespace.length() > MAX_LENGTH_NAMESPACE) {
throw NamespacedIdentifierException.invalidNamespace(namespace, "length " + namespace.length() + " is greater than maximum allowed " + MAX_LENGTH_NAMESPACE);
}
if (!namespace.chars().allMatch(chr -> chr == '-' || chr == '.' || chr == '_' || (chr >= 'a' && chr <= 'z') || (chr >= 'A' && chr <= 'Z') || (chr >= '0' && chr <= '9'))) {
throw NamespacedIdentifierException.invalidNamespace(namespace, "contains illegal characters - only [a-zA-Z0-9-._] are allowed");
}

return namespace;
}

/**
* Check that the given identifier is valid for a {@code NamespacedIdentifier}.
*/
public static String validateIdentifier(String identifier) {
if (identifier == null || identifier.isEmpty()) {
throw NamespacedIdentifierException.invalidIdentifier(identifier, "null or empty");
}
if (identifier.length() > MAX_LENGTH_IDENTIFIER) {
throw NamespacedIdentifierException.invalidIdentifier(identifier, "length " + identifier.length() + " is greater than maximum allowed " + MAX_LENGTH_IDENTIFIER);
}
if (!identifier.chars().allMatch(chr -> chr == '-' || chr == '.' || chr == '_' || chr == '/' || (chr >= 'a' && chr <= 'z') || (chr >= 'A' && chr <= 'Z') || (chr >= '0' && chr <= '9'))) {
throw NamespacedIdentifierException.invalidIdentifier(identifier, "contains illegal characters - only [a-zA-Z0-9-._/] are allowed");
}

return identifier;
}

public static boolean equals(NamespacedIdentifier a, NamespacedIdentifier b) {
return a.namespace().equals(b.namespace()) && a.identifier().equals(b.identifier());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package net.ornithemc.osl.core.impl.mixin;

import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Pseudo;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;

import net.minecraft.client.resource.Identifier;

import net.ornithemc.osl.core.api.util.NamespacedIdentifier;
import net.ornithemc.osl.core.api.util.NamespacedIdentifiers;

@Pseudo // needed because Identifier does not exist in all versions
@Mixin(Identifier.class)
public class IdentifierMixin implements NamespacedIdentifier { // TODO: interface injection

@Shadow
private String namespace;
@Shadow
private String path;

@Inject(
method = "equals",
remap = false,
cancellable = true,
at = @At(
value = "HEAD"
)
)
private void osl$core$equalsNamespacedIdentifier(Object o, CallbackInfoReturnable<Boolean> cir) {
if (o instanceof NamespacedIdentifier) {
cir.setReturnValue(NamespacedIdentifiers.equals(this, (NamespacedIdentifier) o));
}
}

@Override
public String namespace() {
return namespace;
}

@Override
public String identifier() {
return path;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package net.ornithemc.osl.core.impl.util;

import net.ornithemc.osl.core.api.util.NamespacedIdentifier;

@SuppressWarnings("serial")
public class NamespacedIdentifierException extends RuntimeException {

private NamespacedIdentifierException(String message) {
super(message);
}

private NamespacedIdentifierException(String message, Throwable cause) {
super(message, cause);
}

public static NamespacedIdentifierException invalid(NamespacedIdentifier id, Throwable cause) {
return new NamespacedIdentifierException("\'" + id + "\' is not a valid namespaced identifier", cause);
}

public static NamespacedIdentifierException invalid(NamespacedIdentifier id, String reason) {
return new NamespacedIdentifierException("\'" + id + "\' is not a valid namespaced identifier: " + reason);
}

public static NamespacedIdentifierException invalidNamespace(String namespace, String reason) {
return new NamespacedIdentifierException("\'" + namespace + "\' is not a valid namespace: " + reason);
}

public static NamespacedIdentifierException invalidIdentifier(String identifier, String reason) {
return new NamespacedIdentifierException("\'" + identifier + "\' is not a valid identifier: " + reason);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package net.ornithemc.osl.core.impl.util;

import net.ornithemc.osl.core.api.util.NamespacedIdentifier;
import net.ornithemc.osl.core.api.util.NamespacedIdentifiers;

/**
* This class is a version-agnostic implementation of {@link NamespacedIdentifier}.
* <p>
* This class is essentially equivalent to Vanilla's {@code Identifier}. It was added for a
* few reasons. For one, Vanilla's {@code Identifier} was only added in 13w21a, and then was
* client-only until 14w27b. Implementation details of this class also changed a few times,
* and only since 17w43a were {@code Identifiers} validated in any way.
* <br> This class is available for all Minecraft versions, without any version-specific
* implementation details.
*/
public final class NamespacedIdentifierImpl implements NamespacedIdentifier {

private final String namespace;
private final String identifier;

public NamespacedIdentifierImpl(String namespace, String identifier) {
this.namespace = namespace;
this.identifier = identifier;
}

@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (!(o instanceof NamespacedIdentifier)) {
return false;
}
return NamespacedIdentifiers.equals(this, (NamespacedIdentifier) o);
}

@Override
public int hashCode() {
// this impl matches Vanilla Identifier's impl
return 31 * namespace.hashCode() + identifier.hashCode();
}

@Override
public String toString() {
return namespace + SEPARATOR + identifier;
}

@Override
public String namespace() {
return namespace;
}

@Override
public String identifier() {
return identifier;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package net.ornithemc.osl.core.impl.util;

@SuppressWarnings("serial")
public class NamespacedIdentifierParseException extends RuntimeException {

private NamespacedIdentifierParseException(String message) {
super(message);
}

private NamespacedIdentifierParseException(String message, Throwable cause) {
super(message, cause);
}

public static NamespacedIdentifierParseException invalid(String s, Throwable cause) {
return new NamespacedIdentifierParseException("unable to parse namespaced identifier from \'" + s + "\'", cause);
}

public static NamespacedIdentifierParseException invalid(String s, String reason) {
return new NamespacedIdentifierParseException("unable to parse namespaced identifier from \'" + s + "\': " + reason);
}
}
3 changes: 3 additions & 0 deletions libraries/core/src/main/resources/fabric.mod.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@
"license": "Apache-2.0",
"icon": "assets/ornithe-standard-libraries/core/icon.png",
"environment": "*",
"mixins": [
"osl.core.mixins.json"
],
"depends": {
"fabricloader": ">=0.16.0"
}
Expand Down
16 changes: 16 additions & 0 deletions libraries/core/src/main/resources/osl.core.mixins.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"required": true,
"minVersion": "0.8",
"package": "net.ornithemc.osl.core.impl.mixin",
"compatibilityLevel": "JAVA_8",
"mixins": [
"IdentifierMixin"
],
"client": [
],
"server": [
],
"injectors": {
"defaultRequire": 1
}
}