Aims to provide a simple way for server owners to setup daily rewards for their players.
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

1.0.0

equinoxx df5b04db

+1517
+5
.gitattributes
··· 1 + # Disable autocrlf on generated files, they always generate with LF 2 + # Add any extra files or paths here to make git stop saying they 3 + # are changed when only line endings change. 4 + src/generated/**/.cache/* text eol=lf 5 + src/generated/**/*.json text eol=lf
+40
.gitignore
··· 1 + ### Gradle ### 2 + .gradle 3 + build/ 4 + !gradle/wrapper/gradle-wrapper.jar 5 + !**/src/**/build/ 6 + 7 + ### IntelliJ IDEA ### 8 + .idea/ 9 + *.iws 10 + *.iml 11 + *.ipr 12 + out/ 13 + !**/src/**/out/ 14 + 15 + .run/ 16 + 17 + ### Eclipse ### 18 + .apt_generated 19 + .classpath 20 + .eclipse/ 21 + .factorypath 22 + .project 23 + .settings 24 + .springBeans 25 + .sts4-cache 26 + bin/ 27 + !**/src/**/bin/ 28 + 29 + ### VS Code ### 30 + .vscode/ 31 + 32 + ### Mac OS ### 33 + .DS_Store 34 + 35 + ### Minecraft Modding ### 36 + run/ 37 + !**/src/**/run/ 38 + **/src/generated/**/.cache/ 39 + repo/ 40 + !**/src/**/repo/
+13
.zed/settings.json
··· 1 + // Folder-specific settings 2 + // 3 + // For a full list of overridable settings, and general information on folder-specific settings, 4 + // see the documentation: https://zed.dev/docs/configuring-zed#settings-files 5 + { 6 + "lsp": { 7 + "jdtls": { 8 + "settings": { 9 + "jdk_auto_download": true, 10 + }, 11 + }, 12 + }, 13 + }
+7
LICENSE.txt
··· 1 + Copyright © 2026 Equinoxx 2 + 3 + Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 + 5 + The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 + 7 + THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+3
README.md
··· 1 + # Daily Rewards 2 + 3 + Daily Rewards is a mod that lets players claim a configurable reward once every 24 hours.
+192
build.gradle
··· 1 + plugins { 2 + id 'java-library' 3 + id 'maven-publish' 4 + id 'net.neoforged.moddev' version '2.0.141' 5 + id 'idea' 6 + } 7 + 8 + tasks.named('wrapper', Wrapper).configure { 9 + // Define wrapper values here so as to not have to always do so when updating gradlew.properties. 10 + // Switching this to Wrapper.DistributionType.ALL will download the full gradle sources that comes with 11 + // documentation attached on cursor hover of gradle classes and methods. However, this comes with increased 12 + // file size for Gradle. If you do switch this to ALL, run the Gradle wrapper task twice afterwards. 13 + // (Verify by checking gradle/wrapper/gradle-wrapper.properties to see if distributionUrl now points to `-all`) 14 + distributionType = Wrapper.DistributionType.BIN 15 + } 16 + 17 + version = mod_version 18 + group = mod_group_id 19 + 20 + sourceSets.main.resources { 21 + // Include resources generated by data generators. 22 + srcDir('src/generated/resources') 23 + 24 + // Exclude common development only resources from finalized outputs 25 + exclude("**/*.bbmodel") // BlockBench project files 26 + exclude("src/generated/**/.cache") // datagen cache files 27 + } 28 + 29 + repositories { 30 + // Add here additional repositories if required by some of the dependencies below. 31 + } 32 + 33 + base { 34 + archivesName = mod_id 35 + } 36 + 37 + // Mojang ships Java 21 to end users in 1.21.1, so mods should target Java 21. 38 + java.toolchain.languageVersion = JavaLanguageVersion.of(21) 39 + 40 + neoForge { 41 + // Specify the version of NeoForge to use. 42 + version = project.neo_version 43 + 44 + parchment { 45 + mappingsVersion = project.parchment_mappings_version 46 + minecraftVersion = project.parchment_minecraft_version 47 + } 48 + 49 + // This line is optional. Access Transformers are automatically detected 50 + // accessTransformers = project.files('src/main/resources/META-INF/accesstransformer.cfg') 51 + 52 + // Default run configurations. 53 + // These can be tweaked, removed, or duplicated as needed. 54 + runs { 55 + client { 56 + client() 57 + 58 + // Comma-separated list of namespaces to load gametests from. Empty = all namespaces. 59 + systemProperty 'neoforge.enabledGameTestNamespaces', project.mod_id 60 + } 61 + 62 + server { 63 + server() 64 + programArgument '--nogui' 65 + systemProperty 'neoforge.enabledGameTestNamespaces', project.mod_id 66 + } 67 + 68 + // This run config launches GameTestServer and runs all registered gametests, then exits. 69 + // By default, the server will crash when no gametests are provided. 70 + // The gametest system is also enabled by default for other run configs under the /test command. 71 + gameTestServer { 72 + type = "gameTestServer" 73 + systemProperty 'neoforge.enabledGameTestNamespaces', project.mod_id 74 + } 75 + 76 + data { 77 + data() 78 + 79 + // example of overriding the workingDirectory set in configureEach above, uncomment if you want to use it 80 + // gameDirectory = project.file('run-data') 81 + 82 + // Specify the modid for data generation, where to output the resulting resource, and where to look for existing resources. 83 + programArguments.addAll '--mod', project.mod_id, '--all', '--output', file('src/generated/resources/').getAbsolutePath(), '--existing', file('src/main/resources/').getAbsolutePath() 84 + } 85 + 86 + // applies to all the run configs above 87 + configureEach { 88 + // Recommended logging data for a userdev environment 89 + // The markers can be added/remove as needed separated by commas. 90 + // "SCAN": For mods scan. 91 + // "REGISTRIES": For firing of registry events. 92 + // "REGISTRYDUMP": For getting the contents of all registries. 93 + systemProperty 'forge.logging.markers', 'REGISTRIES' 94 + 95 + // Recommended logging level for the console 96 + // You can set various levels here. 97 + // Please read: https://stackoverflow.com/questions/2031163/when-to-use-the-different-log-levels 98 + logLevel = org.slf4j.event.Level.DEBUG 99 + } 100 + } 101 + 102 + mods { 103 + // define mod <-> source bindings 104 + // these are used to tell the game which sources are for which mod 105 + // multi mod projects should define one per mod 106 + "${mod_id}" { 107 + sourceSet(sourceSets.main) 108 + } 109 + } 110 + } 111 + 112 + // Sets up a dependency configuration called 'localRuntime'. 113 + // This configuration should be used instead of 'runtimeOnly' to declare 114 + // a dependency that will be present for runtime testing but that is 115 + // "optional", meaning it will not be pulled by dependents of this mod. 116 + configurations { 117 + runtimeClasspath.extendsFrom localRuntime 118 + } 119 + 120 + dependencies { 121 + // Example optional mod dependency with JEI 122 + // The JEI API is declared for compile time use, while the full JEI artifact is used at runtime 123 + // compileOnly "mezz.jei:jei-${mc_version}-common-api:${jei_version}" 124 + // compileOnly "mezz.jei:jei-${mc_version}-neoforge-api:${jei_version}" 125 + // We add the full version to localRuntime, not runtimeOnly, so that we do not publish a dependency on it 126 + // localRuntime "mezz.jei:jei-${mc_version}-neoforge:${jei_version}" 127 + 128 + // Example mod dependency using a mod jar from ./libs with a flat dir repository 129 + // This maps to ./libs/coolmod-${mc_version}-${coolmod_version}.jar 130 + // The group id is ignored when searching -- in this case, it is "blank" 131 + // implementation "blank:coolmod-${mc_version}:${coolmod_version}" 132 + 133 + // Example mod dependency using a file as dependency 134 + // implementation files("libs/coolmod-${mc_version}-${coolmod_version}.jar") 135 + 136 + // Example project dependency using a sister or child project: 137 + // implementation project(":myproject") 138 + 139 + // For more info: 140 + // http://www.gradle.org/docs/current/userguide/artifact_dependencies_tutorial.html 141 + // http://www.gradle.org/docs/current/userguide/dependency_management.html 142 + } 143 + 144 + // This block of code expands all declared replace properties in the specified resource targets. 145 + // A missing property will result in an error. Properties are expanded using ${} Groovy notation. 146 + var generateModMetadata = tasks.register("generateModMetadata", ProcessResources) { 147 + var replaceProperties = [ 148 + minecraft_version : minecraft_version, 149 + minecraft_version_range: minecraft_version_range, 150 + neo_version : neo_version, 151 + loader_version_range : loader_version_range, 152 + mod_id : mod_id, 153 + mod_name : mod_name, 154 + mod_license : mod_license, 155 + mod_version : mod_version, 156 + ] 157 + inputs.properties replaceProperties 158 + expand replaceProperties 159 + from "src/main/templates" 160 + into "build/generated/sources/modMetadata" 161 + } 162 + // Include the output of "generateModMetadata" as an input directory for the build 163 + // this works with both building through Gradle and the IDE. 164 + sourceSets.main.resources.srcDir generateModMetadata 165 + // To avoid having to run "generateModMetadata" manually, make it run on every project reload 166 + neoForge.ideSyncTask generateModMetadata 167 + 168 + // Example configuration to allow publishing using the maven-publish plugin 169 + publishing { 170 + publications { 171 + register('mavenJava', MavenPublication) { 172 + from components.java 173 + } 174 + } 175 + repositories { 176 + maven { 177 + url "file://${project.projectDir}/repo" 178 + } 179 + } 180 + } 181 + 182 + tasks.withType(JavaCompile).configureEach { 183 + options.encoding = 'UTF-8' // Use the UTF-8 charset for Java compilation 184 + } 185 + 186 + // IDEA no longer automatically downloads sources/javadoc jars for dependencies, so we need to explicitly enable the behavior. 187 + idea { 188 + module { 189 + downloadSources = true 190 + downloadJavadoc = true 191 + } 192 + }
+39
gradle.properties
··· 1 + # Sets default memory used for gradle commands. Can be overridden by user or command line properties. 2 + org.gradle.jvmargs=-Xmx1G 3 + org.gradle.daemon=true 4 + org.gradle.parallel=true 5 + org.gradle.caching=true 6 + org.gradle.configuration-cache=true 7 + 8 + #read more on this at https://github.com/neoforged/ModDevGradle?tab=readme-ov-file#better-minecraft-parameter-names--javadoc-parchment 9 + # you can also find the latest versions at: https://parchmentmc.org/docs/getting-started 10 + parchment_minecraft_version=1.21.1 11 + parchment_mappings_version=2024.11.17 12 + # Environment Properties 13 + # You can find the latest versions here: https://projects.neoforged.net/neoforged/neoforge 14 + # The Minecraft version must agree with the Neo version to get a valid artifact 15 + minecraft_version=1.21.1 16 + # The Minecraft version range can use any release version of Minecraft as bounds. 17 + # Snapshots, pre-releases, and release candidates are not guaranteed to sort properly 18 + # as they do not follow standard versioning conventions. 19 + minecraft_version_range=[1.21.1] 20 + # The Neo version must agree with the Minecraft version to get a valid artifact 21 + neo_version=21.1.219 22 + # The loader version range can only use the major version of FML as bounds 23 + loader_version_range=[1,) 24 + 25 + ## Mod Properties 26 + 27 + # The unique mod identifier for the mod. Must be lowercase in English locale. Must fit the regex [a-z][a-z0-9_]{1,63} 28 + # Must match the String constant located in the main mod class annotated with @Mod. 29 + mod_id=dailyrewards 30 + # The human-readable display name for the mod. 31 + mod_name=Daily Rewards 32 + # The license of the mod. Review your options at https://choosealicense.com/. All Rights Reserved is the default. 33 + mod_license=All Rights Reserved 34 + # The mod version. See https://semver.org/ 35 + mod_version=1.0.0 36 + # The group ID for the mod. It is only important when publishing as an artifact to a Maven repository. 37 + # This should match the base package used for the mod sources. 38 + # See https://maven.apache.org/guides/mini/guide-naming-conventions.html 39 + mod_group_id=dev.equinoxx.dailyrewards
gradle/wrapper/gradle-wrapper.jar

This is a binary file and will not be displayed.

+7
gradle/wrapper/gradle-wrapper.properties
··· 1 + distributionBase=GRADLE_USER_HOME 2 + distributionPath=wrapper/dists 3 + distributionUrl=https\://services.gradle.org/distributions/gradle-9.2.1-bin.zip 4 + networkTimeout=10000 5 + validateDistributionUrl=true 6 + zipStoreBase=GRADLE_USER_HOME 7 + zipStorePath=wrapper/dists
+251
gradlew
··· 1 + #!/bin/sh 2 + 3 + # 4 + # Copyright © 2015-2021 the original authors. 5 + # 6 + # Licensed under the Apache License, Version 2.0 (the "License"); 7 + # you may not use this file except in compliance with the License. 8 + # You may obtain a copy of the License at 9 + # 10 + # https://www.apache.org/licenses/LICENSE-2.0 11 + # 12 + # Unless required by applicable law or agreed to in writing, software 13 + # distributed under the License is distributed on an "AS IS" BASIS, 14 + # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 + # See the License for the specific language governing permissions and 16 + # limitations under the License. 17 + # 18 + # SPDX-License-Identifier: Apache-2.0 19 + # 20 + 21 + ############################################################################## 22 + # 23 + # Gradle start up script for POSIX generated by Gradle. 24 + # 25 + # Important for running: 26 + # 27 + # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 28 + # noncompliant, but you have some other compliant shell such as ksh or 29 + # bash, then to run this script, type that shell name before the whole 30 + # command line, like: 31 + # 32 + # ksh Gradle 33 + # 34 + # Busybox and similar reduced shells will NOT work, because this script 35 + # requires all of these POSIX shell features: 36 + # * functions; 37 + # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 38 + # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 39 + # * compound commands having a testable exit status, especially «case»; 40 + # * various built-in commands including «command», «set», and «ulimit». 41 + # 42 + # Important for patching: 43 + # 44 + # (2) This script targets any POSIX shell, so it avoids extensions provided 45 + # by Bash, Ksh, etc; in particular arrays are avoided. 46 + # 47 + # The "traditional" practice of packing multiple parameters into a 48 + # space-separated string is a well documented source of bugs and security 49 + # problems, so this is (mostly) avoided, by progressively accumulating 50 + # options in "$@", and eventually passing that to Java. 51 + # 52 + # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 53 + # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 54 + # see the in-line comments for details. 55 + # 56 + # There are tweaks for specific operating systems such as AIX, CygWin, 57 + # Darwin, MinGW, and NonStop. 58 + # 59 + # (3) This script is generated from the Groovy template 60 + # https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 61 + # within the Gradle project. 62 + # 63 + # You can find Gradle at https://github.com/gradle/gradle/. 64 + # 65 + ############################################################################## 66 + 67 + # Attempt to set APP_HOME 68 + 69 + # Resolve links: $0 may be a link 70 + app_path=$0 71 + 72 + # Need this for daisy-chained symlinks. 73 + while 74 + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 75 + [ -h "$app_path" ] 76 + do 77 + ls=$( ls -ld "$app_path" ) 78 + link=${ls#*' -> '} 79 + case $link in #( 80 + /*) app_path=$link ;; #( 81 + *) app_path=$APP_HOME$link ;; 82 + esac 83 + done 84 + 85 + # This is normally unused 86 + # shellcheck disable=SC2034 87 + APP_BASE_NAME=${0##*/} 88 + # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) 89 + APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit 90 + 91 + # Use the maximum available, or set MAX_FD != -1 to use that value. 92 + MAX_FD=maximum 93 + 94 + warn () { 95 + echo "$*" 96 + } >&2 97 + 98 + die () { 99 + echo 100 + echo "$*" 101 + echo 102 + exit 1 103 + } >&2 104 + 105 + # OS specific support (must be 'true' or 'false'). 106 + cygwin=false 107 + msys=false 108 + darwin=false 109 + nonstop=false 110 + case "$( uname )" in #( 111 + CYGWIN* ) cygwin=true ;; #( 112 + Darwin* ) darwin=true ;; #( 113 + MSYS* | MINGW* ) msys=true ;; #( 114 + NONSTOP* ) nonstop=true ;; 115 + esac 116 + 117 + CLASSPATH="\\\"\\\"" 118 + 119 + 120 + # Determine the Java command to use to start the JVM. 121 + if [ -n "$JAVA_HOME" ] ; then 122 + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 123 + # IBM's JDK on AIX uses strange locations for the executables 124 + JAVACMD=$JAVA_HOME/jre/sh/java 125 + else 126 + JAVACMD=$JAVA_HOME/bin/java 127 + fi 128 + if [ ! -x "$JAVACMD" ] ; then 129 + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 130 + 131 + Please set the JAVA_HOME variable in your environment to match the 132 + location of your Java installation." 133 + fi 134 + else 135 + JAVACMD=java 136 + if ! command -v java >/dev/null 2>&1 137 + then 138 + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 139 + 140 + Please set the JAVA_HOME variable in your environment to match the 141 + location of your Java installation." 142 + fi 143 + fi 144 + 145 + # Increase the maximum file descriptors if we can. 146 + if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 147 + case $MAX_FD in #( 148 + max*) 149 + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. 150 + # shellcheck disable=SC2039,SC3045 151 + MAX_FD=$( ulimit -H -n ) || 152 + warn "Could not query maximum file descriptor limit" 153 + esac 154 + case $MAX_FD in #( 155 + '' | soft) :;; #( 156 + *) 157 + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. 158 + # shellcheck disable=SC2039,SC3045 159 + ulimit -n "$MAX_FD" || 160 + warn "Could not set maximum file descriptor limit to $MAX_FD" 161 + esac 162 + fi 163 + 164 + # Collect all arguments for the java command, stacking in reverse order: 165 + # * args from the command line 166 + # * the main class name 167 + # * -classpath 168 + # * -D...appname settings 169 + # * --module-path (only if needed) 170 + # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 171 + 172 + # For Cygwin or MSYS, switch paths to Windows format before running java 173 + if "$cygwin" || "$msys" ; then 174 + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 175 + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 176 + 177 + JAVACMD=$( cygpath --unix "$JAVACMD" ) 178 + 179 + # Now convert the arguments - kludge to limit ourselves to /bin/sh 180 + for arg do 181 + if 182 + case $arg in #( 183 + -*) false ;; # don't mess with options #( 184 + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 185 + [ -e "$t" ] ;; #( 186 + *) false ;; 187 + esac 188 + then 189 + arg=$( cygpath --path --ignore --mixed "$arg" ) 190 + fi 191 + # Roll the args list around exactly as many times as the number of 192 + # args, so each arg winds up back in the position where it started, but 193 + # possibly modified. 194 + # 195 + # NB: a `for` loop captures its iteration list before it begins, so 196 + # changing the positional parameters here affects neither the number of 197 + # iterations, nor the values presented in `arg`. 198 + shift # remove old arg 199 + set -- "$@" "$arg" # push replacement arg 200 + done 201 + fi 202 + 203 + 204 + # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 205 + DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 206 + 207 + # Collect all arguments for the java command: 208 + # * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, 209 + # and any embedded shellness will be escaped. 210 + # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be 211 + # treated as '${Hostname}' itself on the command line. 212 + 213 + set -- \ 214 + "-Dorg.gradle.appname=$APP_BASE_NAME" \ 215 + -classpath "$CLASSPATH" \ 216 + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ 217 + "$@" 218 + 219 + # Stop when "xargs" is not available. 220 + if ! command -v xargs >/dev/null 2>&1 221 + then 222 + die "xargs is not available" 223 + fi 224 + 225 + # Use "xargs" to parse quoted args. 226 + # 227 + # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 228 + # 229 + # In Bash we could simply go: 230 + # 231 + # readarray ARGS < <( xargs -n1 <<<"$var" ) && 232 + # set -- "${ARGS[@]}" "$@" 233 + # 234 + # but POSIX shell has neither arrays nor command substitution, so instead we 235 + # post-process each arg (as a line of input to sed) to backslash-escape any 236 + # character that might be a shell metacharacter, then use eval to reverse 237 + # that process (while maintaining the separation between arguments), and wrap 238 + # the whole thing up as a single "set" statement. 239 + # 240 + # This will of course break if any of these variables contains a newline or 241 + # an unmatched quote. 242 + # 243 + 244 + eval "set -- $( 245 + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 246 + xargs -n1 | 247 + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 248 + tr '\n' ' ' 249 + )" '"$@"' 250 + 251 + exec "$JAVACMD" "$@"
+94
gradlew.bat
··· 1 + @rem 2 + @rem Copyright 2015 the original author or authors. 3 + @rem 4 + @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 + @rem you may not use this file except in compliance with the License. 6 + @rem You may obtain a copy of the License at 7 + @rem 8 + @rem https://www.apache.org/licenses/LICENSE-2.0 9 + @rem 10 + @rem Unless required by applicable law or agreed to in writing, software 11 + @rem distributed under the License is distributed on an "AS IS" BASIS, 12 + @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 + @rem See the License for the specific language governing permissions and 14 + @rem limitations under the License. 15 + @rem 16 + @rem SPDX-License-Identifier: Apache-2.0 17 + @rem 18 + 19 + @if "%DEBUG%"=="" @echo off 20 + @rem ########################################################################## 21 + @rem 22 + @rem Gradle startup script for Windows 23 + @rem 24 + @rem ########################################################################## 25 + 26 + @rem Set local scope for the variables with windows NT shell 27 + if "%OS%"=="Windows_NT" setlocal 28 + 29 + set DIRNAME=%~dp0 30 + if "%DIRNAME%"=="" set DIRNAME=. 31 + @rem This is normally unused 32 + set APP_BASE_NAME=%~n0 33 + set APP_HOME=%DIRNAME% 34 + 35 + @rem Resolve any "." and ".." in APP_HOME to make it shorter. 36 + for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 37 + 38 + @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 39 + set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 40 + 41 + @rem Find java.exe 42 + if defined JAVA_HOME goto findJavaFromJavaHome 43 + 44 + set JAVA_EXE=java.exe 45 + %JAVA_EXE% -version >NUL 2>&1 46 + if %ERRORLEVEL% equ 0 goto execute 47 + 48 + echo. 1>&2 49 + echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 50 + echo. 1>&2 51 + echo Please set the JAVA_HOME variable in your environment to match the 1>&2 52 + echo location of your Java installation. 1>&2 53 + 54 + goto fail 55 + 56 + :findJavaFromJavaHome 57 + set JAVA_HOME=%JAVA_HOME:"=% 58 + set JAVA_EXE=%JAVA_HOME%/bin/java.exe 59 + 60 + if exist "%JAVA_EXE%" goto execute 61 + 62 + echo. 1>&2 63 + echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 64 + echo. 1>&2 65 + echo Please set the JAVA_HOME variable in your environment to match the 1>&2 66 + echo location of your Java installation. 1>&2 67 + 68 + goto fail 69 + 70 + :execute 71 + @rem Setup the command line 72 + 73 + set CLASSPATH= 74 + 75 + 76 + @rem Execute Gradle 77 + "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* 78 + 79 + :end 80 + @rem End local scope for the variables with windows NT shell 81 + if %ERRORLEVEL% equ 0 goto mainEnd 82 + 83 + :fail 84 + rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 85 + rem the _cmd.exe /c_ return code! 86 + set EXIT_CODE=%ERRORLEVEL% 87 + if %EXIT_CODE% equ 0 set EXIT_CODE=1 88 + if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 89 + exit /b %EXIT_CODE% 90 + 91 + :mainEnd 92 + if "%OS%"=="Windows_NT" endlocal 93 + 94 + :omega
+9
settings.gradle
··· 1 + pluginManagement { 2 + repositories { 3 + gradlePluginPortal() 4 + } 5 + } 6 + 7 + plugins { 8 + id 'org.gradle.toolchains.foojay-resolver-convention' version '1.0.0' 9 + }
+114
src/main/java/dev/equinoxx/dailyrewards/Config.java
··· 1 + package dev.equinoxx.dailyrewards; 2 + 3 + import java.util.ArrayList; 4 + import java.util.List; 5 + import net.minecraft.core.registries.BuiltInRegistries; 6 + import net.minecraft.resources.ResourceLocation; 7 + import net.neoforged.bus.api.SubscribeEvent; 8 + import net.neoforged.fml.common.EventBusSubscriber; 9 + import net.neoforged.fml.event.config.ModConfigEvent; 10 + import net.neoforged.neoforge.common.ModConfigSpec; 11 + 12 + @EventBusSubscriber(modid = DailyRewards.MODID) 13 + public class Config { 14 + 15 + private static final ModConfigSpec.Builder BUILDER = 16 + new ModConfigSpec.Builder().comment( 17 + "Server-side daily reward configuration.", 18 + "Edit this file to customize what players receive when they run /dailyrewards." 19 + ); 20 + 21 + private static final ModConfigSpec.ConfigValue< 22 + List<? extends String> 23 + > CLAIM_REWARDS_RAW = BUILDER.comment( 24 + "List of daily reward entries in compact syntax.", 25 + "Each entry must look like: item_id=count", 26 + "Example: [\"minecraft:gold_ingot=5\", \"minecraft:bread=2\"]", 27 + "The item id must be a valid registry name and the count must be at least 1.", 28 + "Use one entry per reward item; duplicate item ids are allowed if you want separate stacks." 29 + ).defineListAllowEmpty( 30 + "claimRewards", 31 + List.of("minecraft:iron_ingot=1"), 32 + () -> "", 33 + Config::validateRewardEntry 34 + ); 35 + 36 + public static volatile List<ClaimItem> CLAIM_ITEMS = List.of( 37 + new ClaimItem("minecraft:iron_ingot", 1) 38 + ); 39 + 40 + static final ModConfigSpec SPEC = BUILDER.build(); 41 + 42 + private Config() {} 43 + 44 + @SubscribeEvent 45 + public static void onConfigReload(final ModConfigEvent event) { 46 + List<? extends String> rawEntries = CLAIM_REWARDS_RAW.get(); 47 + List<ClaimItem> items = new ArrayList<>(rawEntries.size()); 48 + 49 + for (String rawEntry : rawEntries) { 50 + ClaimItem parsed = parseRewardEntry(rawEntry); 51 + if (parsed != null) { 52 + items.add(parsed); 53 + } 54 + } 55 + 56 + if (items.isEmpty()) { 57 + items = List.of(new ClaimItem("minecraft:iron_ingot", 1)); 58 + } 59 + 60 + CLAIM_ITEMS = List.copyOf(items); 61 + } 62 + 63 + private static boolean validateRewardEntry(final Object obj) { 64 + if (!(obj instanceof String entry)) { 65 + return false; 66 + } 67 + 68 + return parseRewardEntry(entry) != null; 69 + } 70 + 71 + private static ClaimItem parseRewardEntry(String rawEntry) { 72 + if (rawEntry == null) { 73 + return null; 74 + } 75 + 76 + String entry = rawEntry.trim(); 77 + if (entry.isEmpty()) { 78 + return null; 79 + } 80 + 81 + int separatorIndex = entry.lastIndexOf('='); 82 + if (separatorIndex <= 0 || separatorIndex >= entry.length() - 1) { 83 + return null; 84 + } 85 + 86 + String itemId = entry.substring(0, separatorIndex).trim(); 87 + String countText = entry.substring(separatorIndex + 1).trim(); 88 + 89 + if (itemId.isEmpty() || countText.isEmpty()) { 90 + return null; 91 + } 92 + 93 + // The item id must be a valid registry name like `minecraft:iron_ingot`. 94 + // The count is the number of items to give for that reward entry. 95 + if ( 96 + !BuiltInRegistries.ITEM.containsKey(ResourceLocation.parse(itemId)) 97 + ) { 98 + return null; 99 + } 100 + 101 + try { 102 + int count = Integer.parseInt(countText); 103 + if (count < 1) { 104 + return null; 105 + } 106 + 107 + return new ClaimItem(itemId, count); 108 + } catch (NumberFormatException exception) { 109 + return null; 110 + } 111 + } 112 + 113 + public record ClaimItem(String itemId, int count) {} 114 + }
+89
src/main/java/dev/equinoxx/dailyrewards/DailyRewards.java
··· 1 + package dev.equinoxx.dailyrewards; 2 + 3 + import com.mojang.logging.LogUtils; 4 + import dev.equinoxx.dailyrewards.command.DailyRewardCommand; 5 + import dev.equinoxx.dailyrewards.persistence.ClaimPersistence; 6 + import dev.equinoxx.dailyrewards.service.DailyRewardService; 7 + import dev.equinoxx.dailyrewards.service.ReminderService; 8 + import dev.equinoxx.dailyrewards.service.RewardService; 9 + import java.nio.file.Path; 10 + import net.neoforged.bus.api.SubscribeEvent; 11 + import net.neoforged.fml.ModContainer; 12 + import net.neoforged.fml.common.Mod; 13 + import net.neoforged.fml.config.ModConfig; 14 + import net.neoforged.neoforge.common.NeoForge; 15 + import net.neoforged.neoforge.event.RegisterCommandsEvent; 16 + import net.neoforged.neoforge.event.entity.player.PlayerEvent; 17 + import net.neoforged.neoforge.event.server.ServerStoppingEvent; 18 + import net.neoforged.neoforge.event.tick.ServerTickEvent; 19 + import org.slf4j.Logger; 20 + 21 + /** 22 + * Main mod bootstrap. 23 + * 24 + * <p>This class wires together the core mod components: 25 + * the claim persistence layer, reward handling, reminder handling, 26 + * and the command/event registration used by the server. 27 + */ 28 + @Mod(DailyRewards.MODID) 29 + public class DailyRewards { 30 + 31 + public static final String MODID = "dailyrewards"; 32 + public static final Logger LOGGER = LogUtils.getLogger(); 33 + 34 + private final DailyRewardService dailyRewardService; 35 + private final DailyRewardCommand dailyRewardCommand; 36 + 37 + public DailyRewards(ModContainer modContainer) { 38 + modContainer.registerConfig(ModConfig.Type.COMMON, Config.SPEC); 39 + 40 + Path claimsFile = Path.of("config", MODID, "claims.txt"); 41 + ClaimPersistence claimPersistence = new ClaimPersistence(claimsFile); 42 + RewardService rewardService = new RewardService(); 43 + ReminderService reminderService = new ReminderService(); 44 + 45 + this.dailyRewardService = new DailyRewardService( 46 + LOGGER, 47 + claimPersistence, 48 + rewardService 49 + ); 50 + this.dailyRewardCommand = new DailyRewardCommand( 51 + dailyRewardService, 52 + claimPersistence, 53 + reminderService 54 + ); 55 + 56 + NeoForge.EVENT_BUS.register(this); 57 + 58 + LOGGER.info("Initialized Daily Rewards mod components"); 59 + } 60 + 61 + @SubscribeEvent 62 + public void onRegisterCommands(RegisterCommandsEvent event) { 63 + dailyRewardCommand.onRegisterCommands(event); 64 + } 65 + 66 + @SubscribeEvent 67 + public void onServerTick(ServerTickEvent.Post event) { 68 + dailyRewardCommand.onServerTick( 69 + event.getServer().getTickCount(), 70 + System.currentTimeMillis(), 71 + event.getServer().getPlayerList().getPlayers() 72 + ); 73 + } 74 + 75 + @SubscribeEvent 76 + public void onPlayerLoggedIn(PlayerEvent.PlayerLoggedInEvent event) { 77 + if ( 78 + event.getEntity() instanceof 79 + net.minecraft.server.level.ServerPlayer player 80 + ) { 81 + dailyRewardCommand.onPlayerLoggedIn(player); 82 + } 83 + } 84 + 85 + @SubscribeEvent 86 + public void onServerStopping(ServerStoppingEvent event) { 87 + dailyRewardCommand.onServerStopping(); 88 + } 89 + }
+217
src/main/java/dev/equinoxx/dailyrewards/command/DailyRewardCommand.java
··· 1 + package dev.equinoxx.dailyrewards.command; 2 + 3 + import com.mojang.brigadier.arguments.StringArgumentType; 4 + import com.mojang.brigadier.suggestion.Suggestions; 5 + import com.mojang.brigadier.suggestion.SuggestionsBuilder; 6 + import dev.equinoxx.dailyrewards.persistence.ClaimPersistence; 7 + import dev.equinoxx.dailyrewards.service.DailyRewardService; 8 + import dev.equinoxx.dailyrewards.service.ReminderService; 9 + import java.util.UUID; 10 + import java.util.concurrent.CompletableFuture; 11 + import net.minecraft.commands.CommandSourceStack; 12 + import net.minecraft.commands.Commands; 13 + import net.minecraft.network.chat.Component; 14 + import net.minecraft.server.level.ServerPlayer; 15 + import net.neoforged.bus.api.SubscribeEvent; 16 + import net.neoforged.neoforge.event.RegisterCommandsEvent; 17 + 18 + public final class DailyRewardCommand { 19 + 20 + private final DailyRewardService dailyRewardService; 21 + private final ClaimPersistence claimPersistence; 22 + private final ReminderService reminderService; 23 + 24 + public DailyRewardCommand( 25 + DailyRewardService dailyRewardService, 26 + ClaimPersistence claimPersistence, 27 + ReminderService reminderService 28 + ) { 29 + this.dailyRewardService = dailyRewardService; 30 + this.claimPersistence = claimPersistence; 31 + this.reminderService = reminderService; 32 + } 33 + 34 + @SubscribeEvent 35 + public void onRegisterCommands(RegisterCommandsEvent event) { 36 + var dispatcher = event.getDispatcher(); 37 + dispatcher.register( 38 + Commands.literal("dailyrewards") 39 + .requires(source -> source.hasPermission(0)) 40 + .executes(context -> { 41 + if ( 42 + !(context.getSource().getEntity() instanceof 43 + ServerPlayer player) 44 + ) { 45 + context 46 + .getSource() 47 + .sendFailure( 48 + Component.literal( 49 + "This command can only be used by a player." 50 + ) 51 + ); 52 + return 0; 53 + } 54 + 55 + long now = System.currentTimeMillis(); 56 + long remainingMs = 57 + dailyRewardService.getRemainingCooldownMs( 58 + player.getUUID(), 59 + now 60 + ); 61 + 62 + if (remainingMs > 0L) { 63 + long remainingHours = remainingMs / 3_600_000L; 64 + long remainingMinutes = 65 + (remainingMs % 3_600_000L) / 60_000L; 66 + 67 + player.sendSystemMessage( 68 + Component.literal( 69 + "You can claim your next daily reward in " + 70 + remainingHours + 71 + "h " + 72 + remainingMinutes + 73 + "m." 74 + ) 75 + ); 76 + return 0; 77 + } 78 + 79 + return dailyRewardService.claimRewards(player); 80 + }) 81 + .then( 82 + Commands.literal("reset") 83 + .requires(source -> source.hasPermission(2)) 84 + .then( 85 + Commands.argument( 86 + "player", 87 + StringArgumentType.word() 88 + ) 89 + .suggests((context, builder) -> 90 + suggestTargets(context.getSource(), builder) 91 + ) 92 + .executes(context -> { 93 + String targetValue = 94 + StringArgumentType.getString( 95 + context, 96 + "player" 97 + ); 98 + 99 + UUID targetUuid = resolvePlayerUuid( 100 + context.getSource(), 101 + targetValue 102 + ); 103 + 104 + if (targetUuid == null) { 105 + context 106 + .getSource() 107 + .sendFailure( 108 + Component.literal( 109 + "Player not found." 110 + ) 111 + ); 112 + return 0; 113 + } 114 + 115 + dailyRewardService.resetClaimTimer( 116 + targetUuid 117 + ); 118 + claimPersistence.save( 119 + dailyRewardService.getLastClaimTimes() 120 + ); 121 + 122 + context 123 + .getSource() 124 + .sendSuccess( 125 + () -> 126 + Component.literal( 127 + "Reset daily reward timer for " + 128 + targetValue + 129 + "." 130 + ), 131 + true 132 + ); 133 + 134 + return 1; 135 + }) 136 + ) 137 + ) 138 + ); 139 + } 140 + 141 + public void onPlayerLoggedIn(ServerPlayer player) { 142 + reminderService.clearReminderState(player.getUUID()); 143 + 144 + Long lastClaimTimestamp = dailyRewardService 145 + .getLastClaimTimes() 146 + .get(player.getUUID()); 147 + 148 + if ( 149 + reminderService.hasClaimAvailable( 150 + lastClaimTimestamp, 151 + System.currentTimeMillis() 152 + ) 153 + ) { 154 + reminderService.sendClaimAvailableNotification(player); 155 + } 156 + } 157 + 158 + public void onServerTick( 159 + long currentTick, 160 + long now, 161 + Iterable<ServerPlayer> players 162 + ) { 163 + for (ServerPlayer player : players) { 164 + Long lastClaimTimestamp = dailyRewardService 165 + .getLastClaimTimes() 166 + .get(player.getUUID()); 167 + 168 + if ( 169 + reminderService.shouldSendReminder( 170 + player.getUUID(), 171 + currentTick, 172 + now, 173 + lastClaimTimestamp 174 + ) 175 + ) { 176 + reminderService.sendClaimAvailableNotification(player); 177 + } 178 + } 179 + } 180 + 181 + public void onServerStopping() { 182 + claimPersistence.save(dailyRewardService.getLastClaimTimes()); 183 + reminderService.clearAllReminderState(); 184 + dailyRewardService.clearAllClaimTimes(); 185 + } 186 + 187 + private UUID resolvePlayerUuid( 188 + CommandSourceStack source, 189 + String targetValue 190 + ) { 191 + try { 192 + return UUID.fromString(targetValue); 193 + } catch (IllegalArgumentException ignored) { 194 + // Fall back to online player name lookup. 195 + } 196 + 197 + ServerPlayer target = source 198 + .getServer() 199 + .getPlayerList() 200 + .getPlayerByName(targetValue); 201 + return target != null ? target.getUUID() : null; 202 + } 203 + 204 + private CompletableFuture<Suggestions> suggestTargets( 205 + CommandSourceStack source, 206 + SuggestionsBuilder builder 207 + ) { 208 + for (ServerPlayer player : source 209 + .getServer() 210 + .getPlayerList() 211 + .getPlayers()) { 212 + builder.suggest(player.getName().getString()); 213 + builder.suggest(player.getUUID().toString()); 214 + } 215 + return builder.buildFuture(); 216 + } 217 + }
+110
src/main/java/dev/equinoxx/dailyrewards/persistence/ClaimPersistence.java
··· 1 + package dev.equinoxx.dailyrewards.persistence; 2 + 3 + import dev.equinoxx.dailyrewards.DailyRewards; 4 + import java.io.IOException; 5 + import java.nio.charset.StandardCharsets; 6 + import java.nio.file.Files; 7 + import java.nio.file.Path; 8 + import java.util.HashMap; 9 + import java.util.Map; 10 + import java.util.UUID; 11 + import org.slf4j.Logger; 12 + 13 + public final class ClaimPersistence { 14 + 15 + private static final Logger LOGGER = DailyRewards.LOGGER; 16 + 17 + // Claim timestamps are stored as simple `uuid=timestamp` lines. 18 + // This format is easy to read, easy to edit, and keeps the save file human-friendly. 19 + // Format: 20 + // <player-uuid>=<last-claim-time-millis> 21 + // Example: 22 + // 123e4567-e89b-12d3-a456-426614174000=1735689600000 23 + private final Path claimsFile; 24 + 25 + public ClaimPersistence(Path claimsFile) { 26 + this.claimsFile = claimsFile; 27 + } 28 + 29 + public Map<UUID, Long> load() { 30 + if (!Files.exists(claimsFile)) { 31 + return Map.of(); 32 + } 33 + 34 + Map<UUID, Long> loadedClaims = new HashMap<>(); 35 + 36 + try { 37 + for (String line : Files.readAllLines( 38 + claimsFile, 39 + StandardCharsets.UTF_8 40 + )) { 41 + String entry = line.trim(); 42 + if (entry.isEmpty() || entry.startsWith("#")) { 43 + continue; 44 + } 45 + 46 + int separatorIndex = entry.indexOf('='); 47 + if ( 48 + separatorIndex <= 0 || separatorIndex >= entry.length() - 1 49 + ) { 50 + continue; 51 + } 52 + 53 + String uuidText = entry.substring(0, separatorIndex).trim(); 54 + String timestampText = entry 55 + .substring(separatorIndex + 1) 56 + .trim(); 57 + 58 + try { 59 + UUID uuid = UUID.fromString(uuidText); 60 + long timestamp = Long.parseLong(timestampText); 61 + loadedClaims.put(uuid, timestamp); 62 + } catch (IllegalArgumentException ignored) { 63 + // Keep loading the rest of the file even if one line is malformed. 64 + LOGGER.warn( 65 + "Skipping malformed claim entry in {}: {}", 66 + claimsFile, 67 + entry 68 + ); 69 + } 70 + } 71 + } catch (IOException exception) { 72 + LOGGER.error( 73 + "Failed to load daily reward claims from disk", 74 + exception 75 + ); 76 + return Map.of(); 77 + } 78 + 79 + return Map.copyOf(loadedClaims); 80 + } 81 + 82 + public void save(Map<UUID, Long> claims) { 83 + try { 84 + Path parent = claimsFile.getParent(); 85 + if (parent != null) { 86 + Files.createDirectories(parent); 87 + } 88 + 89 + StringBuilder builder = new StringBuilder(); 90 + for (Map.Entry<UUID, Long> entry : claims.entrySet()) { 91 + builder 92 + .append(entry.getKey()) 93 + .append('=') 94 + .append(entry.getValue()) 95 + .append('\n'); 96 + } 97 + 98 + Files.writeString( 99 + claimsFile, 100 + builder.toString(), 101 + StandardCharsets.UTF_8 102 + ); 103 + } catch (IOException exception) { 104 + LOGGER.error( 105 + "Failed to save daily reward claims to disk", 106 + exception 107 + ); 108 + } 109 + } 110 + }
+151
src/main/java/dev/equinoxx/dailyrewards/service/DailyRewardService.java
··· 1 + package dev.equinoxx.dailyrewards.service; 2 + 3 + import dev.equinoxx.dailyrewards.persistence.ClaimPersistence; 4 + import java.util.Map; 5 + import java.util.UUID; 6 + import java.util.concurrent.ConcurrentHashMap; 7 + import net.minecraft.commands.CommandSourceStack; 8 + import net.minecraft.network.chat.Component; 9 + import net.minecraft.server.level.ServerPlayer; 10 + import org.slf4j.Logger; 11 + 12 + /** 13 + * Stores and manages daily reward claim state. 14 + * 15 + * <p>This service keeps the last claim timestamp for each player, enforces the 24 hour cooldown, 16 + * loads and saves claim data through {@link ClaimPersistence}, and exposes the data used by the 17 + * command and reminder systems. 18 + */ 19 + public final class DailyRewardService { 20 + 21 + private static final long CLAIM_COOLDOWN_MS = 24L * 60L * 60L * 1000L; 22 + 23 + private final Logger logger; 24 + private final ClaimPersistence persistence; 25 + private final RewardService rewardService; 26 + private final Map<UUID, Long> lastClaimTimes = new ConcurrentHashMap<>(); 27 + 28 + public DailyRewardService( 29 + Logger logger, 30 + ClaimPersistence persistence, 31 + RewardService rewardService 32 + ) { 33 + this.logger = logger; 34 + this.persistence = persistence; 35 + this.rewardService = rewardService; 36 + 37 + loadClaims(); 38 + 39 + if (this.logger != null) { 40 + this.logger.info( 41 + "Loaded {} daily reward claim entries", 42 + lastClaimTimes.size() 43 + ); 44 + } 45 + } 46 + 47 + public Map<UUID, Long> getLastClaimTimes() { 48 + return lastClaimTimes; 49 + } 50 + 51 + public void loadClaims() { 52 + if (persistence == null) { 53 + return; 54 + } 55 + 56 + lastClaimTimes.clear(); 57 + lastClaimTimes.putAll(persistence.load()); 58 + } 59 + 60 + public void saveClaims() { 61 + if (persistence == null) { 62 + return; 63 + } 64 + 65 + persistence.save(lastClaimTimes); 66 + } 67 + 68 + public int claimRewards(ServerPlayer player) { 69 + long now = System.currentTimeMillis(); 70 + Long lastClaim = lastClaimTimes.get(player.getUUID()); 71 + 72 + if (lastClaim != null) { 73 + long elapsed = now - lastClaim; 74 + if (elapsed < CLAIM_COOLDOWN_MS) { 75 + long remainingMs = CLAIM_COOLDOWN_MS - elapsed; 76 + long remainingHours = remainingMs / 3_600_000L; 77 + long remainingMinutes = (remainingMs % 3_600_000L) / 60_000L; 78 + 79 + player.sendSystemMessage( 80 + Component.literal( 81 + "You can claim your next daily reward in " + 82 + remainingHours + 83 + "h " + 84 + remainingMinutes + 85 + "m." 86 + ) 87 + ); 88 + return 0; 89 + } 90 + } 91 + 92 + lastClaimTimes.put(player.getUUID(), now); 93 + saveClaims(); 94 + 95 + rewardService.giveDailyRewards(player); 96 + player.sendSystemMessage( 97 + Component.literal("You claimed your daily reward.") 98 + ); 99 + return 1; 100 + } 101 + 102 + public long getRemainingCooldownMs(UUID playerId, long now) { 103 + Long lastClaim = lastClaimTimes.get(playerId); 104 + if (lastClaim == null) { 105 + return 0L; 106 + } 107 + 108 + long remainingMs = CLAIM_COOLDOWN_MS - (now - lastClaim); 109 + return Math.max(0L, remainingMs); 110 + } 111 + 112 + public boolean hasClaimAvailable(UUID playerId, long now) { 113 + return getRemainingCooldownMs(playerId, now) == 0L; 114 + } 115 + 116 + public void resetClaimTimer(UUID playerUuid) { 117 + lastClaimTimes.remove(playerUuid); 118 + saveClaims(); 119 + } 120 + 121 + public void clearAllClaimTimes() { 122 + lastClaimTimes.clear(); 123 + saveClaims(); 124 + } 125 + 126 + public UUID resolvePlayerUuid( 127 + CommandSourceStack source, 128 + String targetValue 129 + ) { 130 + try { 131 + return UUID.fromString(targetValue); 132 + } catch (IllegalArgumentException ignored) { 133 + // Fall back to online player name lookup. 134 + } 135 + 136 + ServerPlayer target = source 137 + .getServer() 138 + .getPlayerList() 139 + .getPlayerByName(targetValue); 140 + return target != null ? target.getUUID() : null; 141 + } 142 + 143 + public void logLoadedClaims() { 144 + if (logger != null) { 145 + logger.info( 146 + "Loaded {} daily reward claim entries", 147 + lastClaimTimes.size() 148 + ); 149 + } 150 + } 151 + }
+87
src/main/java/dev/equinoxx/dailyrewards/service/ReminderService.java
··· 1 + package dev.equinoxx.dailyrewards.service; 2 + 3 + import java.util.Map; 4 + import java.util.UUID; 5 + import java.util.concurrent.ConcurrentHashMap; 6 + import net.minecraft.ChatFormatting; 7 + import net.minecraft.network.chat.Component; 8 + import net.minecraft.network.chat.HoverEvent; 9 + import net.minecraft.network.chat.Style; 10 + import net.minecraft.server.level.ServerPlayer; 11 + 12 + public final class ReminderService { 13 + 14 + private static final long CLAIM_COOLDOWN_MS = 24L * 60L * 60L * 1000L; 15 + private static final long REMINDER_INTERVAL_MS = 60L * 1000L; 16 + private static final long REMINDER_INTERVAL_TICKS = 17 + REMINDER_INTERVAL_MS / 50L; 18 + 19 + private static final Component READY_MESSAGE = Component.literal( 20 + "Your daily reward is ready to claim! " 21 + ).withStyle(ChatFormatting.YELLOW); 22 + 23 + private static final Component CLAIM_NOW = Component.literal( 24 + "[Claim Now]" 25 + ).withStyle( 26 + Style.EMPTY.withColor(ChatFormatting.GREEN) 27 + .withUnderlined(true) 28 + .withClickEvent( 29 + new net.minecraft.network.chat.ClickEvent( 30 + net.minecraft.network.chat.ClickEvent.Action.RUN_COMMAND, 31 + "/dailyrewards" 32 + ) 33 + ) 34 + .withHoverEvent( 35 + new HoverEvent( 36 + HoverEvent.Action.SHOW_TEXT, 37 + Component.literal("Click to run /dailyrewards") 38 + ) 39 + ) 40 + ); 41 + 42 + private final Map<UUID, Long> lastReminderTicks = new ConcurrentHashMap<>(); 43 + 44 + // Returns true when the supplied claim timestamp has expired and the player can claim again. 45 + public boolean hasClaimAvailable(Long lastClaimTimestamp, long now) { 46 + if (lastClaimTimestamp == null) { 47 + return true; 48 + } 49 + 50 + return now - lastClaimTimestamp >= CLAIM_COOLDOWN_MS; 51 + } 52 + 53 + // Tracks reminder cadence per player so the server does not spam the same message every tick. 54 + public boolean shouldSendReminder( 55 + UUID playerId, 56 + long currentTick, 57 + long now, 58 + Long lastClaimTimestamp 59 + ) { 60 + if (!hasClaimAvailable(lastClaimTimestamp, now)) { 61 + return false; 62 + } 63 + 64 + long lastReminderTick = lastReminderTicks.getOrDefault(playerId, 0L); 65 + if (currentTick - lastReminderTick < REMINDER_INTERVAL_TICKS) { 66 + return false; 67 + } 68 + 69 + lastReminderTicks.put(playerId, currentTick); 70 + return true; 71 + } 72 + 73 + // Sends the clickable reminder message shown when a reward is ready. 74 + public void sendClaimAvailableNotification(ServerPlayer player) { 75 + player.sendSystemMessage(READY_MESSAGE.copy().append(CLAIM_NOW.copy())); 76 + } 77 + 78 + // Clears reminder state for one player, usually when they reconnect or claim their reward. 79 + public void clearReminderState(UUID playerId) { 80 + lastReminderTicks.remove(playerId); 81 + } 82 + 83 + // Clears all reminder state when the server stops. 84 + public void clearAllReminderState() { 85 + lastReminderTicks.clear(); 86 + } 87 + }
+51
src/main/java/dev/equinoxx/dailyrewards/service/RewardService.java
··· 1 + package dev.equinoxx.dailyrewards.service; 2 + 3 + import dev.equinoxx.dailyrewards.Config; 4 + import net.minecraft.core.registries.BuiltInRegistries; 5 + import net.minecraft.resources.ResourceLocation; 6 + import net.minecraft.server.level.ServerPlayer; 7 + import net.minecraft.world.item.Item; 8 + import net.minecraft.world.item.ItemStack; 9 + import net.minecraft.world.item.Items; 10 + 11 + /** 12 + * Gives the configured daily rewards to a player. 13 + * 14 + * <p>The reward list comes from the server config as compact strings in the form 15 + * {@code item_id=count}. For example: 16 + * 17 + * <pre> 18 + * minecraft:iron_ingot=1 19 + * minecraft:diamond=2 20 + * </pre> 21 + * 22 + * <p>Each entry is parsed into an item registry id and an amount, then added to the player's 23 + * inventory. If the inventory is full, the item stack is dropped instead. 24 + */ 25 + public final class RewardService { 26 + 27 + public void giveDailyRewards(ServerPlayer player) { 28 + for (Config.ClaimItem claimItem : Config.CLAIM_ITEMS) { 29 + giveSingleReward(player, claimItem); 30 + } 31 + } 32 + 33 + private void giveSingleReward( 34 + ServerPlayer player, 35 + Config.ClaimItem claimItem 36 + ) { 37 + ResourceLocation itemId = ResourceLocation.parse(claimItem.itemId()); 38 + Item item = BuiltInRegistries.ITEM.get(itemId); 39 + 40 + if (item == Items.AIR) { 41 + return; 42 + } 43 + 44 + ItemStack stack = new ItemStack(item, claimItem.count()); 45 + boolean added = player.getInventory().add(stack); 46 + 47 + if (!added) { 48 + player.drop(stack, false); 49 + } 50 + } 51 + }
+9
src/main/resources/assets/dailyrewards/lang/en_us.json
··· 1 + { 2 + "dailyrewards.configuration.title": "Daily Rewards Configuration", 3 + "dailyrewards.configuration.section.dailyrewards.common.toml": "Daily Rewards Configuration", 4 + "dailyrewards.configuration.section.dailyrewards.common.toml.title": "Daily Rewards Configuration", 5 + "dailyrewards.configuration.claimRewards": "Daily Reward Items", 6 + "dailyrewards.configuration.claimRewards.tooltip": "List reward entries in compact `item_id=count` format, for example `minecraft:iron_ingot=1`.", 7 + "dailyrewards.configuration.claimRewards.entry": "Reward Entry", 8 + "dailyrewards.configuration.claimRewards.entry.tooltip": "Use the compact format `item_id=count`. The item ID must be a valid item registry name and the count must be at least 1." 9 + }
+29
src/main/templates/META-INF/neoforge.mods.toml
··· 1 + # Metadata for the Daily Rewards mod. 2 + # This file is processed by Gradle to produce the final NeoForge mod metadata. 3 + 4 + modLoader = "javafml" 5 + loaderVersion = "${loader_version_range}" 6 + license = "${mod_license}" 7 + 8 + [[mods]] 9 + modId = "${mod_id}" 10 + version = "${mod_version}" 11 + displayName = "${mod_name}" 12 + authors = "Equinoxx" 13 + description = ''' 14 + Adds daily claimable rewards for players. 15 + ''' 16 + 17 + [[dependencies.${mod_id}]] 18 + modId = "neoforge" 19 + type = "required" 20 + versionRange = "[${neo_version},)" 21 + ordering = "NONE" 22 + side = "SERVER" 23 + 24 + [[dependencies.${mod_id}]] 25 + modId = "minecraft" 26 + type = "required" 27 + versionRange = "${minecraft_version_range}" 28 + ordering = "NONE" 29 + side = "SERVER"