diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..00a51af --- /dev/null +++ b/.gitattributes @@ -0,0 +1,6 @@ +# +# https://help.github.com/articles/dealing-with-line-endings/ +# +# These are explicitly windows files and should use crlf +*.bat text eol=crlf + diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b18a4dd --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +.gradle +build +out +.idea +*.iml diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..27d6fe5 --- /dev/null +++ b/build.gradle @@ -0,0 +1,90 @@ +import java.nio.charset.StandardCharsets +import java.nio.file.Files +import java.nio.file.StandardOpenOption + +plugins { + // Apply the application plugin to add support for building a CLI application in Java. + id 'application' + id 'antlr' +} + + +group = 'net.shrimpworks' +version = "0.1" + +application { + // Define the main class for the application. + mainClass = 'net.shrimpworks.unreal.scriptbrowser.App' +} + +repositories { + // Use Maven Central for resolving dependencies. + mavenCentral() +} + +dependencies { + antlr 'org.antlr:antlr4:4.10.1' + + implementation 'org.freemarker:freemarker:2.3.31' +} + +jar { + manifest { + attributes( + 'Implementation-Title': project.name, + 'Implementation-Version': project.version, + 'Main-Class': application.mainClassName, + ) + } +} + +task execJar(type: Jar) { + // exclude jar signatures - else it invalidates our fat jar + exclude 'META-INF/*.RSA', 'META-INF/*.SF', 'META-INF/*.DSA' + + duplicatesStrategy = DuplicatesStrategy.EXCLUDE + + archiveClassifier = "exec" + archiveFileName = "${archiveBaseName.get()}-${archiveClassifier.get()}.${archiveExtension.get()}" + from sourceSets.main.output + + dependsOn configurations.runtimeClasspath + + manifest { + attributes( + 'Implementation-Title': project.name, + 'Implementation-Version': project.version, + 'Main-Class': application.mainClassName, + 'Class-Path': configurations.runtimeClasspath.files.collect { it.getName() }.join(' ') + ) + } + + // build the fat executable jar file + from { + configurations.runtimeClasspath.findAll { it.name.endsWith('jar') }.collect { zipTree(it) } + } +} + +/** + * Create list of static files, used for later extraction from the jar to disk. + */ +processResources.doLast { + def wwwStaticDir = projectDir.toPath().resolve('src/main/resources/net/shrimpworks/unreal/scriptbrowser/www/static') + def destFile = buildDir.toPath().resolve('resources/main/net/shrimpworks/unreal/scriptbrowser/www/static.list') + def staticList = new StringBuilder() + files { fileTree(wwwStaticDir).matching { exclude('**/*.xcf') } }.each { + staticList.append(wwwStaticDir.getParent().relativize(it.toPath())).append("\t").append(it.lastModified()).append("\n") + } + Files.write(destFile, staticList.toString().replaceAll("\\\\", "/").getBytes(StandardCharsets.UTF_8), + StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING) +} + +testing { + suites { + // Configure the built-in test suite + test { + // Use JUnit Jupiter test framework + useJUnitJupiter('5.8.1') + } + } +} \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..41d9927 Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..aa991fc --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.2-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..1b6c787 --- /dev/null +++ b/gradlew @@ -0,0 +1,234 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +APP_NAME="Gradle" +APP_BASE_NAME=${0##*/} + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..107acd3 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..cc2df0e --- /dev/null +++ b/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'uscript-browser' diff --git a/src/main/antlr/net/shrimpworks/unreal/unrealscript/UnrealScript.g4 b/src/main/antlr/net/shrimpworks/unreal/unrealscript/UnrealScript.g4 new file mode 100644 index 0000000..b3bf38a --- /dev/null +++ b/src/main/antlr/net/shrimpworks/unreal/unrealscript/UnrealScript.g4 @@ -0,0 +1,448 @@ +grammar UnrealScript; + +@header { +package net.shrimpworks.unreal.unrealscript; +} + +program : classdecl + ( declarations | replicationblock | body)* + ( defaultpropertiesblock )? + ; + +// CLASS +classdecl : CLASS classname ( (EXTENDS | EXPANDS) parentclass )? ( classparams )* ';'; +classparams : constclassparams + | WITHIN packageidentifier + | DEPENDSON '(' packageidentifier ')' + | DLLBIND '(' packageidentifier ')' + | CONFIG ( '(' packageidentifier ')' )? + | HIDECATEGORIES '(' identifierlist ')' + | SHOWCATEGORIES '(' identifierlist ')'; +constclassparams : ABSTRACT + | NATIVE + | NATIVEREPLICATION + | NONATIVEREPLICATION + | SAFEREPLACE + | PEROBJECTCONFIG + | TRANSIENT + | NOEXPORT + | EXPORTSTRUCTS + | COLLAPSECATEGORIES + | DONTCOLLAPSECATEGORIES + | PLACEABLE + | NOTPLACEABLE + | EDITINLINENEW + | NOTEDITINLINENEW + | DYNAMICRECOMPILE; +identifier : IDENT + | ABSTRACT + | ARRAY + | ASSERT + | AUTO + | BOOL + | BYTE + | CASE + | CLASS + | CLIENT + | COERCE + | COLLAPSECATEGORIES + | CONFIG + | CONST + | CONSTRUCTIVE + | DEFAULT + | DEFAULTPROPERTIES + | DELEGATE + | DEPENDSON + | DEPRECATED + | DLLBIND + | DLLIMPORT + | DO + | DONTCOLLAPSECATEGORIES + | DYNAMICRECOMPILE + | EDFINDABLE + | EDITCONST + | EDITINLINE + | EDITINLINENEW + | EDITINLINENOTIFY + | EDITINLINEUSE + | ELSE + | ENUM + | EVENT + | EXEC + | EXPORT + | EXPORTSTRUCTS + | EXTENDS + | EXPANDS + | FINAL + | FLOAT + | FOR + | FOREACH + | FUNCTION + | GLOBALCONFIG + | HIDECATEGORIES + | IF + | IGNORES + | INPUT + | INT + | ITERATOR + | LATENT + | LOCAL + | LOCALIZED + | NAME + | NATIVE + | NATIVEREPLICATION + | NEW + | NOEXPORT + | NONATIVEREPLICATION + | NOTEDITINLINENEW + | NOTPLACEABLE + | OPERATOR + | OPTIONAL + | OUT + | PEROBJECTCONFIG + | PLACEABLE + | POINTER + | POSTOPERATOR + | PREOPERATOR + | PRIVATE + | PROTECTED + | RELIABLE + | REPLICATION + | REPNOTIFY + | RETURN + | SAFEREPLACE + | SELF + | SERVER + | SHOWCATEGORIES + | SIMULATED + | SINGULAR + | SKIP_ + | STATE + | STATIC + | STRING + | STRUCT + | SWITCH + | TRANSIENT + | TRAVEL + | UNRELIABLE + | UNTIL + | VAR + | WHILE + | WITHIN ; +classname : identifier; +parentclass : packageidentifier; +packageidentifier : ( identifier '.' )? classname; + +// DECLARATIONS +declarations : ( constdecl | vardecl | enumdecl | structdecl ) ';'; + +constdecl : CONST identifier '=' expression; +constvalue : ( StringVal | NameVal | IntVal | FloatVal | BoolVal | NoneVal | classval | objectval | VectVal | keyvaluelist ); +classval : CLASS NameVal; +objectval : identifier NameVal; +//enumval : identifier EnumVal; + +vardecl : VAR ( configgroup )? ( varparams )* vartype varidentifier arrayindex? ( ',' varidentifier arrayindex? )*; +arrayindex : ('[' expression ']'); +varparams : CONFIG | CONST | EDITCONST | EXPORT | GLOBALCONFIG | INPUT | + LOCALIZED | NATIVE | PRIVATE | PROTECTED | REPNOTIFY | TRANSIENT | TRAVEL | + EDITINLINE | DEPRECATED | EDFINDABLE | EDITINLINEUSE | EDITINLINENOTIFY; +configgroup : '(' ( identifier )? ')'; +vartype : arraydecl | dynarraydecl | enumdecl | structdecl | classtype | basictype | packageidentifier; +basictype : BYTE | INT | FLOAT | STRING | BOOL | NAME | POINTER; +varidentifier : identifier; + +arraydecl : identifier '[' expression? ']'; +dynarraydecl : ARRAY '<' ( varparams )* (basictype | classtype | packageidentifier) '>'; +classtype : CLASS ('<' packageidentifier '>')?; + +enumdecl : ENUM identifier '{' enumoptions '}'; +enumoptions : enumoption ( ',' enumoption )* ','?; +enumoption : identifier; + +structdecl : STRUCT ( structparams )* identifier ( EXTENDS packageidentifier )? + '{' structbody '}'; +structparams : ( NATIVE | EXPORT | CONSTRUCTIVE); +structbody : ( vardecl ';' )*; + + +// REPLICATION +replicationblock : REPLICATION '{' ( replicationbody )* '}'; +replicationbody : ( RELIABLE | UNRELIABLE ) IF '(' expression ')' + identifier ( ',' identifier )* ';'; + +// BODY +body : ( statedecl | functiondecl )+; + +// State parts +statedecl : ( stateparams )* STATE ('(' ')')? identifier ( configgroup )? ( EXTENDS identifier )? statebody; +statebody : '{' ( stateignore )? ( functiondecl )* statelabels '}'; +stateignore : IGNORES identifier ( ',' identifier )* ';'; +statelabel : identifier ':' ( statement )*; +statelabels : ( statelabel )*; +stateparams : AUTO | SIMULATED; + +// Function parts +functiondecl : ( normalfunc | operatorfunc ); + +normalfunc : ( functionparams )* functiontype ( localtype )? + identifier '(' ( functionargs ( ',' functionargs )* )? ')' + ('{' functionbody '}')? ';'?; +functiontype : FUNCTION | EVENT | DELEGATE; + +functionparams : constfuncparams | NATIVE ( '(' IntVal ')' )?; +constfuncparams : CLIENT | DLLIMPORT | FINAL | ITERATOR | LATENT | SERVER | SIMULATED | SINGULAR | STATIC | + EXEC | PROTECTED | PRIVATE | RELIABLE; + +operatorfunc : ( functionparams )* operatortype ('{' functionbody '}')? ';'?; +operatortype : ( binaryoperator | unaryoperator ); +binaryoperator : OPERATOR '(' IntVal ')' localtype opidentifier + '(' functionargs ',' functionargs ')' ; +unaryoperator : ( PREOPERATOR | POSTOPERATOR ) localtype opidentifier + '(' functionargs ')' ; +opidentifier : identifier | operatornames; +operatornames : '~' | '!' | '@' | '#' | '$' | '%' | '^' | '&' | '*' | + '-' | '=' | '+' | '|' | '\\' | ':' | '<' | '>' | '/' | + '?' | '`' | + '<<' | '>>' | '!=' | '<=' | '>=' | '++' | '--' | '?-' | '+=' | + '-=' | '*=' | '/=' | '&&' | '||' | '^^' | '==' | '**' | + '~=' | '@=' | '$=' | '>>>'; + +functionargs : ( OPTIONAL | OUT | COERCE | SKIP_ )* functionargtype identifier ('=' constvalue)*; +functionargtype : arraydecl | dynarraydecl | classtype | basictype | packageidentifier; +functionbody : ( localdecl )* ( statement )*; +localdecl : LOCAL localtype identifier arrayindex? ( ',' identifier arrayindex? )* ';'; +localtype : arraydecl | dynarraydecl | classtype | basictype | packageidentifier; + +// Code parts +codeblock : ( statement | ( '{' ( statement )* '}' ) ); + +statement : ';' + | assertion + | ifstatement + | switchstatement + | whileloop + | doloop + | foreachloop + | forloop + | returnstatement + | expression ';'; +assertion : ASSERT expression ';'; +ifstatement : IF parExpression codeblock ( ELSE codeblock )?; +switchstatement : SWITCH (parExpression | expression) '{' ( caserule )* ( defaultrule )? '}'; +whileloop : WHILE parExpression codeblock; +doloop : DO codeblock UNTIL '(' expression ')'; +foreachloop : FOREACH qualifiedidentifier '(' expressionList? ')' codeblock; +forloop : FOR '(' identifier '=' expression ';' expression ';' expression ')' codeblock?; +returnstatement : RETURN ( expression )? ';'; + +caserule : CASE expression ':' casecodeblock; +casecodeblock : statement* | ( '{' statement* '}' ); +defaultrule : DEFAULT ':' casecodeblock; + +preoperator : '!' | '~' | '-' | '+' | '++' | '--' ; +postoperator : '++' | '--' ; +operator : '~' | '!' | '@' | '#' | '$' | '%' | '^' | '&' | '*' | + '-' | '+' | '|' | '\\' | ':' | '/' | + '?' | '`' | + '<<' | '>>' | '++' | '--' | '?-' | + '-=' | '*=' | '/=' | '&&' | '||' | '^^' | '**' | + '~=' | '@=' | '$=' | '>>>' ; + +comparator : '!=' | '<=' | '>=' | '==' | '<' | '>' | '~='; + +assignment : '=' | '+=' | '-=' | '*=' | '/=' | '&=' | '|=' | '^=' | + '>>=' | '>>>=' | '<<=' | '%=' | '~=' | '@=' ; + +expression : primary + | expression '.' identifier + | expression '[' expression ']' + | expression '(' expressionList? ')' + | basictype '(' expressionList? ')' + | NEW creator + | expression postoperator + | preoperator expression + | expression operator expression + | expression comparator expression + | expression identifier expression + | expression '?' expression ':' expression + | expression assignment expression; +primary : parExpression | SELF | constvalue | identifier; +creator : ('(' expression? (',' expression)*')')? (identifier | classval); +parExpression : '(' expression ')'; +expressionList : ','? expression (',' expression?)*; + +// DEFAULTPROPERTIES +defaultpropertiesblock : DEFAULTPROPERTIES '{' ( defprop )* '}'; +defprop : defpropidentifier '=' defpropvalue? ';'?; +defpropvalue : ( constvalue | stringlist | floatconst | intconst | keyvaluelist | identifier ); +defpropidentifier : identifier ( ( '(' IntVal ')' ) | ( '[' IntVal ']' ) )?; + +qualifiedidentifier : ( ( CLASS '\'' packageidentifier '\'' '.' DEFAULT '.' identifier ) + | ( ( identifier '.' )* identifier ) + ); +identifierlist : identifier ( ',' identifier )*; + +keyvalue : identifier '=' ( constvalue | floatconst | intconst | identifier ); +keyvaluelist : '(' (keyvalue ( ',' keyvalue )*)? ')'; +stringlist : '(' StringVal ( ',' StringVal )* ')'; +floatconst : '-'? FloatVal; +intconst : '-'? IntVal; + +// LITERALS +StringVal : '"' StringCharacters? '"'; +NameVal : ('\'' NameCharacters? '\'' | NoneVal); +IntVal : (DecimalIntegerLiteral | HexIntegerLiteral); +FloatVal : (Digits '.' Digits? ExponentPart? | Digits ExponentPart | Digits) F?; +BoolVal : (F A L S E) | (T R U E); +NoneVal : N O N E; +VectVal : V E C T '(' FloatVal ',' FloatVal ',' FloatVal ')'; + +// KEYWORDS +ABSTRACT: A B S T R A C T; +ARRAY: A R R A Y; +ASSERT: A S S E R T; +AUTO: A U T O; +BOOL: B O O L; +BYTE: B Y T E; +CASE: C A S E; +CLASS: C L A S S; +CLIENT: C L I E N T; +COERCE: C O E R C E; +COLLAPSECATEGORIES: C O L L A P S E C A T E G O R I E S; +CONFIG: C O N F I G; +CONST: C O N S T; +CONSTRUCTIVE: C O N S T R U C T I V E; +DEFAULT: D E F A U L T; +DEFAULTPROPERTIES: D E F A U L T P R O P E R T I E S; +DELEGATE: D E L E G A T E; +DEPENDSON: D E P E N D S O N; +DEPRECATED: D E P R E C A T E D; +DLLBIND: D L L B I N D; +DLLIMPORT: D L L I M P O R T; +DO: D O; +DONTCOLLAPSECATEGORIES: D O N T C O L L A P S E C A T E G O R I E S; +DYNAMICRECOMPILE: D Y N A M I C R E C O M P I L E; +EDFINDABLE: E D F I N D A B L E; +EDITCONST: E D I T C O N S T; +EDITINLINE: E D I T I N L I N E; +EDITINLINENEW: E D I T I N L I N E N E W; +EDITINLINENOTIFY: E D I T I N L I N E N O T I F Y; +EDITINLINEUSE: E D I T I N L I N E U S E; +ELSE: E L S E; +ENUM: E N U M; +EVENT: E V E N T; +EXEC: E X E C; +EXPORT: E X P O R T; +EXPORTSTRUCTS: E X P O R T S T R U C T S; +EXTENDS: E X T E N D S; +EXPANDS: E X P A N D S; +FINAL: F I N A L; +FLOAT: F L O A T; +FOR: F O R; +FOREACH: F O R E A C H; +FUNCTION: F U N C T I O N; +GLOBALCONFIG: G L O B A L C O N F I G; +HIDECATEGORIES: H I D E C A T E G O R I E S; +IF: I F; +IGNORES: I G N O R E S; +INPUT: I N P U T; +INT: I N T; +ITERATOR: I T E R A T O R; +LATENT: L A T E N T; +LOCAL: L O C A L; +LOCALIZED: L O C A L I Z E D; +NAME: N A M E; +NATIVE: N A T I V E; +NATIVEREPLICATION: N A T I V E R E P L I C A T I O N; +NEW: N E W; +NOEXPORT: N O E X P O R T; +NONATIVEREPLICATION: N O N A T I V E R E P L I C A T I O N; +NOTEDITINLINENEW: N O T E D I T I N L I N E N E W; +NOTPLACEABLE: N O T P L A C E A B L E; +OPERATOR: O P E R A T O R; +OPTIONAL: O P T I O N A L; +OUT: O U T; +PEROBJECTCONFIG: P E R O B J E C T C O N F I G; +PLACEABLE: P L A C E A B L E; +POINTER: P O I N T E R; +POSTOPERATOR: P O S T O P E R A T O R; +PREOPERATOR: P R E O P E R A T O R; +PRIVATE: P R I V A T E; +PROTECTED: P R O T E C T E D; +RELIABLE: R E L I A B L E; +REPLICATION: R E P L I C A T I O N; +REPNOTIFY: R E P N O T I F Y; +RETURN: R E T U R N; +SAFEREPLACE: S A F E R E P L A C E; +SELF: S E L F; +SERVER: S E R V E R; +SHOWCATEGORIES: S H O W C A T E G O R I E S; +SIMULATED: S I M U L A T E D; +SINGULAR: S I N G U L A R; +SKIP_: S K I P; +STATE: S T A T E; +STATIC: S T A T I C; +STRING: S T R I N G; +STRUCT: S T R U C T; +SWITCH: S W I T C H; +TRANSIENT: T R A N S I E N T; +TRAVEL: T R A V E L; +UNRELIABLE: U N R E L I A B L E; +UNTIL: U N T I L; +VAR: V A R; +WHILE: W H I L E; +WITHIN: W I T H I N; + +fragment Alpha : [a-zA-Z_]; +fragment StringCharacters: StringCharacter+; +fragment StringCharacter : EscapeSequence | ~["\\]; +fragment EscapeSequence : '\\' [nt"\\]; +fragment NameCharacters : NameCharacter+; +fragment NameCharacter : ~[']; +fragment DecimalIntegerLiteral : '0' | NonZeroDigit Digits?; +fragment HexIntegerLiteral : '0' [xX] HexDigits; +fragment Digits : Digit+; +fragment Digit : '0' | NonZeroDigit; +fragment NonZeroDigit : [1-9]; +fragment HexDigits : HexDigit+; +fragment HexDigit : [0-9a-fA-F]; +fragment ExponentPart : ExponentIndicator SignedInteger; +fragment ExponentIndicator : [eE]; +fragment SignedInteger : Sign? Digits; +fragment Sign : [+-]; + +fragment A: [aA]; +fragment B: [bB]; +fragment C: [cC]; +fragment D: [dD]; +fragment E: [eE]; +fragment F: [fF]; +fragment G: [gG]; +fragment H: [hH]; +fragment I: [iI]; +fragment J: [jJ]; +fragment K: [kK]; +fragment L: [lL]; +fragment M: [mM]; +fragment N: [nN]; +fragment O: [oO]; +fragment P: [pP]; +fragment Q: [qQ]; +fragment R: [rR]; +fragment S: [sS]; +fragment T: [tT]; +fragment U: [uU]; +fragment V: [vV]; +fragment W: [wW]; +fragment X: [xX]; +fragment Y: [yY]; +fragment Z: [zZ]; + +IDENT : [a-zA-Z_] [a-zA-Z0-9_]*; + +// Whitespaces and comments +WS : [ \t\r\n\u000C]+ -> channel(3); +DIRECTIVE : '#' ~[\r\n]* -> channel(2); +COMMENT : '/*' (COMMENT|.)*? '*/' -> channel(1); +LINE_COMMENT : '//' ~[\r\n]* -> channel(1); diff --git a/src/main/java/net/shrimpworks/unreal/scriptbrowser/App.java b/src/main/java/net/shrimpworks/unreal/scriptbrowser/App.java new file mode 100644 index 0000000..da04e27 --- /dev/null +++ b/src/main/java/net/shrimpworks/unreal/scriptbrowser/App.java @@ -0,0 +1,149 @@ +package net.shrimpworks.unreal.scriptbrowser; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardOpenOption; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.antlr.v4.runtime.BaseErrorListener; +import org.antlr.v4.runtime.CharStreams; +import org.antlr.v4.runtime.CommonTokenStream; +import org.antlr.v4.runtime.RecognitionException; +import org.antlr.v4.runtime.Recognizer; +import org.antlr.v4.runtime.tree.ParseTreeWalker; + +import net.shrimpworks.unreal.scriptbrowser.www.Generator; +import net.shrimpworks.unreal.unrealscript.UnrealScriptLexer; +import net.shrimpworks.unreal.unrealscript.UnrealScriptParser; + +public class App { + + public static class UnrealScriptErrorListener extends BaseErrorListener { + + public static final UnrealScriptErrorListener INSTANCE = new UnrealScriptErrorListener(); + + @Override + public void syntaxError(Recognizer recognizer, Object offendingSymbol, int line, int charPositionInLine, String msg, + RecognitionException e) { + // no-op? + } + } + + public static void main(String[] args) throws IOException { + CLI cli = CLI.parse(Map.of(), args); + + Path srcPath = Paths.get(cli.commands()[0]); + Path outPath = Paths.get(cli.commands()[1]); + + USources sources = new USources(); + + try (Stream paths = Files.list(srcPath)) { + System.err.printf("Loading classes from %s%n", srcPath); + final AtomicInteger classCounter = new AtomicInteger(0); + + paths.map(p -> { + if (!Files.isDirectory(p)) return null; + final UPackage pkg = new UPackage(sources, fileName(p)); + + try (Stream all = Files.walk(p, 3)) { + all.forEach(f -> { + if (!extension(f).equalsIgnoreCase("uc")) return; + + try (InputStream is = Files.newInputStream(f, StandardOpenOption.READ)) { + UnrealScriptLexer lexer = new UnrealScriptLexer(CharStreams.fromStream(is)); + lexer.removeErrorListeners(); + lexer.addErrorListener(UnrealScriptErrorListener.INSTANCE); + CommonTokenStream tokens = new CommonTokenStream(lexer); + UnrealScriptParser parser = new UnrealScriptParser(tokens); + parser.removeErrorListeners(); + parser.addErrorListener(UnrealScriptErrorListener.INSTANCE); + ClassInfoListener listener = new ClassInfoListener(f, pkg); + ParseTreeWalker.DEFAULT.walk(listener, parser.program()); + } catch (IOException e) { + throw new RuntimeException(e); + } + }); + } catch (IOException e) { + throw new RuntimeException(e); + } + classCounter.addAndGet(pkg.classes.size()); + return pkg; + }) + .filter(Objects::nonNull) + .filter(p -> !p.classes.isEmpty()) + .forEach(sources::addPackage); + + System.err.printf("Loaded %d classes in %d packages%n", classCounter.get(), sources.packages.size()); + } + + Generator.offloadStatic("static.list", outPath); + + System.err.println("Generating source pages"); + sources.packages.values().forEach(pkg -> pkg.classes.values().parallelStream().forEach(e -> Generator.src(e, outPath))); + + System.err.println("Generating navigation tree"); + Generator.tree(children(sources, null), outPath); +// printTree(children(sources, null), 0); + + System.err.println("Done"); + } + + public static List children(USources sources, UClass parent) { + return sources.packages.values().stream() + .flatMap(p -> p.classes.values().stream()) + .filter(c -> { + if (parent == null && c.parent == null) return true; + else return parent != null && parent.name.equalsIgnoreCase(c.parent); + }) + .sorted() + .map(c -> new UClassNode(c, children(sources, c))) + .collect(Collectors.toList()); + } + + public static void printTree(List nodes, int depth) { + for (UClassNode n : nodes) { + System.out.printf("%s%s%n", " ".repeat(depth), n.clazz.name); + printTree(n.children, depth + 2); + } + } + + public static String extension(Path path) { + return extension(path.toString()); + } + + public static String extension(String path) { + return path.substring(path.lastIndexOf(".") + 1); + } + + public static String fileName(Path path) { + return fileName(path.toString()); + } + + public static String fileName(String path) { + String tmp = path.replaceAll("\\\\", "/"); + return tmp.substring(Math.max(0, tmp.lastIndexOf("/") + 1)); + } + + public static String filePath(String path) { + String tmp = path.replaceAll("\\\\", "/"); + return tmp.substring(0, Math.max(0, tmp.lastIndexOf("/"))); + } + + public static String plainName(Path path) { + return plainName(path.toString()); + } + + public static String plainName(String path) { + String tmp = fileName(path); + return tmp.substring(0, tmp.lastIndexOf(".")).replaceAll("/", "").trim().replaceAll("[^\\x20-\\x7E]", ""); + } + +} diff --git a/src/main/java/net/shrimpworks/unreal/scriptbrowser/CLI.java b/src/main/java/net/shrimpworks/unreal/scriptbrowser/CLI.java new file mode 100644 index 0000000..e395d57 --- /dev/null +++ b/src/main/java/net/shrimpworks/unreal/scriptbrowser/CLI.java @@ -0,0 +1,78 @@ +package net.shrimpworks.unreal.scriptbrowser; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class CLI { + + private static final String OPTION_PATTERN = "--([a-zA-Z0-9-_]+)=(.+)?"; + + private static final String PROPERTIES = ".uscript-browser.conf"; + + private final String[] commands; + private final Map options; + + public CLI(String[] commands, Map options) { + this.commands = commands; + this.options = options; + } + + public static CLI parse(Map defOptions, String... args) { + final List commands = new ArrayList<>(); + final Map props = new HashMap<>(); + + // populate default options + props.putAll(defOptions); + + Path confFile = Paths.get(PROPERTIES); + if (!Files.exists(confFile)) confFile = Paths.get(System.getProperty("user.home")).resolve(PROPERTIES); + if (Files.exists(confFile)) { + try { + Properties fileProps = new Properties(); + fileProps.load(Files.newInputStream(confFile)); + for (String p : fileProps.stringPropertyNames()) { + props.put(p, fileProps.getProperty(p)); + } + } catch (IOException e) { + System.err.println("Failed to read properties from file " + confFile + ": " + e.toString()); + } + } + + Pattern optPattern = Pattern.compile(OPTION_PATTERN); + + for (String arg : args) { + Matcher optMatcher = optPattern.matcher(arg); + + if (optMatcher.matches()) { + props.put(optMatcher.group(1), optMatcher.group(2) == null ? "" : optMatcher.group(2)); + } else { + commands.add(arg); + } + } + + return new CLI(commands.toArray(new String[0]), props); + } + + public String option(String key, String defaultValue) { + return options.getOrDefault(key, defaultValue); + } + + public void putOption(String key, String value) { + + options.put(key, value); + } + + public String[] commands() { + return commands; + } + +} diff --git a/src/main/java/net/shrimpworks/unreal/scriptbrowser/ClassFormatterListener.java b/src/main/java/net/shrimpworks/unreal/scriptbrowser/ClassFormatterListener.java new file mode 100644 index 0000000..a63d952 --- /dev/null +++ b/src/main/java/net/shrimpworks/unreal/scriptbrowser/ClassFormatterListener.java @@ -0,0 +1,289 @@ +package net.shrimpworks.unreal.scriptbrowser; + +import java.util.HashSet; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; + +import org.antlr.v4.runtime.CommonTokenStream; +import org.antlr.v4.runtime.ParserRuleContext; +import org.antlr.v4.runtime.Token; +import org.antlr.v4.runtime.TokenStreamRewriter; + +import net.shrimpworks.unreal.unrealscript.UnrealScriptBaseListener; +import net.shrimpworks.unreal.unrealscript.UnrealScriptParser; + +public class ClassFormatterListener extends UnrealScriptBaseListener { + + private final UClass clazz; + + private final CommonTokenStream tokens; + private final TokenStreamRewriter rewriter; + + // stateful processing + private final Set locals = new HashSet<>(); + private boolean inFunction = false; + + public ClassFormatterListener(UClass clazz, CommonTokenStream tokens) { + this.clazz = clazz; + this.tokens = tokens; + this.rewriter = new TokenStreamRewriter(tokens); + } + + public String getTranslatedText() { + return rewriter.getText(); + } + + @Override + public void enterProgram(UnrealScriptParser.ProgramContext ctx) { + List comments = tokens.getHiddenTokensToLeft(ctx.getStart().getTokenIndex(), 1); + if (comments != null) { + for (Token comment : comments) { + tokenStyle(comment, "cmt"); + } + } + } + + @Override + public void enterEveryRule(ParserRuleContext ctx) { + List comments = tokens.getHiddenTokensToRight(ctx.getStop().getTokenIndex(), 1); + if (comments != null) { + for (Token comment : comments) { + tokenStyle(comment, "cmt"); + } + } + List directives = tokens.getHiddenTokensToRight(ctx.getStop().getTokenIndex(), 2); + if (directives != null) { + for (Token directive : directives) { + tokenStyle(directive, "dir"); + } + } + } + + @Override + public void enterClassname(UnrealScriptParser.ClassnameContext ctx) { +// tokenStyle(ctx, "cls"); + } + + @Override + public void enterClassdecl(UnrealScriptParser.ClassdeclContext ctx) { + tokenStyle(ctx.CLASS().getSymbol(), "kw"); + ctx.classparams().forEach(p -> tokenStyle(p, "kw")); + if (ctx.EXPANDS() != null) tokenStyle(ctx.EXPANDS().getSymbol(), "kw"); + if (ctx.EXTENDS() != null) tokenStyle(ctx.EXTENDS().getSymbol(), "kw"); + } + + @Override + public void enterFunctiontype(UnrealScriptParser.FunctiontypeContext ctx) { + tokenStyle(ctx, "kw"); + } + + @Override + public void enterBasictype(UnrealScriptParser.BasictypeContext ctx) { + tokenStyle(ctx, "basic"); + } + + @Override + public void enterConstvalue(UnrealScriptParser.ConstvalueContext ctx) { + if (ctx.BoolVal() != null) tokenStyle(ctx, "bool"); + else if (ctx.FloatVal() != null) tokenStyle(ctx, "num"); + else if (ctx.IntVal() != null) tokenStyle(ctx, "num"); + else if (ctx.StringVal() != null) tokenStyle(ctx, "str"); + else if (ctx.NameVal() != null) tokenStyle(ctx, "name"); + else if (ctx.NoneVal() != null) tokenStyle(ctx, "none"); + } + + @Override + public void enterOperator(UnrealScriptParser.OperatorContext ctx) { + tokenStyle(ctx, "op"); + } + + @Override + public void enterComparator(UnrealScriptParser.ComparatorContext ctx) { + tokenStyle(ctx, "op"); + } + + @Override + public void enterAssignment(UnrealScriptParser.AssignmentContext ctx) { + tokenStyle(ctx, "op"); + } + + @Override + public void enterVardecl(UnrealScriptParser.VardeclContext ctx) { + tokenStyle(ctx.VAR().getSymbol(), "kw"); + ctx.varparams().forEach(p -> tokenStyle(p, "kw")); + ctx.varidentifier().forEach(p -> { + tokenStyle(p, "var"); + tokenAnchor(p, "V_" + p.identifier().getText().toLowerCase()); + }); + + if (ctx.vartype().classtype() != null) linkClass(ctx.vartype().classtype()); +// else if (ctx.vartype().basictype() != null) type = ctx.vartype().basictype().getText(); +// else if (ctx.vartype().enumdecl() != null) type = ctx.vartype().enumdecl().identifier().getText(); +// else if (ctx.vartype().arraydecl() != null) type = ctx.vartype().arraydecl().identifier().getText(); +// else if (ctx.vartype().dynarraydecl() != null) { +// if (ctx.vartype().dynarraydecl().basictype() != null) type = ctx.vartype().dynarraydecl().basictype().getText(); +// else if (ctx.vartype().dynarraydecl().classtype() != null) type = ctx.vartype().dynarraydecl().classtype().getText(); +// } + } + + private void linkClass(UnrealScriptParser.ClasstypeContext ctx) { + Optional pkg = Optional.empty(); + if (ctx.packageidentifier().identifier() != null) { + pkg = clazz.pkg.sourceSet.pkg(ctx.packageidentifier().identifier().getText()); + } + + + + pkg + .flatMap(p -> p.clazz(ctx.packageidentifier().classname().identifier().getText())) + .ifPresentOrElse( + cls -> tokenLink(ctx, String.format("../%s/%s.html", cls.pkg.name, cls.name)), + () -> clazz.pkg.sourceSet.clazz(ctx.packageidentifier().classname().identifier().getText())); + + + pkg + .flatMap(p -> p.clazz(ctx.packageidentifier().classname().identifier().getText())) + .ifPresentOrElse( + cls -> tokenLink(ctx, String.format("../%s/%s.html", cls.pkg.name, cls.name)), + () -> clazz.pkg.sourceSet.clazz(ctx.packageidentifier().classname().identifier().getText())); + +// clazz.parent() +// .flatMap(p -> p.functions().entrySet().stream() +// .filter(kv -> kv.getValue().contains(ctx.getText())) +// .findFirst() +// ) +// .ifPresent(f -> tokenLink( +// tokens.get(ctx.start.getTokenIndex() - 2), ctx.stop, +// String.format("../%s/%s.html#F_%s", f.getKey().pkg.name, f.getKey().name, ctx.getText().toLowerCase()) +// )); +// +// tokenLink( +// tokens.get(ctx.start.getTokenIndex() - 2), ctx.stop, +// String.format("../%s/%s.html#F_%s", f.getKey().pkg.name, f.getKey().name, ctx.getText().toLowerCase()) +// ) + } + + @Override + public void enterLocaldecl(UnrealScriptParser.LocaldeclContext ctx) { + tokenStyle(ctx.LOCAL().getSymbol(), "kw"); + ctx.identifier().forEach(p -> locals.add(p.getText())); + } + + @Override + public void enterFunctionbody(UnrealScriptParser.FunctionbodyContext ctx) { + inFunction = true; + } + + @Override + public void exitFunctionbody(UnrealScriptParser.FunctionbodyContext ctx) { + inFunction = false; + locals.clear(); + } + + @Override + public void enterIdentifier(UnrealScriptParser.IdentifierContext ctx) { + if (!inFunction) return; + + if (!Objects.equals(tokens.get(ctx.start.getTokenIndex() - 1).getText(), ".") && locals.contains(ctx.getText())) { + tokenStyle(ctx, "lcl"); + } else if (!Objects.equals(tokens.get(ctx.start.getTokenIndex() - 1).getText(), ".")) { + if (clazz.variables().values().stream().anyMatch(v -> v.contains(ctx.getText()))) { + tokenStyle(ctx, "var"); + } + } else if (Objects.equals(tokens.get(ctx.stop.getTokenIndex() + 1).getText(), "(")) { + if (Objects.equals(tokens.get(ctx.start.getTokenIndex() - 2).getText().toLowerCase(), "super")) { + clazz.parent() + .flatMap(p -> p.functions().entrySet().stream() + .filter(kv -> kv.getValue().contains(ctx.getText())) + .findFirst() + ) + .ifPresent(f -> tokenLink( + tokens.get(ctx.start.getTokenIndex() - 2), ctx.stop, + String.format("../%s/%s.html#F_%s", f.getKey().pkg.name, f.getKey().name, ctx.getText().toLowerCase()) + )); + } else { + if (clazz.functions().values().stream().anyMatch(v -> v.contains(ctx.getText()))) { + tokenLink(ctx, "#F_" + ctx.getText().toLowerCase()); + } + } + } + } + + @Override + public void enterEnumdecl(UnrealScriptParser.EnumdeclContext ctx) { + tokenStyle(ctx.ENUM().getSymbol(), "kw"); + tokenStyle(ctx.identifier(), "ident"); + } + + @Override + public void enterConstdecl(UnrealScriptParser.ConstdeclContext ctx) { + tokenStyle(ctx.CONST().getSymbol(), "kw"); + tokenStyle(ctx.identifier(), "ident"); + } + + @Override + public void enterStructdecl(UnrealScriptParser.StructdeclContext ctx) { + tokenStyle(ctx.STRUCT().getSymbol(), "kw"); + tokenStyle(ctx.identifier(), "ident"); + if (ctx.EXTENDS() != null) tokenStyle(ctx.EXTENDS().getSymbol(), "kw"); + ctx.structparams().forEach(p -> tokenStyle(p, "kw")); + } + + @Override + public void enterFunctiondecl(UnrealScriptParser.FunctiondeclContext ctx) { + if (ctx.normalfunc() != null) { + tokenAnchor(ctx.normalfunc().identifier(), "F_" + ctx.normalfunc().identifier().getText().toLowerCase()); + tokenStyle(ctx.normalfunc().identifier(), "ident"); + ctx.normalfunc().functionparams().forEach(p -> tokenStyle(p, "kw")); + } else if (ctx.operatorfunc() != null) { + ctx.operatorfunc().functionparams().forEach(p -> tokenStyle(p, "kw")); + tokenStyle(ctx.operatorfunc().operatortype(), "kw"); + } + } + + @Override + public void enterReplicationblock(UnrealScriptParser.ReplicationblockContext ctx) { + tokenStyle(ctx.REPLICATION().getSymbol(), "kw"); + } + + @Override + public void enterStatement(UnrealScriptParser.StatementContext ctx) { + if (ctx.ifstatement() != null) { + tokenStyle(ctx.ifstatement().IF().getSymbol(), "kw"); + if (ctx.ifstatement().ELSE() != null) tokenStyle(ctx.ifstatement().ELSE().getSymbol(), "kw"); + } else if (ctx.whileloop() != null) tokenStyle(ctx.whileloop().WHILE().getSymbol(), "kw"); + else if (ctx.doloop() != null) tokenStyle(ctx.doloop().DO().getSymbol(), "kw"); + else if (ctx.forloop() != null) tokenStyle(ctx.forloop().FOR().getSymbol(), "kw"); + else if (ctx.foreachloop() != null) tokenStyle(ctx.foreachloop().FOREACH().getSymbol(), "kw"); + else if (ctx.switchstatement() != null) tokenStyle(ctx.switchstatement().SWITCH().getSymbol(), "kw"); + else if (ctx.assertion() != null) tokenStyle(ctx.assertion().ASSERT().getSymbol(), "kw"); + else if (ctx.returnstatement() != null) tokenStyle(ctx.returnstatement().RETURN().getSymbol(), "kw"); + } + + private void tokenAnchor(ParserRuleContext ctx, String name) { + rewriter.insertBefore(ctx.start, "«a id=\"" + name + "\"»"); + rewriter.insertAfter(ctx.stop, "«/a»"); + } + + private void tokenLink(Token start, Token stop, String path) { + rewriter.insertBefore(start, "«a href=\"" + path + "\"»"); + rewriter.insertAfter(stop, "«/a»"); + } + + private void tokenLink(ParserRuleContext ctx, String path) { + rewriter.insertBefore(ctx.start, "«a href=\"" + path + "\"»"); + rewriter.insertAfter(ctx.stop, "«/a»"); + } + + private void tokenStyle(ParserRuleContext ctx, String style) { + rewriter.insertBefore(ctx.start, "«span class=\"" + style + "\"»"); + rewriter.insertAfter(ctx.stop, "«/span»"); + } + + private void tokenStyle(Token token, String style) { + rewriter.insertBefore(token, "«span class=\"" + style + "\"»"); + rewriter.insertAfter(token, "«/span»"); + } +} diff --git a/src/main/java/net/shrimpworks/unreal/scriptbrowser/ClassInfoListener.java b/src/main/java/net/shrimpworks/unreal/scriptbrowser/ClassInfoListener.java new file mode 100644 index 0000000..3f2836b --- /dev/null +++ b/src/main/java/net/shrimpworks/unreal/scriptbrowser/ClassInfoListener.java @@ -0,0 +1,69 @@ +package net.shrimpworks.unreal.scriptbrowser; + +import java.nio.file.Path; +import java.util.stream.Collectors; + +import org.antlr.v4.runtime.RuleContext; + +import net.shrimpworks.unreal.unrealscript.UnrealScriptBaseListener; +import net.shrimpworks.unreal.unrealscript.UnrealScriptParser; + +class ClassInfoListener extends UnrealScriptBaseListener { + + private final Path sourceFile; + private final UPackage pkg; + + private UClass clazz; + + public ClassInfoListener(Path sourceFile, UPackage pkg) { + this.sourceFile = sourceFile; + this.pkg = pkg; + } + + @Override + public void enterClassdecl(UnrealScriptParser.ClassdeclContext ctx) { + clazz = new UClass( + sourceFile, + pkg, + ctx.classname().getText(), + ctx.parentclass() == null ? null : ctx.parentclass().getText() + ); + pkg.addClass(clazz); + + clazz.params.addAll(ctx.classparams().stream().map(RuleContext::getText).collect(Collectors.toSet())); + } + + @Override + public void enterVardecl(UnrealScriptParser.VardeclContext ctx) { + String type = null; + if (ctx.vartype().classtype() != null) type = ctx.vartype().classtype().getText(); + else if (ctx.vartype().basictype() != null) type = ctx.vartype().basictype().getText(); + else if (ctx.vartype().enumdecl() != null) type = ctx.vartype().enumdecl().identifier().getText(); + else if (ctx.vartype().arraydecl() != null) type = ctx.vartype().arraydecl().identifier().getText(); + else if (ctx.vartype().dynarraydecl() != null) { + if (ctx.vartype().dynarraydecl().basictype() != null) type = ctx.vartype().dynarraydecl().basictype().getText(); + else if (ctx.vartype().dynarraydecl().classtype() != null) type = ctx.vartype().dynarraydecl().classtype().getText(); + } + + for (UnrealScriptParser.VaridentifierContext varItent : ctx.varidentifier()) { + clazz.addMember(UClass.UMember.UMemberKind.VARIABLE, type, varItent.getText()); + } + } + + @Override + public void enterNormalfunc(UnrealScriptParser.NormalfuncContext ctx) { + String type = null; + if (ctx.localtype() != null) type = ctx.localtype().getText(); + clazz.addMember(UClass.UMember.UMemberKind.FUNCTION, type, ctx.identifier().getText()); + } + + @Override + public void enterStructdecl(UnrealScriptParser.StructdeclContext ctx) { + clazz.addMember(UClass.UMember.UMemberKind.STRUCT, null, ctx.identifier().getText()); + } + + @Override + public void enterEnumdecl(UnrealScriptParser.EnumdeclContext ctx) { + clazz.addMember(UClass.UMember.UMemberKind.ENUM, null, ctx.identifier().getText()); + } +} diff --git a/src/main/java/net/shrimpworks/unreal/scriptbrowser/UClass.java b/src/main/java/net/shrimpworks/unreal/scriptbrowser/UClass.java new file mode 100644 index 0000000..7434b40 --- /dev/null +++ b/src/main/java/net/shrimpworks/unreal/scriptbrowser/UClass.java @@ -0,0 +1,86 @@ +package net.shrimpworks.unreal.scriptbrowser; + +import java.nio.file.Path; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +public class UClass implements Comparable { + + public final Path path; + public final UPackage pkg; + public final String name; + public final String parent; + public final Set params; + public final Set members; + + public UClass(Path path, UPackage pkg, String name, String parent) { + this.path = path; + this.pkg = pkg; + this.name = name; + this.parent = parent; + + this.params = new HashSet<>(); + this.members = new HashSet<>(); + } + + public void addMember(UMember.UMemberKind kind, String type, String name) { + members.add(new UMember(kind, type, name)); + } + + @Override + public String toString() { + if (parent != null) return String.format("class %s extends %s", name, parent); + else return String.format("class %s", name); + } + + @Override + public int compareTo(UClass o) { + return name.compareToIgnoreCase(o.name); + } + + public Optional parent() { + if (parent == null) return Optional.empty(); + + return pkg.clazz(parent) + .or(() -> pkg.sourceSet.clazz(parent)); + } + + public Map> variables() { + return members(UMember.UMemberKind.VARIABLE); + } + + public Map> functions() { + return members(UMember.UMemberKind.FUNCTION); + } + + private Map> members(UMember.UMemberKind kind) { + Map> mems = new HashMap<>(); + mems.put(this, members.stream().filter(m -> m.kind == kind).map(m -> m.name).collect(Collectors.toSet())); + parent().ifPresent(p -> mems.putAll(p.members(kind))); + return mems; + } + + public static class UMember { + + public static enum UMemberKind { + FUNCTION, + VARIABLE, + STRUCT, + ENUM + } + + public final UMemberKind kind; + public final String type; + public final String name; + + public UMember(UMemberKind kind, String type, String name) { + this.kind = kind; + this.type = type; + this.name = name; + } + } +} diff --git a/src/main/java/net/shrimpworks/unreal/scriptbrowser/UClassNode.java b/src/main/java/net/shrimpworks/unreal/scriptbrowser/UClassNode.java new file mode 100644 index 0000000..37e39b8 --- /dev/null +++ b/src/main/java/net/shrimpworks/unreal/scriptbrowser/UClassNode.java @@ -0,0 +1,15 @@ +package net.shrimpworks.unreal.scriptbrowser; + +import java.util.List; + +public class UClassNode { + + public final UClass clazz; + public final List children; + + public UClassNode(UClass clazz, List children) { + this.clazz = clazz; + this.children = children; + } + +} diff --git a/src/main/java/net/shrimpworks/unreal/scriptbrowser/UPackage.java b/src/main/java/net/shrimpworks/unreal/scriptbrowser/UPackage.java new file mode 100644 index 0000000..3b7c5d8 --- /dev/null +++ b/src/main/java/net/shrimpworks/unreal/scriptbrowser/UPackage.java @@ -0,0 +1,36 @@ +package net.shrimpworks.unreal.scriptbrowser; + +import java.util.Map; +import java.util.Optional; +import java.util.TreeMap; + +public class UPackage implements Comparable { + + public final USources sourceSet; + public final String name; + public final Map classes; + + public UPackage(USources sourceSet, String name) { + this.sourceSet = sourceSet; + this.name = name; + this.classes = new TreeMap<>(); + } + + public void addClass(UClass clazz) { + classes.put(clazz.name.toLowerCase(), clazz); + } + + public Optional clazz(String name) { + return Optional.ofNullable(classes.get(name.toLowerCase())); + } + + @Override + public String toString() { + return String.format("package %s", name); + } + + @Override + public int compareTo(UPackage o) { + return name.compareToIgnoreCase(o.name); + } +} diff --git a/src/main/java/net/shrimpworks/unreal/scriptbrowser/USources.java b/src/main/java/net/shrimpworks/unreal/scriptbrowser/USources.java new file mode 100644 index 0000000..f71220f --- /dev/null +++ b/src/main/java/net/shrimpworks/unreal/scriptbrowser/USources.java @@ -0,0 +1,40 @@ +package net.shrimpworks.unreal.scriptbrowser; + +import java.util.Map; +import java.util.Optional; +import java.util.TreeMap; + +public class USources { + + public final Map packages; + + public USources() { + this.packages = new TreeMap<>(); + } + + public void addPackage(UPackage pkg) { + packages.put(pkg.name.toLowerCase(), pkg); + } + + public Optional pkg(String name) { + if (name == null) return Optional.empty(); + return Optional.ofNullable(packages.get(name.toLowerCase())); + } + + public Optional clazz(String name) { + return clazz(name, (String)null); + } + + public Optional clazz(String name, UPackage pkg) { + if (pkg == null) return clazz(name, (String)null); + return pkg.clazz(name) + .or(() -> packages.values().stream().flatMap(p -> p.clazz(name).stream()).findFirst()); + } + + public Optional clazz(String name, String pkg) { + return pkg(pkg) + .flatMap(p -> p.clazz(name)) + .or(() -> packages.values().stream().flatMap(p -> p.clazz(name).stream()).findFirst()); + } + +} diff --git a/src/main/java/net/shrimpworks/unreal/scriptbrowser/www/Generator.java b/src/main/java/net/shrimpworks/unreal/scriptbrowser/www/Generator.java new file mode 100644 index 0000000..4a40d44 --- /dev/null +++ b/src/main/java/net/shrimpworks/unreal/scriptbrowser/www/Generator.java @@ -0,0 +1,153 @@ +package net.shrimpworks.unreal.scriptbrowser.www; + +import java.beans.IntrospectionException; +import java.beans.PropertyDescriptor; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Writer; +import java.nio.channels.Channels; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.nio.file.StandardOpenOption; +import java.nio.file.attribute.FileTime; +import java.util.Collection; +import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; + +import freemarker.core.HTMLOutputFormat; +import freemarker.template.Configuration; +import freemarker.template.DefaultObjectWrapper; +import freemarker.template.Template; +import freemarker.template.TemplateException; +import org.antlr.v4.runtime.CharStreams; +import org.antlr.v4.runtime.CommonTokenStream; +import org.antlr.v4.runtime.tree.ParseTreeWalker; + +import net.shrimpworks.unreal.scriptbrowser.App; +import net.shrimpworks.unreal.scriptbrowser.ClassFormatterListener; +import net.shrimpworks.unreal.scriptbrowser.UClass; +import net.shrimpworks.unreal.scriptbrowser.UClassNode; +import net.shrimpworks.unreal.unrealscript.UnrealScriptLexer; +import net.shrimpworks.unreal.unrealscript.UnrealScriptParser; + +public class Generator { + + private static final Configuration TPL_CONFIG = new Configuration(Configuration.VERSION_2_3_31); + + static { + TPL_CONFIG.setClassForTemplateLoading(Generator.class, ""); + DefaultObjectWrapper ow = new DefaultObjectWrapper(TPL_CONFIG.getIncompatibleImprovements()); + ow.setExposeFields(true); + ow.setMethodAppearanceFineTuner((in, out) -> { + out.setReplaceExistingProperty(false); + out.setMethodShadowsProperty(false); + try { + in.getContainingClass().getField(in.getMethod().getName()); + // this did not throw a NoSuchFieldException, so we know there is a property named after the method - do not expose the method + out.setExposeMethodAs(null); + } catch (NoSuchFieldException e) { + try { + // we got a NoSuchFieldException, which means there's no property named after the method, so we can expose it + out.setExposeAsProperty( + new PropertyDescriptor(in.getMethod().getName(), in.getContainingClass(), in.getMethod().getName(), null) + ); + } catch (IntrospectionException ex) { + // pass + } + // pass + } + }); + TPL_CONFIG.setObjectWrapper(ow); + TPL_CONFIG.setOutputEncoding(StandardCharsets.UTF_8.name()); + TPL_CONFIG.setOutputFormat(HTMLOutputFormat.INSTANCE); + } + + public static void tree(Collection nodes, Path outPath) { + try { + Template tpl = TPL_CONFIG.getTemplate("tree.ftl"); + try (Writer writer = Channels.newWriter( + Files.newByteChannel( + outPath.resolve("tree.html"), + StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING + ), StandardCharsets.UTF_8)) { + tpl.process(Map.of( + "nodes", nodes + ), writer); + } + } catch (IOException | TemplateException e) { + throw new RuntimeException(e); + } + } + + public static void src(UClass clazz, Path outPath) { + try (InputStream is = Files.newInputStream(clazz.path, StandardOpenOption.READ)) { + final Path htmlOut = outPath.resolve(clazz.pkg.name); + try { + if (!Files.isDirectory(htmlOut)) Files.createDirectories(htmlOut); + } catch (IOException e) { + // oops + } + + Template tpl = TPL_CONFIG.getTemplate("script.ftl"); + + UnrealScriptLexer lexer = new UnrealScriptLexer(CharStreams.fromStream(is)); + lexer.removeErrorListeners(); + lexer.addErrorListener(App.UnrealScriptErrorListener.INSTANCE); + CommonTokenStream tokens = new CommonTokenStream(lexer); + UnrealScriptParser parser = new UnrealScriptParser(tokens); + parser.removeErrorListeners(); + parser.addErrorListener(App.UnrealScriptErrorListener.INSTANCE); + ClassFormatterListener listener = new ClassFormatterListener(clazz, tokens); + ParseTreeWalker.DEFAULT.walk(listener, parser.program()); + + StringBuilder sb = new StringBuilder(); + AtomicInteger lineCount = new AtomicInteger(0); + listener.getTranslatedText() + .lines() + .forEach(l -> { + lineCount.incrementAndGet(); + sb.append(l.isBlank() ? " " : l.replaceAll("<", "<").replaceAll(">", ">") + .replaceAll("«", "<").replaceAll("»", ">")) + .append("\n"); + }); + try (Writer writer = Channels.newWriter( + Files.newByteChannel( + htmlOut.resolve(clazz.name + ".html"), + StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING + ), StandardCharsets.UTF_8)) { + tpl.process(Map.of( + "clazz", clazz, + "source", sb.toString(), + "lines", lineCount.intValue() + ), writer); + } + + } catch (IOException | TemplateException e) { + throw new RuntimeException(e); + } + + } + + public static void offloadStatic(String resourceList, Path destination) throws IOException { + try (InputStream in = Generator.class.getResourceAsStream(resourceList); + BufferedReader br = new BufferedReader(new InputStreamReader(in))) { + + String line; + while ((line = br.readLine()) != null) { + String[] nameAndDate = line.split("\t"); + String resource = nameAndDate[0]; + long lastModified = Long.parseLong(nameAndDate[1]); + try (InputStream res = Generator.class.getResourceAsStream(resource)) { + Path destPath = destination.resolve(resource); + Files.createDirectories(destPath.getParent()); + Files.copy(res, destPath, StandardCopyOption.REPLACE_EXISTING); + Files.setLastModifiedTime(destPath, FileTime.fromMillis(lastModified)); + } + } + } + } +} diff --git a/src/main/resources/net/shrimpworks/unreal/scriptbrowser/www/script.ftl b/src/main/resources/net/shrimpworks/unreal/scriptbrowser/www/script.ftl new file mode 100644 index 0000000..6dcd54d --- /dev/null +++ b/src/main/resources/net/shrimpworks/unreal/scriptbrowser/www/script.ftl @@ -0,0 +1,24 @@ + + + + ${clazz.pkg.name}.${clazz.name} + + + + + +

