commit b49fb3578ca6521b5a458dccf72d422524d17af8 Author: Kam Date: Mon Jan 22 20:35:00 2024 -0500 Initial commit diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..dfe0770 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# Auto detect text files and perform LF normalization +* text=auto diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f87f9c2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,13 @@ +# Personal Directory and Files + +.gradle +.idea +build +eclipse +.classpath +.project +.settings +bin +.metadata +*.bak +classes diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..d8252cb --- /dev/null +++ b/.gitmodules @@ -0,0 +1,4 @@ +[submodule "src/api"] + path = src/api + url = https://github.com/KAMKEEL/CustomNPC-Plus-API.git + branch = dev diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..ecc1c58 --- /dev/null +++ b/LICENSE @@ -0,0 +1,54 @@ +CustomNPC+ is distributed under a reasonably open license. You probably want to have a read through this if you want to do something with it other than just play. + +You are completely free and have the right to Use, Share and Adapt the mod. These rights can not be removed from you as long as follow the license terms. +If you simply wish to play with the mod and do nothing else, go for it, this license is of no use to you. + +TL;DR +> You CAN +-- Fork and modify the code. +-- Submit Pull Requests to this repository. +-- Copy portions of this code for use in other projects. +-- Write your own code that uses this code as a dependency. (addon mods!) + +> You CANNOT +-- Redistribute this in its entirety as source or compiled code. +-- Create or distribute code which contains 50% or more Functionally Equivalent Statements* from this repository. + +> You MUST +-- Maintain a visible repository of your code which is inspired by, derived from, or copied from this code. Basically, if you use it, pay it forward. You keep rights to your OWN code, but you still must make your source visible. + +General Clauses +Clauses that apply in general to any of the uses that follow. +- Personal Permission Clause + I will only give personal permission if I, KAMKEEL, am directly asked. If personal permission is given this does not imply endorsement. +- Waive Clause + All restrictive clauses in this license can be ignored with personal permission. Read the above clause. +- Extensive Clause + This license applies to CustomNPC+, the CustomNPC+ website, the legacy CustomNPC+ website and all other code, assets or binaries found on this website or the github repository unless otherwise indicated. +- Informal Clause + This license will not hold up in court. I have no intention of suing anyone for breaking it, the worst that can happen to you if you do is that I'll get annoyed and ask you to rectify your mistake. If you use a public distribution platform (such as CurseForge) it's also possible your project will get taken down. Breaking the license also makes you a jerk, and you don't want to be a jerk, do you? + +If you want to Distribute CustomNPC+ +"Distribution" refers to making available binaries, assets, or source of the mod available from the original sources as part of your modpack or otherwise. +- Attribution Clause + You must give appropriate credit to KAMKEEL as the creator of CustomNPC+ or the parts of it you're using. Noppes can also be credit as the original Mod Author. If you do any alterations the fact that you do so should also be indicated. A link back is optional but it would be cool if you would do so. +- Redistribution Clause + You may not redistribute the enitirity of the source code provided, unless heavy modifications are made to it as a whole. More than 50% of the code cannot be redistributed unless direct permission is provided from either KAMKEEL or Noppes. +- Non-Monetary Clause + You may not charge for access to the distribution itself or gain money through it, this includes any type of inline advertisement, such as url shorteners (adf.ly or otherwise) or ads in your service slowing the download down. This includes restricting any amount of access behind a paywall. Charging for ingame goods such as mod items or cosmetic features on a server does not count as distribution and falls purely under the Mojang Terms of Service. + +If you want to Feature CustomNPC+ +"Featuring" refers to using CustomNPC+ in an environment where you do not distribute binaries, assets, or source. An example would be a YouTube Let's Play. +- Thief Clause + You must not claim that you made CustomNPC+. Giving appropriate credit to KAMKEEL as the creator of CustomNPC+ makes you cooler, but you don't have to do it if you don't want to. + +If you want to Use CustomNPC+ Code or Assets +Usage of code or assets falls under the Extensive Clause. +- External Clause + Certain parts of the codebase are indicated to be licensed under alternative licenses. You must obey those licenses. +- Attribution Clause + You must give appropriate credit to KAMKEEL as the creator of CustomNPC+ or the parts of it you're using. Noppes can also be credit as the original Mod Author. If you do any alterations the fact that you do so should also be indicated. A link back is optional but it would be cool if you would do so. +- Copyleft Clause + Your project must be open source (have its source visible and allow for redistribution and modification) and include a clause similar to this one in its license. +- API Clause + Any of the other clauses under this section do not apply to any CustomNPC+ API code (the subdirectory src/api or https://github.com/KAMKEEL/CustomNPC-Plus-API). However, if CustomNPC+ API code is used, it must be included verbatim as it was obtained. diff --git a/README.md b/README.md new file mode 100644 index 0000000..c4634d5 --- /dev/null +++ b/README.md @@ -0,0 +1,86 @@ + +## 👋 Welcome to the CustomNPC+ Repository. +![](images/logo_banner.png) + +> CustomNPC+ is a [Minecraft](https://minecraft.net/) mod that allows you to add custom NPCs to your world. It is developed for creative and storytelling players who want to make their Minecraft worlds more in-depth and unique. CustomNPC+ is only a branch version of the **1.7.10** Forge version of the original (down below) and has no plans to add or update any other version. This is not an official version of CustomNPC. + +---------------- + + + Support Me + +[![Download CustomNPC+](https://img.shields.io/badge/CustomNPC+-0081CB?style=for-the-badge&logo=material-ui&logoColor=white)](https://modrinth.com/mod/customnpc-plus) +[![Download MPM+](https://img.shields.io/badge/MorePlayerModels+-0081CB?style=for-the-badge&logo=material-ui&logoColor=white)](https://www.curseforge.com/minecraft/mc-mods/moreplayermodels-plus) +[![Download PluginMod](https://img.shields.io/badge/Plugin_Mod-0081CB?style=for-the-badge&logo=material-ui&logoColor=white)](https://github.com/KAMKEEL/Plugin-Mod) + +---------------- + +### ❗ Notice +I am not the original creator of CustomNPC. The original creator is @[Noppes](https://github.com/Noppes). I have been permitted to update and develop my own branch version for 1.7.10. The original mod that is updated to the latest versions of Minecraft can be found on these links: [CurseForge](https://www.curseforge.com/minecraft/mc-mods/custom-npcs), [kodevelopment](http://www.kodevelopment.nl/minecraft/customnpcs) + +### ⬇️ Downloads + +- **Modrinth**: [Download](https://modrinth.com/mod/customnpc-plus) +- **CurseForge**: [Download](https://www.curseforge.com/minecraft/mc-mods/customnpc-plus) + +### 🔹 Installation +This mod is an ***update*** to CustomNPC and not an add-on. Do not install CustomNPC with CustomNPC-Plus. CustomNPC-Plus does not remove any functional features from the original mod. It expands upon CustomNPC with original features and backported features from newer versions. Simply drag CustomNPC-Plus into any client/server mods folder to utilize. Please **backup** before using as this could still have unforeseen errors. Report any bugs if found, thank you and enjoy. + +### ❓ Features: +- Flying NPC (Smart Pathfinding) +- 1.8 Skin Support (64x64) (Alex/Steve64) +- Full URL64 Selector (Downloads FULL Size Images for Custom Mob Skins) +- Additional Parts: [Wings, Fins, Capes, Horns, Tail] +- Hide Body Parts [Arms, Legs, Head, Body] +- Custom Overlays +- Animation Maker +- Enhanced Cloner GUI (More Tabs, Filters, Tags) +- Markov Name Generator +- Dark Mode GUI (Link Below) +- 1.12 Scripting Features (Details below) + +#### Scripting: +- CustomNPC+ Api: [API](https://github.com/KAMKEEL/CustomNPC-Plus-API) +- Java Doc: [kamkeel.github.io/CustomNPC-Plus](https://kamkeel.github.io/CustomNPC-Plus/) +- Scripted Items +- Player Overlays +- Script Timers +- Global Scripts +- Script Logging +- GUI Overlays +- Event Scripting +(We never remove script functionality, only add on existing functions) + +## 🔰 Skin Model Feature: +###### **Majority of these cape designs are taken from the internet with fair-usage.* +##### ❔ Please note all *local* skins in this version of CustomNPC, must match the model selected. + + +#### Flying NPCs: + + +#### Url64: + + +#### DarkMode + Cloner GUI: + + +## 🔗 Want to check out my other projects? +[Plugin Mod](https://github.com/KAMKEEL/Plugin-Mod) is a mod designed to add more aesthetic items for RPG and storytelling. It is compatible with CustomNPC+ and has many items intended to be used and wielded by created NPCs. + +[RPG Messenger](https://github.com/KAMKEEL/RPGMessenger) is a bukkit plugin designed to allow operators to create messagable NPCs for RPG Servers. Allows a 'RPG Controller' to reply quickly as NPCs, form groupchats, and messagable parties. + +[CustomNPC+ Dark Mode](https://github.com/KAMKEEL/CustomNPC-Plus-Dark-Mode) is a resource pack for CustomNPC+ that alters all GUIs within the mod to a dark variant that is both clean and pleasing to look at. + +## Cloning / Compiling / Building + +git clone call `git submodule update --init --recursive` + +Using IntelliJ, take the following steps: +1. Run `setupDecompWorkspace` under the `forgegradle` tab. +2. Refresh Gradle. +3. Run `genIntellijRuns` under the `other` tab to set up your Minecraft runs. + +or for Eclipse, run `gradlew setupDecompWorkspace eclipse` then import the project. + +Mixin code will not work if you do not add `--tweakClass org.spongepowered.asm.launch.MixinTweaker --mixin customnpcs.mixins.json` to your program arguments. diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..f066e35 --- /dev/null +++ b/build.gradle @@ -0,0 +1,81 @@ +buildscript { + repositories { + mavenCentral() + maven { + name = "forge" + url = "https://maven.minecraftforge.net/" + } + maven { + name = "sonatype" + url = "https://oss.sonatype.org/content/repositories/snapshots/" + } + maven { + name = "jitpack" + url = "https://jitpack.io" + } + maven { + name = "gt" + url = "https://gregtech.mechaenetia.com/" + } + } + dependencies { + classpath 'com.github.GTNewHorizons:ForgeGradle:1.2.11' + } +} + +task submodulesUpdate(type:Exec) { + description 'Updates (and inits) git submodules' + commandLine 'git', 'submodule', 'update', '--init', '--recursive' + group 'Build Setup' +} + +configurations { + embed + compile.extendsFrom(embed) + shade + compile.extendsFrom(shade) +} + +apply plugin: 'forge' + +version = "1.0" +group= "com.kamkeel.customnpc-plus" // http://maven.apache.org/guides/mini/guide-naming-conventions.html +archivesBaseName = "CustomNPC-Plus-Gecko" + +minecraft { + version = "1.7.10-10.13.4.1614-1.7.10" + runDir = "eclipse" +} + +sourceCompatibility = 1.8 +targetCompatibility = 1.8 + +processResources + { + // this will ensure that this task is redone when the versions change. + inputs.property "version", project.version + inputs.property "mcversion", project.minecraft.version + + // replace stuff in mcmod.info, nothing else + from(sourceSets.main.resources.srcDirs) { + include 'mcmod.info' + + // replace version and mcversion + expand 'version':project.version, 'mcversion':project.minecraft.version + } + + // copy everything else, thats not the mcmod.info + from(sourceSets.main.resources.srcDirs) { + exclude 'mcmod.info' + } + } + +jar { + from sourceSets.api.output + + manifest { + attributes( + 'FMLAT': "customnpcs_at.cfg", + ) + } +} diff --git a/debug.log b/debug.log new file mode 100644 index 0000000..06b228e --- /dev/null +++ b/debug.log @@ -0,0 +1,14 @@ +[0514/203945.010:WARNING:dns_config_service_win.cc(668)] Failed to read DnsConfig. +[0514/203945.039:WARNING:dns_config_service_win.cc(668)] Failed to read DnsConfig. +[0514/203945.047:WARNING:dns_config_service_win.cc(668)] Failed to read DnsConfig. +[0514/205447.662:WARNING:dns_config_service_win.cc(668)] Failed to read DnsConfig. +[0514/205458.151:WARNING:dns_config_service_win.cc(668)] Failed to read DnsConfig. +[0514/205458.162:WARNING:dns_config_service_win.cc(668)] Failed to read DnsConfig. +[0514/205458.186:WARNING:dns_config_service_win.cc(668)] Failed to read DnsConfig. +[0515/004913.901:WARNING:dns_config_service_win.cc(668)] Failed to read DnsConfig. +[0515/004914.692:WARNING:dns_config_service_win.cc(668)] Failed to read DnsConfig. +[0515/004914.711:WARNING:dns_config_service_win.cc(668)] Failed to read DnsConfig. +[0515/004914.724:WARNING:dns_config_service_win.cc(668)] Failed to read DnsConfig. +[0515/011427.812:WARNING:dns_config_service_win.cc(668)] Failed to read DnsConfig. +[0515/011427.829:WARNING:dns_config_service_win.cc(668)] Failed to read DnsConfig. +[0515/011427.838:WARNING:dns_config_service_win.cc(668)] Failed to read DnsConfig. diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..b761216 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..e6d7cad --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-4.4-all.zip \ No newline at end of file diff --git a/gradlew b/gradlew new file mode 100644 index 0000000..91a7e26 --- /dev/null +++ b/gradlew @@ -0,0 +1,164 @@ +#!/usr/bin/env bash + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn ( ) { + echo "$*" +} + +die ( ) { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; +esac + +# For Cygwin, ensure paths are in UNIX format before anything is touched. +if $cygwin ; then + [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` +fi + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >&- +APP_HOME="`pwd -P`" +cd "$SAVED" >&- + +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" = "false" -a "$darwin" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules +function splitJvmOpts() { + JVM_OPTS=("$@") +} +eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS +JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" + +exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..8a0b282 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,90 @@ +@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 + +@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= + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@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 init + +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 init + +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 + +:init +@rem Get command-line arguments, handling Windowz variants + +if not "%OS%" == "Windows_NT" goto win9xME_args +if "%@eval[2+2]" == "4" goto 4NT_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* +goto execute + +:4NT_args +@rem Get arguments from the 4NT Shell from JP Software +set CMD_LINE_ARGS=%$ + +: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 %CMD_LINE_ARGS% + +: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/libs/CustomNPC-Plus-1.9-beta5.jar b/libs/CustomNPC-Plus-1.9-beta5.jar new file mode 100644 index 0000000..2fd3901 Binary files /dev/null and b/libs/CustomNPC-Plus-1.9-beta5.jar differ diff --git a/libs/geckolib-forge-1.7.10-3.0.32.jar b/libs/geckolib-forge-1.7.10-3.0.32.jar new file mode 100644 index 0000000..cfd796a Binary files /dev/null and b/libs/geckolib-forge-1.7.10-3.0.32.jar differ diff --git a/src/main/java/noppes/npcs/CommonGeckoProxy.java b/src/main/java/noppes/npcs/CommonGeckoProxy.java new file mode 100644 index 0000000..ec199cc --- /dev/null +++ b/src/main/java/noppes/npcs/CommonGeckoProxy.java @@ -0,0 +1,7 @@ +package noppes.npcs; + +public class CommonGeckoProxy { + + public void load(){} + +} diff --git a/src/main/java/noppes/npcs/CustomNpcsGecko.java b/src/main/java/noppes/npcs/CustomNpcsGecko.java new file mode 100644 index 0000000..2885982 --- /dev/null +++ b/src/main/java/noppes/npcs/CustomNpcsGecko.java @@ -0,0 +1,36 @@ +package noppes.npcs; + +import cpw.mods.fml.common.Mod; +import cpw.mods.fml.common.Mod.EventHandler; +import cpw.mods.fml.common.SidedProxy; +import cpw.mods.fml.common.event.*; +import cpw.mods.fml.common.registry.EntityRegistry; +import net.minecraft.entity.Entity; +import net.minecraft.entity.EntityList; +import noppes.npcs.entity.*; + +@Mod(modid = "customnpcsgecko", + name = "CustomNPC+ Gecko", + version = "1.0", + dependencies = "required-after:customnpcs;") +public class CustomNpcsGecko { + + @SidedProxy(clientSide = "noppes.npcs.client.ClientGeckoProxy", serverSide = "noppes.npcs.CommonGeckoProxy") + public static CommonGeckoProxy proxy; + + private static int NewEntityStartId = 0; + + public CustomNpcsGecko() {} + + @EventHandler + public void load(FMLPreInitializationEvent ev) { + proxy.load(); + + registerCustomModel(EntityCustomModel.class, "CustomModel"); + } + + public void registerCustomModel(Class cl, String name) { + EntityRegistry.registerModEntity(cl, name, NewEntityStartId++, this, 64, 10, false); + EntityList.stringToClassMapping.put(name, cl); + } +} \ No newline at end of file diff --git a/src/main/java/noppes/npcs/DataGecko.java b/src/main/java/noppes/npcs/DataGecko.java new file mode 100644 index 0000000..6fb9af4 --- /dev/null +++ b/src/main/java/noppes/npcs/DataGecko.java @@ -0,0 +1,82 @@ +package noppes.npcs; + +import kamkeel.addon.DataDisplaySupport; +import net.minecraft.nbt.NBTTagCompound; + +public class DataGecko { + private String model = "geckolib3:geo/npc.geo.json"; + private String animFile = "custom:geo_npc.animation.json"; + private String idleAnim = ""; + private String walkAnim =d ""; + private String attackAnim = ""; + private String hurtAnim = ""; + + public NBTTagCompound writeToNBT(NBTTagCompound nbttagcompound) { + nbttagcompound.setString("Model", model); + nbttagcompound.setString("AnimFile", animFile); + nbttagcompound.setString("IdleAnim", idleAnim); + nbttagcompound.setString("WalkAnim", walkAnim); + nbttagcompound.setString("AttackAnim", attackAnim); + nbttagcompound.setString("HurtAnim", hurtAnim); + return nbttagcompound; + } + + public void readFromNBT(NBTTagCompound nbttagcompound) { + if(nbttagcompound.hasKey("Model")) { + model = nbttagcompound.getString("Model"); + animFile = nbttagcompound.getString("AnimFile"); + idleAnim = nbttagcompound.getString("IdleAnim"); + walkAnim = nbttagcompound.getString("WalkAnim"); + hurtAnim = nbttagcompound.getString("HurtAnim"); + attackAnim = nbttagcompound.getString("AttackAnim"); + } + } + + public String getModel() { + return model; + } + + public void setModel(String model) { + this.model = model; + } + + public String getAnimFile() { + return animFile; + } + + public void setAnimFile(String animFile) { + this.animFile = animFile; + } + + public String getIdleAnim() { + return idleAnim; + } + + public void setIdleAnim(String idleAnim) { + this.idleAnim = idleAnim; + } + + public String getWalkAnim() { + return walkAnim; + } + + public void setWalkAnim(String walkAnim) { + this.walkAnim = walkAnim; + } + + public String getAttackAnim() { + return attackAnim; + } + + public void setAttackAnim(String attackAnim) { + this.attackAnim = attackAnim; + } + + public String getHurtAnim() { + return hurtAnim; + } + + public void setHurtAnim(String hurtAnim) { + this.hurtAnim = hurtAnim; + } +} \ No newline at end of file diff --git a/src/main/java/noppes/npcs/client/ClientGeckoProxy.java b/src/main/java/noppes/npcs/client/ClientGeckoProxy.java new file mode 100644 index 0000000..e4194c6 --- /dev/null +++ b/src/main/java/noppes/npcs/client/ClientGeckoProxy.java @@ -0,0 +1,15 @@ +package noppes.npcs.client; + +import cpw.mods.fml.client.registry.RenderingRegistry; +import noppes.npcs.CommonGeckoProxy; +import noppes.npcs.client.renderer.RenderCustomModel; +import noppes.npcs.entity.EntityCustomModel; + +public class ClientGeckoProxy extends CommonGeckoProxy { + + @Override + public void load() { + RenderingRegistry.registerEntityRenderingHandler(EntityCustomModel.class, new RenderCustomModel()); + } + +} diff --git a/src/main/java/noppes/npcs/client/gui/model/custom/GuiCustomAnimFileSelection.java b/src/main/java/noppes/npcs/client/gui/model/custom/GuiCustomAnimFileSelection.java new file mode 100644 index 0000000..9a1e183 --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/model/custom/GuiCustomAnimFileSelection.java @@ -0,0 +1,80 @@ +package noppes.npcs.client.gui.model.custom; + +import kamkeel.addon.DataDisplaySupport; +import kamkeel.addon.GeckoAddonSupport; +import net.minecraft.client.gui.GuiButton; +import net.minecraft.client.gui.GuiScreen; +import net.minecraft.util.ResourceLocation; +import noppes.npcs.DataGecko; +import noppes.npcs.client.NoppesUtil; +import noppes.npcs.client.gui.util.GuiNPCInterface; +import noppes.npcs.client.gui.util.GuiNPCStringSlot; +import noppes.npcs.client.gui.util.GuiNpcButton; +import noppes.npcs.client.gui.util.GuiNpcLabel; +import noppes.npcs.entity.EntityNPCInterface; +import software.bernie.geckolib3.resource.GeckoLibCache; + +import java.util.Collections; +import java.util.Vector; + +import static kamkeel.addon.GeckoAddonSupport.getGeckoData; + +public class GuiCustomAnimFileSelection extends GuiNPCInterface { + public GuiNPCStringSlot slot; + public GuiScreen parent; + + public GuiCustomAnimFileSelection(EntityNPCInterface npc, GuiScreen parent){ + super(npc); + drawDefaultBackground = false; + title = ""; + this.parent = parent; + } + + @Override + public void initGui(){ + super.initGui(); + String ss = "Selecting geckolib animation file:"; + addLabel(new GuiNpcLabel(0,ss, width / 2 - (this.fontRendererObj.getStringWidth(ss)/2), 20, 0xffffff)); + Vector list = new Vector(); + for(ResourceLocation resLoc : GeckoLibCache.getInstance().getAnimations().keySet()){ + list.add(resLoc.toString()); + } + Collections.sort(list,String.CASE_INSENSITIVE_ORDER); + slot = new GuiNPCStringSlot(list,this,false,18); + slot.registerScrollButtons(4, 5); + + this.addButton(new GuiNpcButton(2, width / 2 - 100, height - 44,98, 20, "gui.back")); + } + + @Override + public void drawScreen(int i, int j, float f) + { + slot.drawScreen(i, j, f); + super.drawScreen(i, j, f); + } + + @Override + public void elementClicked(){ + } + + @Override + public void doubleClicked(){ + DataGecko geckoData = (DataGecko) getGeckoData(npc); + GeckoAddonSupport.putGeckoData(npc, geckoData); + close(); + NoppesUtil.openGUI(player, parent); + } + + @Override + protected void actionPerformed(GuiButton guibutton){ + int id = guibutton.id; + if(id == 2){ + close(); + NoppesUtil.openGUI(player, parent); + } + } + + @Override + public void save() { + } +} \ No newline at end of file diff --git a/src/main/java/noppes/npcs/client/gui/model/custom/GuiCustomAnimationSelection.java b/src/main/java/noppes/npcs/client/gui/model/custom/GuiCustomAnimationSelection.java new file mode 100644 index 0000000..8eca95d --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/model/custom/GuiCustomAnimationSelection.java @@ -0,0 +1,89 @@ +package noppes.npcs.client.gui.model.custom; + +import kamkeel.addon.DataDisplaySupport; +import kamkeel.addon.GeckoAddonSupport; +import net.minecraft.client.gui.GuiButton; +import net.minecraft.client.gui.GuiScreen; +import net.minecraft.util.ResourceLocation; +import noppes.npcs.DataGecko; +import noppes.npcs.client.NoppesUtil; +import noppes.npcs.client.gui.util.GuiNPCInterface; +import noppes.npcs.client.gui.util.GuiNPCStringSlot; +import noppes.npcs.client.gui.util.GuiNpcButton; +import noppes.npcs.client.gui.util.GuiNpcLabel; +import noppes.npcs.entity.EntityNPCInterface; +import software.bernie.geckolib3.core.builder.Animation; +import software.bernie.geckolib3.file.AnimationFile; +import software.bernie.geckolib3.resource.GeckoLibCache; + +import java.util.Collections; +import java.util.Vector; +import java.util.function.Consumer; + +import static kamkeel.addon.GeckoAddonSupport.getGeckoData; + +public class GuiCustomAnimationSelection extends GuiNPCInterface { + public GuiNPCStringSlot slot; + public GuiScreen parent; + public Consumer action; + + public GuiCustomAnimationSelection(EntityNPCInterface npc, GuiScreen parent, Consumer action){ + super(npc); + drawDefaultBackground = false; + title = ""; + this.parent = parent; + this.action=action; + } + + @Override + public void initGui(){ + super.initGui(); + String ss = "Selecting geckolib animation:"; + addLabel(new GuiNpcLabel(0,ss, width / 2 - (this.fontRendererObj.getStringWidth(ss)/2), 20, 0xffffff)); + Vector list = new Vector(); + + DataGecko geckoData = (DataGecko) getGeckoData(npc); + AnimationFile file = GeckoLibCache.getInstance().getAnimations().get(new ResourceLocation(geckoData.animFile)); + if(file!=null) { + for (Animation anim : file.getAllAnimations()) { + list.add(anim.animationName); + } + } + Collections.sort(list,String.CASE_INSENSITIVE_ORDER); + slot = new GuiNPCStringSlot(list,this,false,18); + slot.registerScrollButtons(4, 5); + + this.addButton(new GuiNpcButton(2, width / 2 - 100, height - 44,98, 20, "gui.back")); + } + + @Override + public void drawScreen(int i, int j, float f) + { + slot.drawScreen(i, j, f); + super.drawScreen(i, j, f); + } + + @Override + public void elementClicked(){ + } + + @Override + public void doubleClicked(){ + action.accept(slot.selected); + close(); + NoppesUtil.openGUI(player, parent); + } + + @Override + protected void actionPerformed(GuiButton guibutton){ + int id = guibutton.id; + if(id == 2){ + close(); + NoppesUtil.openGUI(player, parent); + } + } + + @Override + public void save() { + } +} \ No newline at end of file diff --git a/src/main/java/noppes/npcs/client/gui/model/custom/GuiCustomModelSelection.java b/src/main/java/noppes/npcs/client/gui/model/custom/GuiCustomModelSelection.java new file mode 100644 index 0000000..c0b0882 --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/model/custom/GuiCustomModelSelection.java @@ -0,0 +1,82 @@ +package noppes.npcs.client.gui.model.custom; + +import kamkeel.addon.DataDisplaySupport; +import kamkeel.addon.GeckoAddonSupport; +import net.minecraft.client.gui.GuiButton; +import net.minecraft.client.gui.GuiScreen; +import net.minecraft.util.ResourceLocation; +import noppes.npcs.DataGecko; +import noppes.npcs.client.NoppesUtil; +import noppes.npcs.client.gui.util.GuiNPCInterface; +import noppes.npcs.client.gui.util.GuiNPCStringSlot; +import noppes.npcs.client.gui.util.GuiNpcButton; +import noppes.npcs.client.gui.util.GuiNpcLabel; +import noppes.npcs.entity.EntityNPCInterface; +import software.bernie.geckolib3.resource.GeckoLibCache; + +import java.util.Collections; +import java.util.Vector; + +import static kamkeel.addon.GeckoAddonSupport.getGeckoData; + +public class GuiCustomModelSelection extends GuiNPCInterface { + public GuiNPCStringSlot slot; + public GuiScreen parent; + + public GuiCustomModelSelection(EntityNPCInterface npc, GuiScreen parent){ + super(npc); + drawDefaultBackground = false; + title = ""; + this.parent = parent; + } + + @Override + public void initGui(){ + super.initGui(); + String ss = "Selecting geckolib model:"; + addLabel(new GuiNpcLabel(0,ss, width / 2 - (this.fontRendererObj.getStringWidth(ss)/2), 20, 0xffffff)); + Vector list = new Vector(); + for(ResourceLocation resLoc : GeckoLibCache.getInstance().getGeoModels().keySet()){ + list.add(resLoc.toString()); + } + Collections.sort(list,String.CASE_INSENSITIVE_ORDER); + slot = new GuiNPCStringSlot(list,this,false,18); + slot.registerScrollButtons(4, 5); + + this.addButton(new GuiNpcButton(2, width / 2 - 100, height - 44,98, 20, "gui.back")); + } + + @Override + public void drawScreen(int i, int j, float f) + { + slot.drawScreen(i, j, f); + super.drawScreen(i, j, f); + } + + @Override + public void elementClicked(){ + } + + @Override + public void doubleClicked(){ + DataGecko geckoData = (DataGecko) getGeckoData(npc); + geckoData.model=slot.selected; + + GeckoAddonSupport.putGeckoData(npc, geckoData); + close(); + NoppesUtil.openGUI(player, parent); + } + + @Override + protected void actionPerformed(GuiButton guibutton){ + int id = guibutton.id; + if(id == 2){ + close(); + NoppesUtil.openGUI(player, parent); + } + } + + @Override + public void save() { + } +} \ No newline at end of file diff --git a/src/main/java/noppes/npcs/client/model/ModelCustom.java b/src/main/java/noppes/npcs/client/model/ModelCustom.java new file mode 100644 index 0000000..7ab1a8f --- /dev/null +++ b/src/main/java/noppes/npcs/client/model/ModelCustom.java @@ -0,0 +1,22 @@ +package noppes.npcs.client.model; + +import net.minecraft.util.ResourceLocation; +import noppes.npcs.entity.EntityCustomModel; +import software.bernie.geckolib3.model.AnimatedGeoModel; + +public class ModelCustom extends AnimatedGeoModel { + @Override + public ResourceLocation getAnimationFileLocation(EntityCustomModel entity) { + return entity.animResLoc;//new ResourceLocation(GeckoLib.ModID, "animations/bike.animation.json"); + } + + @Override + public ResourceLocation getModelLocation(EntityCustomModel entity) { + return entity.modelResLoc;//new ResourceLocation(GeckoLib.ModID, "geo/bike.geo.json"); + } + + @Override + public ResourceLocation getTextureLocation(EntityCustomModel entity) { + return entity.textureResLoc;//new ResourceLocation(GeckoLib.ModID, "textures/model/entity/bike.png"); + } +} \ No newline at end of file diff --git a/src/main/java/noppes/npcs/client/renderer/RenderCustomModel.java b/src/main/java/noppes/npcs/client/renderer/RenderCustomModel.java new file mode 100644 index 0000000..10ba064 --- /dev/null +++ b/src/main/java/noppes/npcs/client/renderer/RenderCustomModel.java @@ -0,0 +1,65 @@ +package noppes.npcs.client.renderer; + +import net.minecraft.client.renderer.entity.RenderManager; +import net.minecraft.item.ItemStack; +import net.minecraftforge.client.IItemRenderer; +import noppes.npcs.client.model.ModelCustom; +import noppes.npcs.entity.EntityCustomModel; +import org.lwjgl.opengl.GL11; +import software.bernie.geckolib3.geo.render.built.GeoBone; +import software.bernie.geckolib3.geo.render.built.GeoModel; +import software.bernie.geckolib3.renderers.geo.GeoEntityRenderer; + +import java.util.ArrayList; + +public class RenderCustomModel extends GeoEntityRenderer { + + public RenderCustomModel() { + super(new ModelCustom()); + } + public void renderAfter(GeoModel model, EntityCustomModel animatable, float ticks, float red, float green, float blue, float alpha) { + super.renderAfter(model, animatable, ticks, red, green, blue, alpha); + if (model.getBone("held_item").isPresent() && animatable.getHeldItem() != null) { + GeoBone bone = model.getBone("held_item").get(); + this.renderItem(animatable, bone, ticks); + } + } + + public GeoBone[] getPathFromRoot(GeoBone bone) { + ArrayList bones; + for(bones = new ArrayList(); bone != null; bone = bone.parent) { + bones.add(0, bone); + } + + return (GeoBone[])bones.toArray(new GeoBone[0]); + } + + public void renderItem(EntityCustomModel animatable, GeoBone locator, float ticks) { + GL11.glPushMatrix(); + float scale = 0.5F; + GL11.glScaled(scale, scale, scale); + GeoBone[] bonePath = this.getPathFromRoot(locator); + + for(int i = 0; i < bonePath.length; ++i) { + GeoBone b = bonePath[i]; + GL11.glTranslatef(b.getPositionX() / (16.0F * scale), b.getPositionY() / (16.0F * scale), b.getPositionZ() / (16.0F * scale)); + GL11.glTranslatef(b.getPivotX() / (16.0F * scale), b.getPivotY() / (16.0F * scale), b.getPivotZ() / (16.0F * scale)); + GL11.glRotated((double)b.getRotationZ() / Math.PI * 180.0, 0.0, 0.0, 1.0); + GL11.glRotated((double)b.getRotationY() / Math.PI * 180.0, 0.0, 1.0, 0.0); + GL11.glRotated((double)b.getRotationX() / Math.PI * 180.0, 1.0, 0.0, 0.0); + GL11.glScalef(b.getScaleX(), b.getScaleY(), b.getScaleZ()); + GL11.glTranslatef(-b.getPivotX() / (16.0F * scale), -b.getPivotY() / (16.0F * scale), -b.getPivotZ() / (16.0F * scale)); + } + + GL11.glRotatef(250.0F, 1.0F, 0.0F, 0.0F); + GL11.glRotatef(40.0F, 0.0F, 1.0F, 0.0F); + GL11.glTranslatef(-0.4F, -0.55F, 1.7F); + ItemStack stack = animatable.getHeldItem(); + RenderManager.instance.itemRenderer.renderItem(animatable, stack, 0, IItemRenderer.ItemRenderType.INVENTORY); + GL11.glPopMatrix(); + } + + public boolean isBoneRenderOverriden(EntityCustomModel entity, GeoBone bone) { + return bone.name.equals("held_item"); + } +} \ No newline at end of file diff --git a/src/main/java/noppes/npcs/entity/EntityCustomModel.java b/src/main/java/noppes/npcs/entity/EntityCustomModel.java new file mode 100644 index 0000000..426dcac --- /dev/null +++ b/src/main/java/noppes/npcs/entity/EntityCustomModel.java @@ -0,0 +1,104 @@ +package noppes.npcs.entity; + +import net.minecraft.entity.EntityCreature; +import net.minecraft.entity.ai.EntityAIWatchClosest; +import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.item.ItemStack; +import net.minecraft.util.ResourceLocation; +import net.minecraft.world.World; +import software.bernie.geckolib3.core.AnimationState; +import software.bernie.geckolib3.core.IAnimatable; +import software.bernie.geckolib3.core.IAnimationTickable; +import software.bernie.geckolib3.core.PlayState; +import software.bernie.geckolib3.core.builder.AnimationBuilder; +import software.bernie.geckolib3.core.controller.AnimationController; +import software.bernie.geckolib3.core.event.predicate.AnimationEvent; +import software.bernie.geckolib3.core.manager.AnimationData; +import software.bernie.geckolib3.core.manager.AnimationFactory; + +import java.util.Objects; + +public class EntityCustomModel extends EntityCreature implements IAnimatable, IAnimationTickable { + private AnimationFactory factory = new AnimationFactory(this); + public ResourceLocation modelResLoc = new ResourceLocation("geckolib3", "geo/npc.geo.json"); + public ResourceLocation animResLoc = new ResourceLocation("custom", "geo_npc.animation.json"); + public ResourceLocation textureResLoc = new ResourceLocation("geckolib3", "textures/model/entity/geo_npc.png"); + public String idleAnim = ""; + public String walkAnim = ""; + public String hurtAnim = ""; + public String attackAnim = ""; + public AnimationBuilder dialogAnim = null; + public AnimationBuilder manualAnim = null; + public ItemStack leftHeldItem; + + private PlayState predicateMovement(AnimationEvent event) { + if (manualAnim != null) { + if (event.getController().getAnimationState() == AnimationState.Stopped) { + manualAnim = null; + } else { + if (event.getController().currentAnimationBuilder != manualAnim) { + event.getController().markNeedsReload(); + } + event.getController().setAnimation(manualAnim); + return PlayState.CONTINUE; + } + } + if (dialogAnim != null) { + if (event.getController().getAnimationState() == AnimationState.Stopped) { + dialogAnim = null; + } else { + if (event.getController().currentAnimationBuilder != dialogAnim) { + event.getController().markNeedsReload(); + } + event.getController().setAnimation(dialogAnim); + return PlayState.CONTINUE; + } + } + if (!event.isMoving() || walkAnim.isEmpty()) { + if (!idleAnim.isEmpty()) { + event.getController().setAnimation(new AnimationBuilder().loop(idleAnim)); + } else { + return PlayState.STOP; + } + } else { + event.getController().setAnimation(new AnimationBuilder().loop(walkAnim)); + } + return PlayState.CONTINUE; + } + + private PlayState predicateAttack(AnimationEvent event) { + return PlayState.CONTINUE; + } + + public void setDialogAnim(String name) { + dialogAnim = new AnimationBuilder().playOnce(name); + } + + public EntityCustomModel(World worldIn) { + super(worldIn); + this.ignoreFrustumCheck = true; + this.tasks.addTask(6, new EntityAIWatchClosest(this, EntityPlayer.class, 8.0F)); + this.setSize(0.7F, 2F); + } + + @Override + public void registerControllers(AnimationData data) { + data.addAnimationController(new AnimationController<>(this, "movement", 10, this::predicateMovement)); + data.addAnimationController(new AnimationController<>(this, "attack", 10, this::predicateAttack)); + } + + @Override + public AnimationFactory getFactory() { + return this.factory; + } + + @Override + public int tickTimer() { + return ticksExisted; + } + + @Override + public void tick() { + super.onUpdate(); + } +} \ No newline at end of file diff --git a/src/main/java/noppes/npcs/util/AnimationFileUtil.java b/src/main/java/noppes/npcs/util/AnimationFileUtil.java new file mode 100644 index 0000000..6c76a06 --- /dev/null +++ b/src/main/java/noppes/npcs/util/AnimationFileUtil.java @@ -0,0 +1,30 @@ +package noppes.npcs.util; + +import net.minecraft.util.ResourceLocation; +import software.bernie.geckolib3.core.builder.Animation; +import software.bernie.geckolib3.file.AnimationFile; +import software.bernie.geckolib3.resource.GeckoLibCache; + +import java.util.List; +import java.util.Vector; + +public class AnimationFileUtil { + public static List getAnimationList(String animFileName) { + Vector list = new Vector<>(); + AnimationFile file = GeckoLibCache.getInstance().getAnimations().get(new ResourceLocation(animFileName)); + if (file != null) { + for (Animation anim : file.getAllAnimations()) { + list.add(anim.animationName); + } + } + return list; + } + + public static List getAnimationFileList() { + Vector list = new Vector<>(); + for (ResourceLocation resLoc : GeckoLibCache.getInstance().getAnimations().keySet()) { + list.add(resLoc.toString()); + } + return list; + } +} diff --git a/src/main/resources/mcmod.info b/src/main/resources/mcmod.info new file mode 100644 index 0000000..da2fc5e --- /dev/null +++ b/src/main/resources/mcmod.info @@ -0,0 +1,18 @@ +[ +{ + "modid": "customnpcsgecko", + "name": "CustomNPC+ Gecko", + "version": "1.7.10", + "mcversion": "1.7.10", + "description": "CustomNPC+ Gecko is an addon mod for CustomNPC+ to add Custom Model support", + "credits": "KAMKEEL (CNPC+)", + "logoFile": "assets/customnpcs/textures/gui/logo.png", + "url": "https://tinyurl.com/customnpc-plus", + "updateUrl": "", + "authorList": [ + "Goodbird"], + "parent":"", + "screenshots": [], + "dependencies": [] +} +] \ No newline at end of file