package com.mojang.blaze3d.preprocessor; import com.google.common.base.Strings; import com.google.common.collect.Lists; import java.util.List; import java.util.Locale; import java.util.regex.Matcher; import java.util.regex.Pattern; import net.fabricmc.api.EnvType; import net.fabricmc.api.Environment; import net.minecraft.client.renderer.ShaderDefines; import net.minecraft.util.FileUtil; import net.minecraft.util.StringUtil; import org.jspecify.annotations.Nullable; @Environment(EnvType.CLIENT) public abstract class GlslPreprocessor { private static final String C_COMMENT = "/\\*(?:[^*]|\\*+[^*/])*\\*+/"; private static final String LINE_COMMENT = "//[^\\v]*"; private static final Pattern REGEX_MOJ_IMPORT = Pattern.compile( "(#(?:/\\*(?:[^*]|\\*+[^*/])*\\*+/|\\h)*moj_import(?:/\\*(?:[^*]|\\*+[^*/])*\\*+/|\\h)*(?:\"(.*)\"|<(.*)>))" ); private static final Pattern REGEX_VERSION = Pattern.compile("(#(?:/\\*(?:[^*]|\\*+[^*/])*\\*+/|\\h)*version(?:/\\*(?:[^*]|\\*+[^*/])*\\*+/|\\h)*(\\d+))\\b"); private static final Pattern REGEX_ENDS_WITH_WHITESPACE = Pattern.compile("(?:^|\\v)(?:\\s|/\\*(?:[^*]|\\*+[^*/])*\\*+/|(//[^\\v]*))*\\z"); public List process(final String source) { GlslPreprocessor.Context context = new GlslPreprocessor.Context(); List sourceList = this.processImports(source, context, ""); sourceList.set(0, this.setVersion((String)sourceList.get(0), context.glslVersion)); return sourceList; } private List processImports(final String source, final GlslPreprocessor.Context context, final String parentPath) { int thisSourceId = context.sourceId; int previousMatchEnd = 0; String lineMacro = ""; List sourceList = Lists.newArrayList(); Matcher matcher = REGEX_MOJ_IMPORT.matcher(source); while (matcher.find()) { if (!isDirectiveDisabled(source, matcher, previousMatchEnd)) { String path = matcher.group(2); boolean isRelative = path != null; if (!isRelative) { path = matcher.group(3); } if (path != null) { String sourceBeforeImport = source.substring(previousMatchEnd, matcher.start(1)); String importPath = parentPath + path; String contents = this.applyImport(isRelative, importPath); if (!Strings.isNullOrEmpty(contents)) { if (!StringUtil.endsWithNewLine(contents)) { contents = contents + System.lineSeparator(); } context.sourceId++; int importSourceId = context.sourceId; List importedSources = this.processImports(contents, context, isRelative ? FileUtil.getFullResourcePath(importPath) : ""); importedSources.set(0, String.format(Locale.ROOT, "#line %d %d\n%s", 0, importSourceId, this.processVersions((String)importedSources.get(0), context))); if (!StringUtil.isBlank(sourceBeforeImport)) { sourceList.add(sourceBeforeImport); } sourceList.addAll(importedSources); } else { String disabledImport = isRelative ? String.format(Locale.ROOT, "/*#moj_import \"%s\"*/", path) : String.format(Locale.ROOT, "/*#moj_import <%s>*/", path); sourceList.add(lineMacro + sourceBeforeImport + disabledImport); } int lineCount = StringUtil.lineCount(source.substring(0, matcher.end(1))); lineMacro = String.format(Locale.ROOT, "#line %d %d", lineCount, thisSourceId); previousMatchEnd = matcher.end(1); } } } String remaining = source.substring(previousMatchEnd); if (!StringUtil.isBlank(remaining)) { sourceList.add(lineMacro + remaining); } return sourceList; } private String processVersions(final String source, final GlslPreprocessor.Context context) { Matcher matcher = REGEX_VERSION.matcher(source); if (matcher.find() && isDirectiveEnabled(source, matcher)) { context.glslVersion = Math.max(context.glslVersion, Integer.parseInt(matcher.group(2))); return source.substring(0, matcher.start(1)) + "/*" + source.substring(matcher.start(1), matcher.end(1)) + "*/" + source.substring(matcher.end(1)); } else { return source; } } private String setVersion(final String source, final int version) { Matcher matcher = REGEX_VERSION.matcher(source); return matcher.find() && isDirectiveEnabled(source, matcher) ? source.substring(0, matcher.start(2)) + Math.max(version, Integer.parseInt(matcher.group(2))) + source.substring(matcher.end(2)) : source; } private static boolean isDirectiveEnabled(final String source, final Matcher matcher) { return !isDirectiveDisabled(source, matcher, 0); } private static boolean isDirectiveDisabled(final String source, final Matcher matcher, final int start) { int checkLength = matcher.start() - start; if (checkLength == 0) { return false; } else { Matcher preceedingWhiteSpace = REGEX_ENDS_WITH_WHITESPACE.matcher(source.substring(start, matcher.start())); if (!preceedingWhiteSpace.find()) { return true; } else { int lineCommentEnd = preceedingWhiteSpace.end(1); return lineCommentEnd == matcher.start(); } } } @Nullable public abstract String applyImport(boolean isRelative, String path); public static String injectDefines(final String source, final ShaderDefines defines) { if (defines.isEmpty()) { return source; } else { int versionLineEnd = source.indexOf(10); int injectIndex = versionLineEnd + 1; return source.substring(0, injectIndex) + defines.asSourceDirectives() + "#line 1 0\n" + source.substring(injectIndex); } } @Environment(EnvType.CLIENT) private static final class Context { private int glslVersion; private int sourceId; } }