${clazz.pkg.name} / ${clazz.name}

+
+
+ <#list 1..lines as line> +
${line?c}
+ +
+ + <#outputformat "plainText"> +
${source}
+ +
+ + + \ No newline at end of file diff --git a/src/main/resources/net/shrimpworks/unreal/scriptbrowser/www/static/solarized-light.css b/src/main/resources/net/shrimpworks/unreal/scriptbrowser/www/static/solarized-light.css new file mode 100644 index 0000000..2601c71 --- /dev/null +++ b/src/main/resources/net/shrimpworks/unreal/scriptbrowser/www/static/solarized-light.css @@ -0,0 +1,76 @@ +#source { + background-color: #fdf6e3; +} + +header { + background-color: #fdf6e3; +} + +#script { + background-color: #fdf6e3; +} + +#lines { + background-color: #eee8d5; + border: none; +} + +/* Code Formatting */ + +#script pre { + color: #586e75; +} + +pre .cmt { + color: #839496; +} + +pre .dir { + color: #839496; + font-weight: bold; +} + +pre .cls { + font-weight: bold; +} + +pre .op { + color: #cb4b16; + font-weight: normal; +} + +pre .kw { + color: #859900; +} +pre .basic { + color: #b58900; +} + +pre .num { + color: #cb4b16; +} + +pre .bool { + color: #cb4b16; +} + +pre .name { + color: #2aa198; +} + +pre .none { + color: #2aa198; +} + +pre .str { + color: #2aa198; +} + +pre .lcl { + color: #268bd2; +} + +pre .var { + color: #268bd2; + font-weight: bold; +} diff --git a/src/main/resources/net/shrimpworks/unreal/scriptbrowser/www/static/style.css b/src/main/resources/net/shrimpworks/unreal/scriptbrowser/www/static/style.css new file mode 100644 index 0000000..48790ec --- /dev/null +++ b/src/main/resources/net/shrimpworks/unreal/scriptbrowser/www/static/style.css @@ -0,0 +1,158 @@ +body { + margin: 0; + font-family: sans-serif; + text-rendering: optimizeLegibility; +} + +/* Index layout */ + +#page { + display: grid; + grid-template: "header source" + "tree source"; + grid-template-columns: 400px 1fr; + grid-template-rows: 40px 1fr; + grid-gap: 0px; + width: 100vw; + height: 100vh; +} + +#tree { + grid-area: tree; + overflow: scroll; + height: 100%; + width: 100%; +} + +header { + grid-area: header; + position: sticky; + top: 0; +} + +#source { + grid-area: source; + width: 100%; + height: 100%; + border: none; +} + +/* Tree Style */ + +#tree .node a { + /*display: block;*/ + cursor: pointer; +} + +#tree .node .pad { + display: inline-block; + width: 20px; +} + +#tree .node .children { + display: none; +} +#tree .node .children.open { + display: inherit; +} + +/* Source View Layout */ + +h1 { + margin: 0; +} + +#script { + display: grid; + grid-template: "lines source"; + grid-template-columns: 50px 1fr; + grid-gap: 10px; + + width: 100vw; + min-height: 100vh; +} + +#lines { + grid-area: lines; + min-height: 100%; + + font-family: monospace; + text-align: right; + margin: 0; + padding: 5px; + line-height: 1.25em; + + color: gray; + background-color: #e9e9e9; +} + +#script pre { + grid-area: source; + + tab-size: 2; + margin: 0; + padding: 5px; + line-height: 1.25em; + + color: #333; +} + +/* Code Formatting */ + +pre .cmt { + color: gray; +} + +pre .dir { + color: darkgreen; +} + +pre .cls { + font-weight: bold; +} + +pre .ident { + font-weight: bold; +} + +pre .op { + color: brown; + font-weight: bold; +} + +pre .kw { + color: black; +} + +pre .basic { + color: crimson; +} + +pre .num { + color: navy; +} + +pre .bool { + color: navy; +} + +pre .name { + color: darkmagenta; +} + +pre .none { + color: darkmagenta; +} + +pre .str { + color: chocolate; +} + +pre .lcl { + color: blue; +} + +pre .var { + color: blue; + font-weight: bold; +} \ No newline at end of file diff --git a/src/main/resources/net/shrimpworks/unreal/scriptbrowser/www/tree.ftl b/src/main/resources/net/shrimpworks/unreal/scriptbrowser/www/tree.ftl new file mode 100644 index 0000000..49695bc --- /dev/null +++ b/src/main/resources/net/shrimpworks/unreal/scriptbrowser/www/tree.ftl @@ -0,0 +1,63 @@ +<#macro treenode node depth> +
+ <#list 0..depth as d> + <#if d == depth && node.children?size gt 0> + + + <#else> +   + + + + ${node.clazz.name} + + <#list node.children> +
+ <#items as child><@treenode child depth+1/> +
+ +
+ + + + + + + + + + + +
+
+ <#list nodes as node> + <@treenode node 0/> + +
+ +
+ + +
+ + + + + \ No newline at end of file diff --git a/src/test/java/net/shrimpworks/unreal/scriptbrowser/AppTest.java b/src/test/java/net/shrimpworks/unreal/scriptbrowser/AppTest.java new file mode 100644 index 0000000..587db07 --- /dev/null +++ b/src/test/java/net/shrimpworks/unreal/scriptbrowser/AppTest.java @@ -0,0 +1,10 @@ +package net.shrimpworks.unreal.scriptbrowser; + +import org.junit.jupiter.api.Test; + +class AppTest { + @Test void appHasAGreeting() { +// App classUnderTest = new App(); +// assertNotNull(classUnderTest.getGreeting(), "app should have a greeting"); + } +}