diff options
author | SoniEx2 <endermoneymod@gmail.com> | 2025-03-04 22:45:19 -0300 |
---|---|---|
committer | SoniEx2 <endermoneymod@gmail.com> | 2025-03-04 22:45:19 -0300 |
commit | 79ff3692b9462fc79d93bd74213ce6904340fc13 (patch) | |
tree | 22055c038783b87cceffe3d2220cc2b568a4493d |
First public commit
55 files changed, 2277 insertions, 0 deletions
diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..097f9f9 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,9 @@ +# +# https://help.github.com/articles/dealing-with-line-endings/ +# +# Linux start script should use lf +/gradlew text eol=lf + +# These are Windows script files and should use crlf +*.bat text eol=crlf + diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..a4e3e0b --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,37 @@ +# Automatically build the project and run any configured tests for every push +# and submitted pull request. This can help catch issues that only occur on +# certain platforms or Java versions, and provides a first line of defence +# against bad commits. + +name: build +on: [pull_request, push] + +jobs: + build: + strategy: + matrix: + # Use these Java versions + java: [ + 21, # Current Java LTS + ] + runs-on: ubuntu-22.04 + steps: + - name: checkout repository + uses: actions/checkout@v4 + - name: validate gradle wrapper + uses: gradle/actions/wrapper-validation@v4 + - name: setup jdk ${{ matrix.java }} + uses: actions/setup-java@v4 + with: + java-version: ${{ matrix.java }} + distribution: 'microsoft' + - name: make gradle wrapper executable + run: chmod +x ./gradlew + - name: build + run: ./gradlew build + - name: capture build artifacts + if: ${{ matrix.java == '21' }} # Only upload artifacts built from latest java + uses: actions/upload-artifact@v4 + with: + name: Artifacts + path: build/libs/ \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bab5adf --- /dev/null +++ b/.gitignore @@ -0,0 +1,42 @@ +# gradle + +.gradle/ +build/ +out/ +classes/ + +# eclipse + +*.launch + +# idea + +.idea/ +*.iml +*.ipr +*.iws + +# vscode + +.settings/ +.vscode/ +bin/ +.classpath +.project + +# macos + +*.DS_Store + +# fabric + +run/ + +# java + +hs_err_*.log +replay_*.log +*.hprof +*.jfr + +/src/main/resources/opus.wasm diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..9b44720 --- /dev/null +++ b/LICENSE @@ -0,0 +1,419 @@ +code is MIT license: + +Copyright (C) 2025 Soni L. + +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: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +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. + +image assets are from https://github.com/malcolmriley/unused-textures , under the following license: + +Attribution 4.0 International + +======================================================================= + +Creative Commons Corporation ("Creative Commons") is not a law firm and +does not provide legal services or legal advice. Distribution of +Creative Commons public licenses does not create a lawyer-client or +other relationship. Creative Commons makes its licenses and related +information available on an "as-is" basis. Creative Commons gives no +warranties regarding its licenses, any material licensed under their +terms and conditions, or any related information. Creative Commons +disclaims all liability for damages resulting from their use to the +fullest extent possible. + +Using Creative Commons Public Licenses + +Creative Commons public licenses provide a standard set of terms and +conditions that creators and other rights holders may use to share +original works of authorship and other material subject to copyright +and certain other rights specified in the public license below. The +following considerations are for informational purposes only, are not +exhaustive, and do not form part of our licenses. + + Considerations for licensors: Our public licenses are + intended for use by those authorized to give the public + permission to use material in ways otherwise restricted by + copyright and certain other rights. Our licenses are + irrevocable. Licensors should read and understand the terms + and conditions of the license they choose before applying it. + Licensors should also secure all rights necessary before + applying our licenses so that the public can reuse the + material as expected. Licensors should clearly mark any + material not subject to the license. This includes other CC- + licensed material, or material used under an exception or + limitation to copyright. More considerations for licensors: + wiki.creativecommons.org/Considerations_for_licensors + + Considerations for the public: By using one of our public + licenses, a licensor grants the public permission to use the + licensed material under specified terms and conditions. If + the licensor's permission is not necessary for any reason--for + example, because of any applicable exception or limitation to + copyright--then that use is not regulated by the license. Our + licenses grant only permissions under copyright and certain + other rights that a licensor has authority to grant. Use of + the licensed material may still be restricted for other + reasons, including because others have copyright or other + rights in the material. A licensor may make special requests, + such as asking that all changes be marked or described. + Although not required by our licenses, you are encouraged to + respect those requests where reasonable. More considerations + for the public: + wiki.creativecommons.org/Considerations_for_licensees + +======================================================================= + +Creative Commons Attribution 4.0 International Public License + +By exercising the Licensed Rights (defined below), You accept and agree +to be bound by the terms and conditions of this Creative Commons +Attribution 4.0 International Public License ("Public License"). To the +extent this Public License may be interpreted as a contract, You are +granted the Licensed Rights in consideration of Your acceptance of +these terms and conditions, and the Licensor grants You such rights in +consideration of benefits the Licensor receives from making the +Licensed Material available under these terms and conditions. + + +Section 1 -- Definitions. + + a. Adapted Material means material subject to Copyright and Similar + Rights that is derived from or based upon the Licensed Material + and in which the Licensed Material is translated, altered, + arranged, transformed, or otherwise modified in a manner requiring + permission under the Copyright and Similar Rights held by the + Licensor. For purposes of this Public License, where the Licensed + Material is a musical work, performance, or sound recording, + Adapted Material is always produced where the Licensed Material is + synched in timed relation with a moving image. + + b. Adapter's License means the license You apply to Your Copyright + and Similar Rights in Your contributions to Adapted Material in + accordance with the terms and conditions of this Public License. + + c. Copyright and Similar Rights means copyright and/or similar rights + closely related to copyright including, without limitation, + performance, broadcast, sound recording, and Sui Generis Database + Rights, without regard to how the rights are labeled or + categorized. For purposes of this Public License, the rights + specified in Section 2(b)(1)-(2) are not Copyright and Similar + Rights. + + d. Effective Technological Measures means those measures that, in the + absence of proper authority, may not be circumvented under laws + fulfilling obligations under Article 11 of the WIPO Copyright + Treaty adopted on December 20, 1996, and/or similar international + agreements. + + e. Exceptions and Limitations means fair use, fair dealing, and/or + any other exception or limitation to Copyright and Similar Rights + that applies to Your use of the Licensed Material. + + f. Licensed Material means the artistic or literary work, database, + or other material to which the Licensor applied this Public + License. + + g. Licensed Rights means the rights granted to You subject to the + terms and conditions of this Public License, which are limited to + all Copyright and Similar Rights that apply to Your use of the + Licensed Material and that the Licensor has authority to license. + + h. Licensor means the individual(s) or entity(ies) granting rights + under this Public License. + + i. Share means to provide material to the public by any means or + process that requires permission under the Licensed Rights, such + as reproduction, public display, public performance, distribution, + dissemination, communication, or importation, and to make material + available to the public including in ways that members of the + public may access the material from a place and at a time + individually chosen by them. + + j. Sui Generis Database Rights means rights other than copyright + resulting from Directive 96/9/EC of the European Parliament and of + the Council of 11 March 1996 on the legal protection of databases, + as amended and/or succeeded, as well as other essentially + equivalent rights anywhere in the world. + + k. You means the individual or entity exercising the Licensed Rights + under this Public License. Your has a corresponding meaning. + + +Section 2 -- Scope. + + a. License grant. + + 1. Subject to the terms and conditions of this Public License, + the Licensor hereby grants You a worldwide, royalty-free, + non-sublicensable, non-exclusive, irrevocable license to + exercise the Licensed Rights in the Licensed Material to: + + a. reproduce and Share the Licensed Material, in whole or + in part; and + + b. produce, reproduce, and Share Adapted Material. + + 2. Exceptions and Limitations. For the avoidance of doubt, where + Exceptions and Limitations apply to Your use, this Public + License does not apply, and You do not need to comply with + its terms and conditions. + + 3. Term. The term of this Public License is specified in Section + 6(a). + + 4. Media and formats; technical modifications allowed. The + Licensor authorizes You to exercise the Licensed Rights in + all media and formats whether now known or hereafter created, + and to make technical modifications necessary to do so. The + Licensor waives and/or agrees not to assert any right or + authority to forbid You from making technical modifications + necessary to exercise the Licensed Rights, including + technical modifications necessary to circumvent Effective + Technological Measures. For purposes of this Public License, + simply making modifications authorized by this Section 2(a) + (4) never produces Adapted Material. + + 5. Downstream recipients. + + a. Offer from the Licensor -- Licensed Material. Every + recipient of the Licensed Material automatically + receives an offer from the Licensor to exercise the + Licensed Rights under the terms and conditions of this + Public License. + + b. No downstream restrictions. You may not offer or impose + any additional or different terms or conditions on, or + apply any Effective Technological Measures to, the + Licensed Material if doing so restricts exercise of the + Licensed Rights by any recipient of the Licensed + Material. + + 6. No endorsement. Nothing in this Public License constitutes or + may be construed as permission to assert or imply that You + are, or that Your use of the Licensed Material is, connected + with, or sponsored, endorsed, or granted official status by, + the Licensor or others designated to receive attribution as + provided in Section 3(a)(1)(A)(i). + + b. Other rights. + + 1. Moral rights, such as the right of integrity, are not + licensed under this Public License, nor are publicity, + privacy, and/or other similar personality rights; however, to + the extent possible, the Licensor waives and/or agrees not to + assert any such rights held by the Licensor to the limited + extent necessary to allow You to exercise the Licensed + Rights, but not otherwise. + + 2. Patent and trademark rights are not licensed under this + Public License. + + 3. To the extent possible, the Licensor waives any right to + collect royalties from You for the exercise of the Licensed + Rights, whether directly or through a collecting society + under any voluntary or waivable statutory or compulsory + licensing scheme. In all other cases the Licensor expressly + reserves any right to collect such royalties. + + +Section 3 -- License Conditions. + +Your exercise of the Licensed Rights is expressly made subject to the +following conditions. + + a. Attribution. + + 1. If You Share the Licensed Material (including in modified + form), You must: + + a. retain the following if it is supplied by the Licensor + with the Licensed Material: + + i. identification of the creator(s) of the Licensed + Material and any others designated to receive + attribution, in any reasonable manner requested by + the Licensor (including by pseudonym if + designated); + + ii. a copyright notice; + + iii. a notice that refers to this Public License; + + iv. a notice that refers to the disclaimer of + warranties; + + v. a URI or hyperlink to the Licensed Material to the + extent reasonably practicable; + + b. indicate if You modified the Licensed Material and + retain an indication of any previous modifications; and + + c. indicate the Licensed Material is licensed under this + Public License, and include the text of, or the URI or + hyperlink to, this Public License. + + 2. You may satisfy the conditions in Section 3(a)(1) in any + reasonable manner based on the medium, means, and context in + which You Share the Licensed Material. For example, it may be + reasonable to satisfy the conditions by providing a URI or + hyperlink to a resource that includes the required + information. + + 3. If requested by the Licensor, You must remove any of the + information required by Section 3(a)(1)(A) to the extent + reasonably practicable. + + 4. If You Share Adapted Material You produce, the Adapter's + License You apply must not prevent recipients of the Adapted + Material from complying with this Public License. + + +Section 4 -- Sui Generis Database Rights. + +Where the Licensed Rights include Sui Generis Database Rights that +apply to Your use of the Licensed Material: + + a. for the avoidance of doubt, Section 2(a)(1) grants You the right + to extract, reuse, reproduce, and Share all or a substantial + portion of the contents of the database; + + b. if You include all or a substantial portion of the database + contents in a database in which You have Sui Generis Database + Rights, then the database in which You have Sui Generis Database + Rights (but not its individual contents) is Adapted Material; and + + c. You must comply with the conditions in Section 3(a) if You Share + all or a substantial portion of the contents of the database. + +For the avoidance of doubt, this Section 4 supplements and does not +replace Your obligations under this Public License where the Licensed +Rights include other Copyright and Similar Rights. + + +Section 5 -- Disclaimer of Warranties and Limitation of Liability. + + a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE + EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS + AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF + ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS, + IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION, + WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR + PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS, + ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT + KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT + ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU. + + b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE + TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION, + NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT, + INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES, + COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR + USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN + ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR + DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR + IN PART, THIS LIMITATION MAY NOT APPLY TO YOU. + + c. The disclaimer of warranties and limitation of liability provided + above shall be interpreted in a manner that, to the extent + possible, most closely approximates an absolute disclaimer and + waiver of all liability. + + +Section 6 -- Term and Termination. + + a. This Public License applies for the term of the Copyright and + Similar Rights licensed here. However, if You fail to comply with + this Public License, then Your rights under this Public License + terminate automatically. + + b. Where Your right to use the Licensed Material has terminated under + Section 6(a), it reinstates: + + 1. automatically as of the date the violation is cured, provided + it is cured within 30 days of Your discovery of the + violation; or + + 2. upon express reinstatement by the Licensor. + + For the avoidance of doubt, this Section 6(b) does not affect any + right the Licensor may have to seek remedies for Your violations + of this Public License. + + c. For the avoidance of doubt, the Licensor may also offer the + Licensed Material under separate terms or conditions or stop + distributing the Licensed Material at any time; however, doing so + will not terminate this Public License. + + d. Sections 1, 5, 6, 7, and 8 survive termination of this Public + License. + + +Section 7 -- Other Terms and Conditions. + + a. The Licensor shall not be bound by any additional or different + terms or conditions communicated by You unless expressly agreed. + + b. Any arrangements, understandings, or agreements regarding the + Licensed Material not stated herein are separate from and + independent of the terms and conditions of this Public License. + + +Section 8 -- Interpretation. + + a. For the avoidance of doubt, this Public License does not, and + shall not be interpreted to, reduce, limit, restrict, or impose + conditions on any use of the Licensed Material that could lawfully + be made without permission under this Public License. + + b. To the extent possible, if any provision of this Public License is + deemed unenforceable, it shall be automatically reformed to the + minimum extent necessary to make it enforceable. If the provision + cannot be reformed, it shall be severed from this Public License + without affecting the enforceability of the remaining terms and + conditions. + + c. No term or condition of this Public License will be waived and no + failure to comply consented to unless expressly agreed to by the + Licensor. + + d. Nothing in this Public License constitutes or may be interpreted + as a limitation upon, or waiver of, any privileges and immunities + that apply to the Licensor or You, including from the legal + processes of any jurisdiction or authority. + + +======================================================================= + +Creative Commons is not a party to its public +licenses. Notwithstanding, Creative Commons may elect to apply one of +its public licenses to material it publishes and in those instances +will be considered the “Licensor.” The text of the Creative Commons +public licenses is dedicated to the public domain under the CC0 Public +Domain Dedication. Except for the limited purpose of indicating that +material is shared under a Creative Commons public license or as +otherwise permitted by the Creative Commons policies published at +creativecommons.org/policies, Creative Commons does not authorize the +use of the trademark "Creative Commons" or any other trademark or logo +of Creative Commons without its prior written consent including, +without limitation, in connection with any unauthorized modifications +to any of its public licenses or any other arrangements, +understandings, or agreements concerning use of licensed material. For +the avoidance of doubt, this paragraph does not form part of the +public licenses. + +Creative Commons may be contacted at creativecommons.org. diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..2c2c885 --- /dev/null +++ b/build.gradle @@ -0,0 +1,113 @@ +plugins { + id 'fabric-loom' version '1.9-SNAPSHOT' + id 'maven-publish' + id "org.jetbrains.kotlin.jvm" version "2.1.10" +} + +version = project.mod_version +group = project.maven_group + +base { + archivesName = project.archives_base_name +} + +repositories { + // Add repositories to retrieve artifacts from in here. + // You should only use this when depending on other mods because + // Loom adds the essential maven repositories to download Minecraft and libraries from automatically. + // See https://docs.gradle.org/current/userguide/declaring_repositories.html + // for more information about repositories. +} + +loom { + splitEnvironmentSourceSets() + + mods { + "pirate-radio" { + sourceSet sourceSets.main + sourceSet sourceSets.client + } + } + +} + +fabricApi { + configureDataGeneration { + client = true + } +} + +dependencies { + // To change the versions see the gradle.properties file + minecraft "com.mojang:minecraft:${project.minecraft_version}" + mappings "net.fabricmc:yarn:${project.yarn_mappings}:v2" + modImplementation "net.fabricmc:fabric-loader:${project.loader_version}" + + // Fabric API. This is technically optional, but you probably want it anyway. + modImplementation "net.fabricmc.fabric-api:fabric-api:${project.fabric_version}" + modImplementation "net.fabricmc:fabric-language-kotlin:${project.fabric_kotlin_version}" + + include implementation("com.dylibso.chicory:runtime:${project.chicory_version}") + include implementation("com.dylibso.chicory:wasm:${project.chicory_version}") + include implementation("com.dylibso.chicory:aot-experimental:${project.chicory_version}") + include implementation("com.github.wendykierp:JTransforms:3.1:with-dependencies") + + testImplementation "net.fabricmc:fabric-loader-junit:${project.loader_version}" + testImplementation "org.jetbrains.kotlin:kotlin-test:${project.kotlin_test_version}" +} + +test { + useJUnitPlatform() +} + +processResources { + inputs.property "version", project.version + + filesMatching("fabric.mod.json") { + expand "version": project.version + } +} + +tasks.withType(JavaCompile).configureEach { + it.options.release = 21 +} + +tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all { + kotlinOptions { + jvmTarget = 21 + } +} + +java { + // Loom will automatically attach sourcesJar to a RemapSourcesJar task and to the "build" task + // if it is present. + // If you remove this line, sources will not be generated. + withSourcesJar() + + sourceCompatibility = JavaVersion.VERSION_21 + targetCompatibility = JavaVersion.VERSION_21 +} + +jar { + from("LICENSE") { + rename { "${it}_${project.base.archivesName.get()}"} + } +} + +// configure the maven publication +publishing { + publications { + create("mavenJava", MavenPublication) { + artifactId = project.archives_base_name + from components.java + } + } + + // See https://docs.gradle.org/current/userguide/publishing_maven.html for information on how to set up publishing. + repositories { + // Add repositories to publish to here. + // Notice: This block does NOT have the same function as the block in the top level. + // The repositories here will be used for publishing your artifact, not for + // retrieving dependencies. + } +} diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..24c6f63 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,20 @@ +# Done to increase the memory available to gradle. +org.gradle.jvmargs=-Xmx1G +org.gradle.parallel=true + +# Fabric Properties +# check these on https://fabricmc.net/develop +minecraft_version=1.21.1 +yarn_mappings=1.21.1+build.3 +loader_version=0.16.10 +fabric_kotlin_version=1.13.1+kotlin.2.1.10 + +# Mod Properties +mod_version=1.0.0 +maven_group=space.autistic.radio +archives_base_name=pirate-radio + +# Dependencies +fabric_version=0.115.0+1.21.1 +kotlin_test_version=2.1.10 +chicory_version=1.1.0 \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..a4b76b9 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.jar Binary files differdiff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..e2847c8 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..f5feea6 --- /dev/null +++ b/gradlew @@ -0,0 +1,252 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s +' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + 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 +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..9d21a21 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,94 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 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! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/opus/.gitignore b/opus/.gitignore new file mode 100644 index 0000000..edb557c --- /dev/null +++ b/opus/.gitignore @@ -0,0 +1,2 @@ +/opus-* +/libs diff --git a/opus/build_script.sh b/opus/build_script.sh new file mode 100755 index 0000000..cb2e0ef --- /dev/null +++ b/opus/build_script.sh @@ -0,0 +1,29 @@ +#!/bin/sh + +set -e + +cd opus-1.5.2 + +s_EXPORTED_FUNCTIONS="-s EXPORTED_FUNCTIONS=$(printf '_%s,' \ +opus_decoder_create \ +opus_strerror \ +opus_decode_float \ +malloc +)_free" + +#shared_flags="-O1 -fno-unroll-loops -fno-inline -flto -mnontrapping-fptoint" +shared_flags="-Oz -flto -fno-inline -mnontrapping-fptoint" +compile_flags="-DNDEBUG -DNO_STDIO $shared_flags" +link_flags="$shared_flags -s MALLOC=emmalloc -s ASSERTIONS=0 -s INITIAL_MEMORY=196608 -s TOTAL_STACK=48736 -s STANDALONE_WASM=1 -s PURE_WASI=1 -s STACK_OVERFLOW_CHECK=0 -s WASM_BIGINT=1 -s ABORTING_MALLOC=0 -s MEMORY_GROWTH_GEOMETRIC_STEP=0 -s ALLOW_MEMORY_GROWTH=1" + +emconfigure ./configure STRIP='emstrip' CFLAGS="$compile_flags" LDFLAGS="$link_flags" --host=wasm32 --disable-dependency-tracking --disable-shared +emmake make + +#emcc --no-entry $s_EXPORTED_FUNCTIONS $link_flags libmp3lame/.libs/libmp3lame.a +#em++ --no-entry $s_EXPORTED_FUNCTIONS $link_flags $compile_flags -o pocketfft.wasm impl.cc + +#mkdir -p ../../src/main/resources/opus/ +#cp a.out.wasm ../../src/main/resources/opus.wasm +#cp LICENSE COPYING ../../src/main/resources/opus/ + +exit 0 diff --git a/opus/get_opus.sh b/opus/get_opus.sh new file mode 100755 index 0000000..a7fd7e0 --- /dev/null +++ b/opus/get_opus.sh @@ -0,0 +1,4 @@ +#!/bin/sh + +wget https://downloads.xiph.org/releases/opus/opus-1.5.2.tar.gz +tar -xf opus-1.5.2.tar.gz diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..75c4d72 --- /dev/null +++ b/settings.gradle @@ -0,0 +1,10 @@ +pluginManagement { + repositories { + maven { + name = 'Fabric' + url = 'https://maven.fabricmc.net/' + } + mavenCentral() + gradlePluginPortal() + } +} \ No newline at end of file diff --git a/src/client/kotlin/space/autistic/radio/client/PirateRadioClient.kt b/src/client/kotlin/space/autistic/radio/client/PirateRadioClient.kt new file mode 100644 index 0000000..54b7640 --- /dev/null +++ b/src/client/kotlin/space/autistic/radio/client/PirateRadioClient.kt @@ -0,0 +1,35 @@ +package space.autistic.radio.client + +import com.mojang.brigadier.CommandDispatcher +import net.fabricmc.api.ClientModInitializer +import net.fabricmc.fabric.api.client.command.v2.ClientCommandManager +import net.fabricmc.fabric.api.client.command.v2.ClientCommandRegistrationCallback +import net.fabricmc.fabric.api.client.command.v2.FabricClientCommandSource +import net.fabricmc.fabric.api.client.rendering.v1.EntityRendererRegistry +import net.minecraft.client.MinecraftClient +import net.minecraft.command.CommandRegistryAccess +import org.slf4j.LoggerFactory +import space.autistic.radio.PirateRadio +import space.autistic.radio.PirateRadio.MOD_ID +import space.autistic.radio.PirateRadioEntityTypes +import space.autistic.radio.client.entity.ElectronicsTraderEntityRenderer +import space.autistic.radio.client.gui.FmReceiverScreen + +object PirateRadioClient : ClientModInitializer { + private val logger = LoggerFactory.getLogger(MOD_ID) + + override fun onInitializeClient() { + EntityRendererRegistry.register(PirateRadioEntityTypes.ELECTRONICS_TRADER, ::ElectronicsTraderEntityRenderer) + PirateRadioEntityModelLayers.initialize() + ClientCommandRegistrationCallback.EVENT.register { dispatcher, _ -> + dispatcher.register( + ClientCommandManager.literal("fm").executes { + it.source.client.send { + it.source.client.setScreen(FmReceiverScreen()) + } + 0 + } + ) + } + } +} \ No newline at end of file diff --git a/src/client/kotlin/space/autistic/radio/client/PirateRadioDataGenerator.kt b/src/client/kotlin/space/autistic/radio/client/PirateRadioDataGenerator.kt new file mode 100644 index 0000000..b5130a1 --- /dev/null +++ b/src/client/kotlin/space/autistic/radio/client/PirateRadioDataGenerator.kt @@ -0,0 +1,62 @@ +package space.autistic.radio.client + +import net.fabricmc.fabric.api.datagen.v1.DataGeneratorEntrypoint +import net.fabricmc.fabric.api.datagen.v1.FabricDataGenerator +import net.fabricmc.fabric.api.datagen.v1.FabricDataOutput +import net.fabricmc.fabric.api.datagen.v1.provider.FabricModelProvider +import net.fabricmc.fabric.api.datagen.v1.provider.FabricRecipeProvider +import net.minecraft.data.client.BlockStateModelGenerator +import net.minecraft.data.client.ItemModelGenerator +import net.minecraft.data.client.Models +import net.minecraft.data.server.recipe.RecipeExporter +import net.minecraft.data.server.recipe.RecipeProvider +import net.minecraft.data.server.recipe.ShapelessRecipeJsonBuilder +import net.minecraft.item.ItemStack +import net.minecraft.recipe.Ingredient +import net.minecraft.recipe.Recipe +import net.minecraft.recipe.ShapelessRecipe +import net.minecraft.recipe.book.CraftingRecipeCategory +import net.minecraft.recipe.book.RecipeCategory +import net.minecraft.registry.RegistryWrapper +import net.minecraft.util.Identifier +import net.minecraft.util.collection.DefaultedList +import space.autistic.radio.PirateRadio.MOD_ID +import space.autistic.radio.PirateRadioItems +import java.util.concurrent.CompletableFuture + +class PirateRadioItemModelGenerator(output: FabricDataOutput) : FabricModelProvider(output) { + override fun generateBlockStateModels(modelGenerator: BlockStateModelGenerator) { + } + + override fun generateItemModels(modelGenderator: ItemModelGenerator) { + modelGenderator.register(PirateRadioItems.SBC, Models.GENERATED) + modelGenderator.register(PirateRadioItems.WIRE, Models.GENERATED) + modelGenderator.register(PirateRadioItems.POWERBANK, Models.GENERATED) + modelGenderator.register(PirateRadioItems.FM_RECEIVER, Models.GENERATED) + modelGenderator.register(PirateRadioItems.STORAGE_CARD, Models.GENERATED) + modelGenderator.register(PirateRadioItems.DISPOSABLE_TRANSMITTER, Models.GENERATED) + } + +} + +class PirateRadioRecipeGenerator( + output: FabricDataOutput?, + registriesFuture: CompletableFuture<RegistryWrapper.WrapperLookup>? +) : FabricRecipeProvider(output, registriesFuture) { + override fun generate(exporter: RecipeExporter) { + ShapelessRecipeJsonBuilder.create(RecipeCategory.MISC, PirateRadioItems.DISPOSABLE_TRANSMITTER) + .input(PirateRadioItems.SBC).input(PirateRadioItems.WIRE).input(PirateRadioItems.POWERBANK) + .input(PirateRadioItems.STORAGE_CARD) + .criterion("has_sbc", RecipeProvider.conditionsFromItem(PirateRadioItems.SBC)).offerTo(exporter) + } + +} + +object PirateRadioDataGenerator : DataGeneratorEntrypoint { + override fun onInitializeDataGenerator(fabricDataGenerator: FabricDataGenerator) { + val pack = fabricDataGenerator.createPack() + + pack.addProvider(::PirateRadioItemModelGenerator) + pack.addProvider(::PirateRadioRecipeGenerator) + } +} \ No newline at end of file diff --git a/src/client/kotlin/space/autistic/radio/client/PirateRadioEntityModelLayers.kt b/src/client/kotlin/space/autistic/radio/client/PirateRadioEntityModelLayers.kt new file mode 100644 index 0000000..765912d --- /dev/null +++ b/src/client/kotlin/space/autistic/radio/client/PirateRadioEntityModelLayers.kt @@ -0,0 +1,18 @@ +package space.autistic.radio.client + +import net.fabricmc.fabric.api.client.rendering.v1.EntityModelLayerRegistry +import net.minecraft.client.model.TexturedModelData +import net.minecraft.client.render.entity.model.EntityModelLayer +import net.minecraft.client.render.entity.model.VillagerResemblingModel +import net.minecraft.util.Identifier +import space.autistic.radio.PirateRadio + +object PirateRadioEntityModelLayers { + val ELECTRONICS_TRADER = EntityModelLayer(Identifier.of(PirateRadio.MOD_ID, "electronics-trader"), "main") + + fun initialize() { + EntityModelLayerRegistry.registerModelLayer(ELECTRONICS_TRADER) { + TexturedModelData.of(VillagerResemblingModel.getModelData(), 64, 64) + } + } +} \ No newline at end of file diff --git a/src/client/kotlin/space/autistic/radio/client/antenna/AntennaModel.kt b/src/client/kotlin/space/autistic/radio/client/antenna/AntennaModel.kt new file mode 100644 index 0000000..74a7c96 --- /dev/null +++ b/src/client/kotlin/space/autistic/radio/client/antenna/AntennaModel.kt @@ -0,0 +1,19 @@ +package space.autistic.radio.client.antenna + +import org.joml.Vector3d + +interface AntennaModel { + /** + * Returns the linear power level/gain to apply for a receiver at the given position. The receiver is assumed to be + * vertically oriented. + * + * Note: 1.0f = 0dB, 0.5f = -3dB (approx.), 0.1f = -10dB. + */ + fun apply(position: Vector3d): Float + + /** + * Returns whether to process block/material attenuation. Useful for "global" antennas (i.e. those that return a + * constant for all positions given to [apply]). + */ + fun shouldAttenuate(): Boolean +} \ No newline at end of file diff --git a/src/client/kotlin/space/autistic/radio/client/antenna/AntennaModelFactory.kt b/src/client/kotlin/space/autistic/radio/client/antenna/AntennaModelFactory.kt new file mode 100644 index 0000000..33a7087 --- /dev/null +++ b/src/client/kotlin/space/autistic/radio/client/antenna/AntennaModelFactory.kt @@ -0,0 +1,7 @@ +package space.autistic.radio.client.antenna + +import org.joml.Quaterniond + +interface AntennaModelFactory { + fun create(orientation: Quaterniond): AntennaModel +} \ No newline at end of file diff --git a/src/client/kotlin/space/autistic/radio/client/antenna/NullModel.kt b/src/client/kotlin/space/autistic/radio/client/antenna/NullModel.kt new file mode 100644 index 0000000..3c188b6 --- /dev/null +++ b/src/client/kotlin/space/autistic/radio/client/antenna/NullModel.kt @@ -0,0 +1,13 @@ +package space.autistic.radio.client.antenna + +import org.joml.Vector3d + +class NullModel : AntennaModel { + override fun apply(position: Vector3d): Float { + return 0f + } + + override fun shouldAttenuate(): Boolean { + return false + } +} diff --git a/src/client/kotlin/space/autistic/radio/client/antenna/WasmAntennaFactory.kt b/src/client/kotlin/space/autistic/radio/client/antenna/WasmAntennaFactory.kt new file mode 100644 index 0000000..7181e95 --- /dev/null +++ b/src/client/kotlin/space/autistic/radio/client/antenna/WasmAntennaFactory.kt @@ -0,0 +1,97 @@ +package space.autistic.radio.client.antenna + +import com.dylibso.chicory.experimental.aot.AotMachineFactory +import com.dylibso.chicory.runtime.ExportFunction +import com.dylibso.chicory.runtime.ImportValues +import com.dylibso.chicory.runtime.Instance +import com.dylibso.chicory.wasm.ChicoryException +import com.dylibso.chicory.wasm.InvalidException +import com.dylibso.chicory.wasm.Parser +import com.dylibso.chicory.wasm.types.MemoryLimits +import com.dylibso.chicory.wasm.types.Value +import com.dylibso.chicory.wasm.types.ValueType +import org.joml.Quaterniond +import org.joml.Vector3d +import space.autistic.radio.PirateRadio +import java.util.logging.Level +import java.util.logging.Logger + +class WasmAntennaFactory(moduleBytes: ByteArray) : AntennaModelFactory { + var failing = false + private val instanceBuilder = run { + try { + val module = Parser.parse(moduleBytes) + Instance.builder(module).withMachineFactory(AotMachineFactory(module)).withImportValues(defaultImports) + // capped at 1MB per antenna + .withMemoryLimits(MemoryLimits(0, 16)) + } catch (e: ChicoryException) { + logger.log(Level.SEVERE, "Error while trying to parse antenna model.", e) + failing = true + null + } + } + + override fun create(orientation: Quaterniond): AntennaModel { + if (failing) { + return NullModel() + } + try { + val instance = instanceBuilder!!.build() + // see basic module abi convention: https://github.com/WebAssembly/tool-conventions/blob/4487bbc2f5a0ad6b5ca76e233bdfa5ed4513dd8c/BasicModuleABI.md + var initialize: ExportFunction? = null + try { + initialize = instance.export("_initialize") + } catch (_: InvalidException) { + // export may not exist, it's fine + } + initialize?.apply() + // initialize antenna orientation + instance.export("set-orientation").apply( + orientation.x.toRawBits(), + orientation.y.toRawBits(), + orientation.z.toRawBits(), + orientation.w.toRawBits() + ) + if (instance.exports().global("should-attenuate").type != ValueType.I32) { + logger.log( + Level.SEVERE, "Error while trying to initialize antenna model: missing 'should-attenuate'" + ) + failing = true + return NullModel() + } + val shouldAttenuate = instance.exports().global("should-attenuate").value != 0L + val apply = instance.export("apply") + return object : AntennaModel { + override fun apply(position: Vector3d): Float { + if (failing) { + return 0f + } + try { + return Value.longToFloat( + apply.apply( + position.x.toRawBits(), position.y.toRawBits(), position.z.toRawBits() + )[0] + ) + } catch (e: ChicoryException) { + logger.log(Level.SEVERE, "Error while trying to evaluate antenna model.", e) + failing = true + return 0f + } + } + + override fun shouldAttenuate(): Boolean { + return shouldAttenuate + } + } + } catch (e: ChicoryException) { + logger.log(Level.SEVERE, "Error while trying to initialize antenna model.", e) + failing = true + return NullModel() + } + } + + companion object { + private val defaultImports = ImportValues.builder().build() + private val logger = Logger.getLogger(PirateRadio.MOD_ID) + } +} \ No newline at end of file diff --git a/src/client/kotlin/space/autistic/radio/client/entity/ElectronicsTraderEntityRenderer.kt b/src/client/kotlin/space/autistic/radio/client/entity/ElectronicsTraderEntityRenderer.kt new file mode 100644 index 0000000..91c29db --- /dev/null +++ b/src/client/kotlin/space/autistic/radio/client/entity/ElectronicsTraderEntityRenderer.kt @@ -0,0 +1,23 @@ +package space.autistic.radio.client.entity + +import net.minecraft.client.render.entity.EntityRendererFactory +import net.minecraft.client.render.entity.MobEntityRenderer +import net.minecraft.client.render.entity.model.VillagerResemblingModel +import net.minecraft.util.Identifier +import space.autistic.radio.PirateRadio +import space.autistic.radio.client.PirateRadioEntityModelLayers +import space.autistic.radio.entity.ElectronicsTraderEntity + +class ElectronicsTraderEntityRenderer(context: EntityRendererFactory.Context) : + MobEntityRenderer<ElectronicsTraderEntity, VillagerResemblingModel<ElectronicsTraderEntity>>( + context, + VillagerResemblingModel(context.getPart(PirateRadioEntityModelLayers.ELECTRONICS_TRADER)), + 0.5f + ) { + + companion object { + val TEXTURE = Identifier.of(PirateRadio.MOD_ID, "electronics-trader") + } + + override fun getTexture(entity: ElectronicsTraderEntity?): Identifier = TEXTURE +} \ No newline at end of file diff --git a/src/client/kotlin/space/autistic/radio/client/gui/FmReceiverScreen.kt b/src/client/kotlin/space/autistic/radio/client/gui/FmReceiverScreen.kt new file mode 100644 index 0000000..4bd4db2 --- /dev/null +++ b/src/client/kotlin/space/autistic/radio/client/gui/FmReceiverScreen.kt @@ -0,0 +1,11 @@ +package space.autistic.radio.client.gui + +import net.minecraft.client.gui.screen.Screen +import net.minecraft.text.Text + +class FmReceiverScreen : Screen(Text.translatable("pirate-radio.fm-receiver")) { + + override fun init() { + // TODO + } +} \ No newline at end of file diff --git a/src/main/generated/.cache/4145a4ade350d062a154f42d7ad0d98fb52bf04b b/src/main/generated/.cache/4145a4ade350d062a154f42d7ad0d98fb52bf04b new file mode 100644 index 0000000..072c021 --- /dev/null +++ b/src/main/generated/.cache/4145a4ade350d062a154f42d7ad0d98fb52bf04b @@ -0,0 +1,3 @@ +// 1.21.1 2025-02-09T00:02:42.294183715 Pirate Radio/Recipes +84f8cd2b2d9d1afcf2a5cf000905c264a6d8267c data/pirate-radio/recipe/disposable-transmitter.json +86e73a1d034dc407ce65e0e61af19b1db43e1939 data/pirate-radio/advancement/recipes/misc/disposable-transmitter.json diff --git a/src/main/generated/.cache/bd1ee27e4c10ec669c0e0894b64dd83a58902c72 b/src/main/generated/.cache/bd1ee27e4c10ec669c0e0894b64dd83a58902c72 new file mode 100644 index 0000000..cf1f8c7 --- /dev/null +++ b/src/main/generated/.cache/bd1ee27e4c10ec669c0e0894b64dd83a58902c72 @@ -0,0 +1,7 @@ +// 1.21.1 2025-02-09T00:02:42.294917543 Pirate Radio/Model Definitions +3507512497435bf1047ebd71ae1f4881ceb67f44 assets/pirate-radio/models/item/fm-receiver.json +ab60b602066c94b5746065e1b139a383a6c26429 assets/pirate-radio/models/item/powerbank.json +fb8af1b0939020c3a89a7736e47d9f688b38a2c9 assets/pirate-radio/models/item/storage-card.json +dbc04d664dacd99a76580bcff2c5b944abb0730e assets/pirate-radio/models/item/sbc.json +4ec0ecb715a1eec2f90f47221614e09a4c5b8f65 assets/pirate-radio/models/item/disposable-transmitter.json +2d14f0908eb7b92790cb29b141e4150c2d1f4a16 assets/pirate-radio/models/item/wire.json diff --git a/src/main/generated/assets/pirate-radio/models/item/disposable-transmitter.json b/src/main/generated/assets/pirate-radio/models/item/disposable-transmitter.json new file mode 100644 index 0000000..5eda62b --- /dev/null +++ b/src/main/generated/assets/pirate-radio/models/item/disposable-transmitter.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:item/generated", + "textures": { + "layer0": "pirate-radio:item/disposable-transmitter" + } +} \ No newline at end of file diff --git a/src/main/generated/assets/pirate-radio/models/item/fm-receiver.json b/src/main/generated/assets/pirate-radio/models/item/fm-receiver.json new file mode 100644 index 0000000..71813c4 --- /dev/null +++ b/src/main/generated/assets/pirate-radio/models/item/fm-receiver.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:item/generated", + "textures": { + "layer0": "pirate-radio:item/fm-receiver" + } +} \ No newline at end of file diff --git a/src/main/generated/assets/pirate-radio/models/item/powerbank.json b/src/main/generated/assets/pirate-radio/models/item/powerbank.json new file mode 100644 index 0000000..90149f7 --- /dev/null +++ b/src/main/generated/assets/pirate-radio/models/item/powerbank.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:item/generated", + "textures": { + "layer0": "pirate-radio:item/powerbank" + } +} \ No newline at end of file diff --git a/src/main/generated/assets/pirate-radio/models/item/sbc.json b/src/main/generated/assets/pirate-radio/models/item/sbc.json new file mode 100644 index 0000000..caa25b1 --- /dev/null +++ b/src/main/generated/assets/pirate-radio/models/item/sbc.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:item/generated", + "textures": { + "layer0": "pirate-radio:item/sbc" + } +} \ No newline at end of file diff --git a/src/main/generated/assets/pirate-radio/models/item/storage-card.json b/src/main/generated/assets/pirate-radio/models/item/storage-card.json new file mode 100644 index 0000000..6b56c92 --- /dev/null +++ b/src/main/generated/assets/pirate-radio/models/item/storage-card.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:item/generated", + "textures": { + "layer0": "pirate-radio:item/storage-card" + } +} \ No newline at end of file diff --git a/src/main/generated/assets/pirate-radio/models/item/wire.json b/src/main/generated/assets/pirate-radio/models/item/wire.json new file mode 100644 index 0000000..8c26725 --- /dev/null +++ b/src/main/generated/assets/pirate-radio/models/item/wire.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:item/generated", + "textures": { + "layer0": "pirate-radio:item/wire" + } +} \ No newline at end of file diff --git a/src/main/generated/data/pirate-radio/advancement/recipes/misc/disposable-transmitter.json b/src/main/generated/data/pirate-radio/advancement/recipes/misc/disposable-transmitter.json new file mode 100644 index 0000000..fca182d --- /dev/null +++ b/src/main/generated/data/pirate-radio/advancement/recipes/misc/disposable-transmitter.json @@ -0,0 +1,32 @@ +{ + "parent": "minecraft:recipes/root", + "criteria": { + "has_sbc": { + "conditions": { + "items": [ + { + "items": "pirate-radio:sbc" + } + ] + }, + "trigger": "minecraft:inventory_changed" + }, + "has_the_recipe": { + "conditions": { + "recipe": "pirate-radio:disposable-transmitter" + }, + "trigger": "minecraft:recipe_unlocked" + } + }, + "requirements": [ + [ + "has_the_recipe", + "has_sbc" + ] + ], + "rewards": { + "recipes": [ + "pirate-radio:disposable-transmitter" + ] + } +} \ No newline at end of file diff --git a/src/main/generated/data/pirate-radio/recipe/disposable-transmitter.json b/src/main/generated/data/pirate-radio/recipe/disposable-transmitter.json new file mode 100644 index 0000000..2a1d645 --- /dev/null +++ b/src/main/generated/data/pirate-radio/recipe/disposable-transmitter.json @@ -0,0 +1,22 @@ +{ + "type": "minecraft:crafting_shapeless", + "category": "misc", + "ingredients": [ + { + "item": "pirate-radio:sbc" + }, + { + "item": "pirate-radio:wire" + }, + { + "item": "pirate-radio:powerbank" + }, + { + "item": "pirate-radio:storage-card" + } + ], + "result": { + "count": 1, + "id": "pirate-radio:disposable-transmitter" + } +} \ No newline at end of file diff --git a/src/main/kotlin/space/autistic/radio/PirateRadio.kt b/src/main/kotlin/space/autistic/radio/PirateRadio.kt new file mode 100644 index 0000000..54d0b9f --- /dev/null +++ b/src/main/kotlin/space/autistic/radio/PirateRadio.kt @@ -0,0 +1,17 @@ +package space.autistic.radio + +import net.fabricmc.api.ModInitializer +import org.slf4j.LoggerFactory + +object PirateRadio : ModInitializer { + const val MOD_ID = "pirate-radio" + private val logger = LoggerFactory.getLogger(MOD_ID) + + override fun onInitialize() { + logger.info("This project is made with love by a queer trans person.\n" + + "The folks of these identities who have contributed to the project would like to make their identities known:\n" + + "Autgender; Not a person; Anticapitalist; Genderqueer; Trans.") + PirateRadioItems.initialize() + PirateRadioEntityTypes.initialize() + } +} \ No newline at end of file diff --git a/src/main/kotlin/space/autistic/radio/PirateRadioEntityTypes.kt b/src/main/kotlin/space/autistic/radio/PirateRadioEntityTypes.kt new file mode 100644 index 0000000..f147394 --- /dev/null +++ b/src/main/kotlin/space/autistic/radio/PirateRadioEntityTypes.kt @@ -0,0 +1,26 @@ +package space.autistic.radio + +import net.fabricmc.fabric.api.`object`.builder.v1.entity.FabricDefaultAttributeRegistry +import net.minecraft.entity.Entity +import net.minecraft.entity.EntityType +import net.minecraft.entity.SpawnGroup +import net.minecraft.entity.mob.MobEntity +import net.minecraft.registry.Registries +import net.minecraft.registry.Registry +import net.minecraft.registry.RegistryKey +import net.minecraft.registry.RegistryKeys +import net.minecraft.util.Identifier +import space.autistic.radio.entity.ElectronicsTraderEntity + +object PirateRadioEntityTypes { + val ELECTRONICS_TRADER_KEY = RegistryKey.of(RegistryKeys.ENTITY_TYPE, Identifier.of(PirateRadio.MOD_ID, "electronics-trader")) + val ELECTRONICS_TRADER = register(EntityType.Builder.create(::ElectronicsTraderEntity, SpawnGroup.MISC).dimensions(0.6F, 1.95F).eyeHeight(1.62F).maxTrackingRange(10), ELECTRONICS_TRADER_KEY) + + fun <T : Entity> register(entityTypeBuilder: EntityType.Builder<T>, registryKey: RegistryKey<EntityType<*>>): EntityType<T> { + return Registry.register(Registries.ENTITY_TYPE, registryKey.value, entityTypeBuilder.build(registryKey.value.path)) + } + + fun initialize() { + FabricDefaultAttributeRegistry.register(ELECTRONICS_TRADER, MobEntity.createMobAttributes()) + } +} \ No newline at end of file diff --git a/src/main/kotlin/space/autistic/radio/PirateRadioItems.kt b/src/main/kotlin/space/autistic/radio/PirateRadioItems.kt new file mode 100644 index 0000000..490acaf --- /dev/null +++ b/src/main/kotlin/space/autistic/radio/PirateRadioItems.kt @@ -0,0 +1,38 @@ +package space.autistic.radio + +import net.fabricmc.fabric.api.itemgroup.v1.ItemGroupEvents +import net.minecraft.item.Item +import net.minecraft.item.ItemGroups +import net.minecraft.registry.Registries +import net.minecraft.registry.Registry +import net.minecraft.registry.RegistryKey +import net.minecraft.registry.RegistryKeys +import net.minecraft.util.Identifier + +object PirateRadioItems { + val SBC_KEY = RegistryKey.of(RegistryKeys.ITEM, Identifier.of(PirateRadio.MOD_ID, "sbc")) + val SBC = register(Item(Item.Settings()), SBC_KEY) + val WIRE_KEY = RegistryKey.of(RegistryKeys.ITEM, Identifier.of(PirateRadio.MOD_ID, "wire")) + val WIRE = register(Item(Item.Settings()), WIRE_KEY) + val POWERBANK_KEY = RegistryKey.of(RegistryKeys.ITEM, Identifier.of(PirateRadio.MOD_ID, "powerbank")) + val POWERBANK = register(Item(Item.Settings()), POWERBANK_KEY) + val STORAGE_CARD_KEY = RegistryKey.of(RegistryKeys.ITEM, Identifier.of(PirateRadio.MOD_ID, "storage-card")) + val STORAGE_CARD = register(Item(Item.Settings()), STORAGE_CARD_KEY) + val DISPOSABLE_TRANSMITTER_KEY = RegistryKey.of(RegistryKeys.ITEM, Identifier.of(PirateRadio.MOD_ID, "disposable-transmitter")) + val DISPOSABLE_TRANSMITTER = register(Item(Item.Settings()), DISPOSABLE_TRANSMITTER_KEY) + val FM_RECEIVER_KEY = RegistryKey.of(RegistryKeys.ITEM, Identifier.of(PirateRadio.MOD_ID, "fm-receiver")) + val FM_RECEIVER = register(Item(Item.Settings()), FM_RECEIVER_KEY) + + fun register(item: Item, registryKey: RegistryKey<Item>): Item { + return Registry.register(Registries.ITEM, registryKey.value, item) + } + + fun initialize() { + ItemGroupEvents.modifyEntriesEvent(ItemGroups.INGREDIENTS).register { + it.add(SBC) + it.add(WIRE) + it.add(POWERBANK) + it.add(STORAGE_CARD) + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/space/autistic/radio/cli/OfflineSimulator.kt b/src/main/kotlin/space/autistic/radio/cli/OfflineSimulator.kt new file mode 100644 index 0000000..517957b --- /dev/null +++ b/src/main/kotlin/space/autistic/radio/cli/OfflineSimulator.kt @@ -0,0 +1,208 @@ +package space.autistic.radio.cli + +import org.joml.Vector2f +import space.autistic.radio.complex.cmul +import space.autistic.radio.fmsim.FmFullConstants +import space.autistic.radio.fmsim.FmFullModulator +import java.io.FileInputStream +import java.io.FileOutputStream +import java.io.InputStream +import java.net.URI +import java.nio.ByteBuffer +import java.nio.ByteOrder +import java.nio.FloatBuffer +import kotlin.io.path.inputStream +import kotlin.io.path.toPath +import kotlin.math.min +import kotlin.system.exitProcess + +fun printUsage() { + println("Usage: OfflineSimulator -o OUTFILE.raw {[-p POWER] [-l|-h] file:///FILE.raw}") + println(" file:///FILE.raw (or ./FILE.raw - the ./ is required)") + println(" The raw input file. 2x48kHz 32-bit float") + println(" -o OUTFILE.raw") + println(" The raw RF stream to output, 2x200kHz 32-bit float") + println(" -p POWER") + println(" The signal amplitude (power level), e.g. 1.0") + println(" -l") + println(" Simulate a partial overlap on the lower half of the tuned-into frequency.") + println(" -h") + println(" Simulate a partial overlap on the upper half of the tuned-into frequency.") +} + +class SimFile(val power: Float, val band: Int, val filename: String) { + var closed: Boolean = false + val buffer: FloatBuffer = FloatBuffer.allocate(8192) + val modulator = FmFullModulator() + var stream: InputStream? = null +} + +fun main(args: Array<String>) { + if (args.isEmpty()) { + printUsage() + exitProcess(1) + } + var hasOutput = false + var inArg = "" + var output = "" + var power = 1.0f + var band = 2 + val files: ArrayList<SimFile> = ArrayList() + for (arg in args) { + if (!hasOutput) { + if (arg == "-o") { + hasOutput = true + inArg = "-o" + } else { + printUsage() + exitProcess(1) + } + } else { + when (inArg) { + "-o" -> { + output = arg + inArg = "" + } + + "-p" -> { + power = arg.toFloatOrNull() ?: run { + println("Error processing -p argument: not a valid float") + printUsage() + exitProcess(1) + } + inArg = "" + } + + "" -> { + if (!arg.startsWith("-")) { + files.add(SimFile(power, band, arg)) + inArg = "" + band = 2 + power = 1.0f + } else { + when (arg) { + "-p" -> inArg = "-p" + "-l" -> band = 1 + "-h" -> band = 3 + else -> { + println("Unknown option") + printUsage() + exitProcess(1) + } + } + } + } + + else -> throw NotImplementedError(inArg) + } + } + } + + if (files.isEmpty()) { + printUsage() + exitProcess(1) + } + + println(ProcessHandle.current().pid()) + + FileOutputStream(output).buffered().use { outputStream -> + for (inputFile in files) { + if (inputFile.filename != "file:///dev/zero") { + if (inputFile.filename.startsWith("./")) { + inputFile.stream = FileInputStream(inputFile.filename) + } else { + inputFile.stream = URI(inputFile.filename).toPath().inputStream() + } + } + } + + val buffer = ByteBuffer.allocate(2 * 4 * FmFullConstants.FFT_DATA_BLOCK_SIZE_LPF_48K_15K_3K1) + val plus100k = FloatBuffer.wrap(FmFullConstants.CBUFFER_100K_300K) + val minus100k = FloatBuffer.wrap(FmFullConstants.CBUFFER_100K_300K) + while (true) { + // initialized to maximum buffer size, trimmed down later + var minBuffer = 8192 + for (inputFile in files) { + val stream = inputFile.stream + if (stream == null) { + if (inputFile.buffer.remaining() > 2 * FmFullConstants.IFFT_DATA_BLOCK_SIZE_48K_300K) { + inputFile.modulator.flush(inputFile.power) { + inputFile.buffer.put(it) + } + } + } else { + val bytes = stream.read(buffer.array()) + if (bytes <= 0) { + stream.close() + inputFile.stream = null + inputFile.closed = true + inputFile.modulator.flush(inputFile.power) { + inputFile.buffer.put(it) + } + } else { + val floats = buffer.slice(0, bytes).order(ByteOrder.LITTLE_ENDIAN).asFloatBuffer() + var shouldFlush = true + inputFile.modulator.process(floats, inputFile.power) { + inputFile.buffer.put(it) + shouldFlush = false + } + if (shouldFlush) { + inputFile.modulator.flush(inputFile.power) { + inputFile.buffer.put(it) + } + } + } + } + minBuffer = min(minBuffer, inputFile.buffer.position()) + } + + val outputBuffer = ByteBuffer.allocate(minBuffer * 4) + val floatView = outputBuffer.order(ByteOrder.LITTLE_ENDIAN).asFloatBuffer() + val floatBufferLo = FloatBuffer.allocate(minBuffer) + val floatBufferHi = FloatBuffer.allocate(minBuffer) + for (inputFile in files) { + inputFile.buffer.flip() + val floatBuffer = when (inputFile.band) { + 1 -> floatBufferLo + 2 -> floatView + 3 -> floatBufferHi + else -> throw IllegalStateException() + } + for (i in 0 until floatBuffer.capacity()) { + floatBuffer.put(i, floatBuffer.get(i) + inputFile.buffer.get()) + } + inputFile.buffer.compact() + } + val z = Vector2f() + val w = Vector2f() + for (i in 0 until floatBufferHi.capacity() step 2) { + z.x = floatBufferHi.get(i) + z.y = floatBufferHi.get(i + 1) + if (!plus100k.hasRemaining()) { + plus100k.clear() + } + w.x = plus100k.get() + w.y = plus100k.get() + z.cmul(w) + floatView.put(i, floatView.get(i) + z.x) + floatView.put(i, floatView.get(i) + z.y) + } + for (i in 0 until floatBufferLo.capacity() step 2) { + z.x = floatBufferLo.get(i) + z.y = floatBufferLo.get(i + 1) + if (!minus100k.hasRemaining()) { + minus100k.clear() + } + w.x = minus100k.get() + w.y = -minus100k.get() + z.cmul(w) + floatView.put(i, floatView.get(i) + z.x) + floatView.put(i, floatView.get(i) + z.y) + } + outputStream.write(outputBuffer.array()) + if (files.all { it.closed }) { + break + } + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/space/autistic/radio/complex/Complex.kt b/src/main/kotlin/space/autistic/radio/complex/Complex.kt new file mode 100644 index 0000000..918dac2 --- /dev/null +++ b/src/main/kotlin/space/autistic/radio/complex/Complex.kt @@ -0,0 +1,32 @@ +package space.autistic.radio.complex + +import org.joml.Vector2f +import org.joml.Vector2fc + +fun Vector2f.cmul(v: Vector2fc): Vector2f { + return this.cmul(v, this) +} + +fun Vector2f.cmul(v: Vector2fc, dest: Vector2f): Vector2f { + val a = this.x * v.x() + val b = this.y * v.y() + val c = (this.x() + this.y()) * (v.x() + v.y()) + val x = a - b + val y = c - a - b + dest.x = x + dest.y = y + return dest +} + +fun Vector2f.conjugate(): Vector2f { + return this.conjugate(this) +} + +fun Vector2f.conjugate(dest: Vector2f): Vector2f { + dest.x = this.x() + dest.y = -this.y() + return dest +} + +val I + get() = Vector2f(0f, 1f) \ No newline at end of file diff --git a/src/main/kotlin/space/autistic/radio/dsp/Biquad1stOrder.kt b/src/main/kotlin/space/autistic/radio/dsp/Biquad1stOrder.kt new file mode 100644 index 0000000..8f86218 --- /dev/null +++ b/src/main/kotlin/space/autistic/radio/dsp/Biquad1stOrder.kt @@ -0,0 +1,11 @@ +package space.autistic.radio.dsp + +class Biquad1stOrder(private val b0: Float, private val b1: Float, private val a1: Float) { + private var delaySlot = 0f + + fun process(samp: Float): Float { + val out = samp * b0 + delaySlot + delaySlot = samp * b1 - out * a1 + return out + } +} \ No newline at end of file diff --git a/src/main/kotlin/space/autistic/radio/entity/ElectronicsTraderEntity.kt b/src/main/kotlin/space/autistic/radio/entity/ElectronicsTraderEntity.kt new file mode 100644 index 0000000..3aa53b1 --- /dev/null +++ b/src/main/kotlin/space/autistic/radio/entity/ElectronicsTraderEntity.kt @@ -0,0 +1,36 @@ +package space.autistic.radio.entity + +import net.minecraft.entity.EntityType +import net.minecraft.entity.ai.goal.HoldInHandsGoal +import net.minecraft.entity.passive.WanderingTraderEntity +import net.minecraft.item.ItemStack +import net.minecraft.item.Items +import net.minecraft.village.TradeOffer +import net.minecraft.village.TradedItem +import net.minecraft.world.World +import space.autistic.radio.PirateRadioItems + +class ElectronicsTraderEntity(entityType: EntityType<out ElectronicsTraderEntity>, world: World) : + WanderingTraderEntity(entityType, world) { + + override fun initGoals() { + super.initGoals() + goalSelector.goals.removeIf { it.goal is HoldInHandsGoal<*> } + } + + override fun fillRecipes() { + val offers = this.getOffers() + offers.add(TradeOffer(TradedItem(Items.EMERALD, 5), ItemStack(PirateRadioItems.POWERBANK), 3, 0, 0f)) + offers.add(TradeOffer(TradedItem(Items.EMERALD, 10), ItemStack(PirateRadioItems.FM_RECEIVER), 3, 0, 0f)) + offers.add(TradeOffer(TradedItem(Items.EMERALD, 15), ItemStack(PirateRadioItems.SBC), 3, 0, 0f)) + offers.add(TradeOffer(TradedItem(Items.EMERALD, 5), ItemStack(PirateRadioItems.STORAGE_CARD), 3, 0, 0f)) + offers.add(TradeOffer(TradedItem(Items.EMERALD, 1), ItemStack(PirateRadioItems.WIRE), 3, 0, 0f)) + } + + override fun tickMovement() { + if (!this.world.isClient) { + super.setDespawnDelay(1000) + } + super.tickMovement() + } +} \ No newline at end of file diff --git a/src/main/kotlin/space/autistic/radio/fmsim/FmFullConstants.kt b/src/main/kotlin/space/autistic/radio/fmsim/FmFullConstants.kt new file mode 100644 index 0000000..5874166 --- /dev/null +++ b/src/main/kotlin/space/autistic/radio/fmsim/FmFullConstants.kt @@ -0,0 +1,109 @@ +package space.autistic.radio.fmsim + +import kotlin.math.PI +import kotlin.math.cos +import kotlin.math.sin + +object FmFullConstants { + // tau = 75us, fh = 20396.25Hz + const val FM_PREEMPAHSIS_B0_48K = 6.7639647f + const val FM_PREEMPHASIS_B1_48K = -4.975628f + + /* const val FM_PREEMPHASIS_A0_48K = 1f */ + const val FM_PREEMPHASIS_A1_48K = 0.78833646f + + const val FM_DEEMPAHSIS_B0_48K = 1f / FM_PREEMPAHSIS_B0_48K + const val FM_DEEMPHASIS_B1_48K = FM_PREEMPHASIS_A1_48K / FM_PREEMPAHSIS_B0_48K + + /* const val FM_DEEMPHASIS_A0_48K = 1f */ + const val FM_DEEMPHASIS_A1_48K = FM_PREEMPHASIS_B1_48K / FM_PREEMPAHSIS_B0_48K + + val FIR_LPF_48K_15K_3K1 = floatArrayOf( + -0.0010006913216784596f, + 0.001505308784544468f, + -2.625857350794219e-18f, + -0.002777613466605544f, + 0.0030173989944159985f, + 0.002290070755407214f, + -0.008225799538195133f, + 0.004239063244313002f, + 0.010359899140894413f, + -0.017650796100497246f, + 1.510757873119297e-17f, + 0.029305754229426384f, + -0.02889496460556984f, + -0.020366130396723747f, + 0.07103750854730606f, + -0.03811456635594368f, + -0.10945471376180649f, + 0.29212409257888794f, + 0.6252123713493347f, + 0.29212409257888794f, + -0.10945471376180649f, + -0.03811456635594368f, + 0.07103750854730606f, + -0.020366130396723747f, + -0.02889496460556984f, + 0.029305754229426384f, + 1.510757873119297e-17f, + -0.017650796100497246f, + 0.010359899140894413f, + 0.004239063244313002f, + -0.008225799538195133f, + 0.002290070755407214f, + 0.0030173989944159985f, + -0.002777613466605544f, + -2.625857350794219e-18f, + 0.001505308784544468f, + -0.0010006913216784596f, + ) + + // chosen such that we can easily do 38kHz mixing in frequency (750*38k/300k = shift of 95 bins, where 750 comes + // from the 4/25 ratio 48k/300k i.e. 120*25/4) + // (the theoretical optimum, as per above, would be around 180) + // (we could have fudged the carrier frequency a bit but we chose not to) + // NOTE: latency = (data block size / 48000) seconds (84 -> 1.75 ms) + const val FFT_SIZE_LPF_48K_15K_3K1 = 120 + const val FFT_OVERLAP_LPF_48K_15K_3K1 = 36 + const val FFT_DATA_BLOCK_SIZE_LPF_48K_15K_3K1 = FFT_SIZE_LPF_48K_15K_3K1 - FFT_OVERLAP_LPF_48K_15K_3K1 + + init { + assert(FFT_OVERLAP_LPF_48K_15K_3K1 >= FIR_LPF_48K_15K_3K1.size - 1) + } + + const val DECIMATION_48K_300K = 4 + const val INTERPOLATION_48K_300K = 25 + + const val IFFT_SIZE_48K_300K = FFT_SIZE_LPF_48K_15K_3K1 * INTERPOLATION_48K_300K / DECIMATION_48K_300K + const val IFFT_OVERLAP_48K_300K = FFT_OVERLAP_LPF_48K_15K_3K1 * INTERPOLATION_48K_300K / DECIMATION_48K_300K + const val IFFT_DATA_BLOCK_SIZE_48K_300K = IFFT_SIZE_48K_300K - IFFT_OVERLAP_48K_300K + + // how many bins to shift for 38kHz mixing + // assuming FFT_SIZE_LPF_48K_15K_3K1 *bins* (complex) + // 19 / 150 is the ratio between 38k/300k + const val FREQUENCY_MIXING_BINS_38K = + FFT_SIZE_LPF_48K_15K_3K1 * INTERPOLATION_48K_300K / DECIMATION_48K_300K * 19 / 150 + + // a single cycle of a 19kHz signal takes (1/19k)/(1/300k) or 300k/19k samples. + // since that number isn't exact, buffer an entire 19 cycles. + const val BUFFER_SIZE_19K_300K = 300 + + val BUFFER_19K_300K = FloatArray(BUFFER_SIZE_19K_300K) { + 0.1f * sin(2 * PI * 19000.0 * it.toDouble() / 300000.0).toFloat() + } + + // we want a carrier deviation of +-75kHz, at a sampling rate of 300kHz + const val CORRECTION_FACTOR = (75000.0 / (300000.0 / (2.0 * PI))).toFloat() + + // these are used for "low/high" mixing + const val CBUFFER_SIZE_100K_300K = 3 + + val CBUFFER_100K_300K = FloatArray(2 * CBUFFER_SIZE_100K_300K) { + val index = it / 2 + if (it and 1 == 0) { + 1f * sin(2 * PI * 100000.0 * index.toDouble() / 300000.0).toFloat() + } else { + 1f * cos(2 * PI * 100000.0 * index.toDouble() / 300000.0).toFloat() + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/space/autistic/radio/fmsim/FmFullMixer.kt b/src/main/kotlin/space/autistic/radio/fmsim/FmFullMixer.kt new file mode 100644 index 0000000..654d50f --- /dev/null +++ b/src/main/kotlin/space/autistic/radio/fmsim/FmFullMixer.kt @@ -0,0 +1,4 @@ +package space.autistic.radio.fmsim + +class FmFullMixer { +} \ No newline at end of file diff --git a/src/main/kotlin/space/autistic/radio/fmsim/FmFullModulator.kt b/src/main/kotlin/space/autistic/radio/fmsim/FmFullModulator.kt new file mode 100644 index 0000000..96ad186 --- /dev/null +++ b/src/main/kotlin/space/autistic/radio/fmsim/FmFullModulator.kt @@ -0,0 +1,176 @@ +package space.autistic.radio.fmsim + +import org.joml.Vector2f +import space.autistic.radio.complex.cmul +import space.autistic.radio.complex.conjugate +import space.autistic.radio.dsp.Biquad1stOrder +import java.nio.FloatBuffer +import java.util.function.Consumer +import org.jtransforms.fft.FloatFFT_1D +import kotlin.math.max +import kotlin.math.min + +class FmFullModulator { + private val leftPlusRight = FloatBuffer.allocate(FmFullConstants.FFT_SIZE_LPF_48K_15K_3K1) + private val leftMinusRight = FloatBuffer.allocate(FmFullConstants.FFT_SIZE_LPF_48K_15K_3K1) + private val biquadLeft = Biquad1stOrder( + FmFullConstants.FM_PREEMPAHSIS_B0_48K, + FmFullConstants.FM_PREEMPHASIS_B1_48K, + FmFullConstants.FM_PREEMPHASIS_A1_48K + ) + private val biquadRight = Biquad1stOrder( + FmFullConstants.FM_PREEMPAHSIS_B0_48K, + FmFullConstants.FM_PREEMPHASIS_B1_48K, + FmFullConstants.FM_PREEMPHASIS_A1_48K + ) + private val fft48kBuffer = FloatBuffer.allocate(FmFullConstants.FFT_SIZE_LPF_48K_15K_3K1) + private val fir48kLpf = FloatBuffer.allocate(FmFullConstants.FFT_SIZE_LPF_48K_15K_3K1) + private val mixingBuffer = FloatBuffer.allocate(FmFullConstants.IFFT_SIZE_48K_300K) + private val outputBuffer = FloatBuffer.allocate(2 * FmFullConstants.IFFT_DATA_BLOCK_SIZE_48K_300K) + private val stereoPilot = FloatBuffer.wrap(FmFullConstants.BUFFER_19K_300K) + + private var cycle = -1f + private var lastSum = 0f + + init { + fir48kLpf.put(0, FmFullConstants.FIR_LPF_48K_15K_3K1) + Companion.fft48k.realForward(fir48kLpf.array()) + + // pre-pad the buffers + while (leftPlusRight.position() < FmFullConstants.FFT_OVERLAP_LPF_48K_15K_3K1) { + leftPlusRight.put(0f) + leftMinusRight.put(0f) + } + } + + /** + * Takes in samples at 48kHz, interleaved stereo and processes them for output. + * + * Calls consumer with processed samples in I/Q format. + */ + fun process(input: FloatBuffer, power: Float, consumer: Consumer<FloatBuffer>) { + while (input.remaining() >= 2) { + while (input.remaining() >= 2 && leftPlusRight.hasRemaining()) { + // FIXME AGC (currently clamping/clipping) + val left = min(max(biquadLeft.process(input.get()), -1f), 1f) + val right = min(max(biquadRight.process(input.get()), -1f), 1f) + leftPlusRight.put(left + right) + leftMinusRight.put(left - right) + } + if (!leftPlusRight.hasRemaining()) { + // zero the mixing buffer + for (i in 0 until mixingBuffer.capacity()) { + mixingBuffer.put(i, 0f) + } + fft48kBuffer.put(0, leftPlusRight, 0, FmFullConstants.FFT_SIZE_LPF_48K_15K_3K1) + Companion.fft48k.realForward(fft48kBuffer.array()) + fft48kBuffer.array().forEachIndexed { index, fl -> + fft48kBuffer.put( + index, + 0.4f / FmFullConstants.FFT_SIZE_LPF_48K_15K_3K1 * fl + ) + } + val z = Vector2f() + val w = Vector2f() + for (i in 2 until FmFullConstants.FFT_SIZE_LPF_48K_15K_3K1 step 2) { + z.x = fft48kBuffer.get(i) + z.y = fft48kBuffer.get(i + 1) + w.x = fir48kLpf.get(i) + w.y = fir48kLpf.get(i + 1) + z.cmul(w) + fft48kBuffer.put(i, z.x) + fft48kBuffer.put(i + 1, z.y) + } + fft48kBuffer.put(0, fft48kBuffer.get(0) * fir48kLpf.get(0)) + fft48kBuffer.put(1, fft48kBuffer.get(1) * fir48kLpf.get(1)) + // copy only around 19kHz of bandwidth + mixingBuffer.put(0, fft48kBuffer, 0, FmFullConstants.FREQUENCY_MIXING_BINS_38K or 1) + // zero out nyquist frequency bucket + mixingBuffer.put(1, 0f) + fft48kBuffer.put(0, leftMinusRight, 0, FmFullConstants.FFT_SIZE_LPF_48K_15K_3K1) + Companion.fft48k.realForward(fft48kBuffer.array()) + fft48kBuffer.array().forEachIndexed { index, fl -> + fft48kBuffer.put( + index, + 0.2f / FmFullConstants.FFT_SIZE_LPF_48K_15K_3K1 * fl + ) + } + for (i in 2 until FmFullConstants.FFT_SIZE_LPF_48K_15K_3K1 step 2) { + z.x = fft48kBuffer.get(i) + z.y = fft48kBuffer.get(i + 1) + w.x = fir48kLpf.get(i) + w.y = fir48kLpf.get(i + 1) + z.cmul(w) + fft48kBuffer.put(i, z.x) + fft48kBuffer.put(i + 1, z.y) + } + fft48kBuffer.put(0, fft48kBuffer.get(0) * fir48kLpf.get(0)) + // (unnecessary) + //fft48kBuffer.put(1, fft48kBuffer.get(1) * fir48kLpf.get(1)) + mixingBuffer.put( + FmFullConstants.FREQUENCY_MIXING_BINS_38K * 2 + 2, + fft48kBuffer, + 2, + // number of floats to copy + // bins are complex, so this halves the bins (~19kHz bandwidth) + // length should be even (for an exact number of complex bins) + FmFullConstants.FREQUENCY_MIXING_BINS_38K and 1.inv() + ) + // the actual 38k bin is at this offset, account for jt convention (buf[0 until 3] = R0,Rn,R1) + mixingBuffer.put(FmFullConstants.FREQUENCY_MIXING_BINS_38K * 2, fft48kBuffer.get(0)) + val base = FmFullConstants.FREQUENCY_MIXING_BINS_38K * 2 + // phase correction factor (due to dropping 150 bins) + // TODO figure out if phase is correct + cycle = -cycle + // bandwidth we care about is about half of 38k, so just, well, half it + for (i in 2 until FmFullConstants.FREQUENCY_MIXING_BINS_38K step 2) { + z.x = mixingBuffer.get(base + i) + z.y = mixingBuffer.get(base + i + 1) + // we also need the conjugate + z.conjugate(w) + mixingBuffer.put(base + i, z.y * -cycle) + mixingBuffer.put(base + i + 1, z.x * cycle) + mixingBuffer.put(base - i, mixingBuffer.get(base - i - 2) - w.y * cycle) + mixingBuffer.put(base - i + 1, mixingBuffer.get(base - i - 1) + w.x * cycle) + } + // handle 38kHz itself + z.x = mixingBuffer.get(base) + z.y = mixingBuffer.get(base + 1) + mixingBuffer.put(base, z.y * -cycle) + mixingBuffer.put(base + 1, z.x * cycle) + // (don't need to handle nyquist) + // mark data block as processed + leftPlusRight.position(FmFullConstants.FFT_DATA_BLOCK_SIZE_LPF_48K_15K_3K1) + leftMinusRight.position(FmFullConstants.FFT_DATA_BLOCK_SIZE_LPF_48K_15K_3K1) + leftPlusRight.compact() + leftMinusRight.compact() + Companion.fft300k.realInverse(mixingBuffer.array(), false) + outputBuffer.clear() + var sum = lastSum + for (i in FmFullConstants.IFFT_OVERLAP_48K_300K until FmFullConstants.IFFT_SIZE_48K_300K) { + if (!stereoPilot.hasRemaining()) { + stereoPilot.clear() + } + val result = mixingBuffer.get(i) + stereoPilot.get() + sum += result * FmFullConstants.CORRECTION_FACTOR + val sin = org.joml.Math.sin(sum) + outputBuffer.put(sin * power) + outputBuffer.put(org.joml.Math.cos(sum) * power) + } + lastSum = sum % (2 * Math.PI).toFloat() + outputBuffer.clear() + consumer.accept(outputBuffer) + } + } + input.compact() + } + + fun flush(power: Float, consumer: Consumer<FloatBuffer>) { + process(FloatBuffer.allocate(2 * leftPlusRight.remaining()), power, consumer) + } + + companion object { + private val fft48k = FloatFFT_1D(FmFullConstants.FFT_SIZE_LPF_48K_15K_3K1.toLong()) + private val fft300k = FloatFFT_1D(FmFullConstants.IFFT_SIZE_48K_300K.toLong()) + } +} \ No newline at end of file diff --git a/src/main/kotlin/space/autistic/radio/opus/OpusDecoder.kt b/src/main/kotlin/space/autistic/radio/opus/OpusDecoder.kt new file mode 100644 index 0000000..56fce2b --- /dev/null +++ b/src/main/kotlin/space/autistic/radio/opus/OpusDecoder.kt @@ -0,0 +1,77 @@ +package space.autistic.radio.opus + +import com.dylibso.chicory.runtime.ByteBufferMemory +import space.autistic.radio.reflection.getBuffer +import java.nio.ByteOrder + +class OpusDecoder(sampleRate: Int, private val channels: Int) { + private val instance = OpusFactory() + + init { + instance.export("_initialize").apply() + } + + private val errorPtr = instance.export("malloc").apply(4)[0] + + init { + if (errorPtr == 0L) { + throw IllegalStateException() + } + instance.memory().writeI32(errorPtr.toInt(), 0) + } + + private val decoder = + instance.export("opus_decoder_create").apply(sampleRate.toLong(), channels.toLong(), errorPtr)[0] + + init { + val error = instance.memory().readI32(errorPtr.toInt()) + if (error < 0) { + throw IllegalStateException( + instance.memory().readCString(instance.export("opus_strerror").apply(error)[0].toInt()) + ) + } + } + + private val opusDecodeFloat = instance.export("opus_decode_float") + + private val outBuf = instance.export("malloc").apply((4 * MAX_FRAME_SIZE * channels).toLong())[0] + + init { + if (outBuf == 0L) { + throw IllegalStateException() + } + } + + private val cbits = instance.export("malloc").apply(MAX_PACKET_SIZE.toLong())[0] + + init { + if (cbits == 0L) { + throw IllegalStateException() + } + } + + private val memory = instance.memory() as ByteBufferMemory + + fun decode(packet: ByteArray): FloatArray { + if (packet.size > MAX_PACKET_SIZE) { + throw IllegalArgumentException("packet too big") + } + memory.getBuffer().put(cbits.toInt(), packet) + val decoded = + opusDecodeFloat.apply(decoder, cbits, packet.size.toLong(), outBuf, MAX_FRAME_SIZE.toLong(), 0L)[0] + if (decoded < 0L) { + throw IllegalStateException( + instance.memory().readCString(instance.export("opus_strerror").apply(decoded)[0].toInt()) + ) + } + val out = FloatArray(decoded.toInt()) + memory.getBuffer().slice(outBuf.toInt(), outBuf.toInt() + 4 * channels * decoded.toInt()) + .order(ByteOrder.LITTLE_ENDIAN).asFloatBuffer().get(out) + return out + } + + companion object { + const val MAX_FRAME_SIZE = 6 * 960 + const val MAX_PACKET_SIZE = 3 * 1276 + } +} \ No newline at end of file diff --git a/src/main/kotlin/space/autistic/radio/opus/OpusFactory.kt b/src/main/kotlin/space/autistic/radio/opus/OpusFactory.kt new file mode 100644 index 0000000..70e0c3c --- /dev/null +++ b/src/main/kotlin/space/autistic/radio/opus/OpusFactory.kt @@ -0,0 +1,26 @@ +package space.autistic.radio.opus + +import com.dylibso.chicory.experimental.aot.AotMachineFactory +import com.dylibso.chicory.runtime.ImportValues +import com.dylibso.chicory.runtime.Instance +import com.dylibso.chicory.wasm.Parser +import net.fabricmc.loader.api.FabricLoader +import java.io.InputStream + +object OpusFactory : () -> Instance { + private val defaultImports = ImportValues.builder().build() + private val module = Parser.parse(getModuleInputStream()) + private val instanceBuilder = + Instance.builder(module) + .withMachineFactory(AotMachineFactory(module)) + .withImportValues(defaultImports) + + override fun invoke(): Instance = instanceBuilder.build() + + private fun getModuleInputStream(): InputStream { + return FabricLoader.getInstance().getModContainer("pirate-radio").flatMap { it.findPath("opus.wasm") } + .map<InputStream?> { it.toFile().inputStream() }.orElseGet { + this.javaClass.getResourceAsStream("/opus.wasm") + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/space/autistic/radio/reflection/MemoryReflection.kt b/src/main/kotlin/space/autistic/radio/reflection/MemoryReflection.kt new file mode 100644 index 0000000..78961da --- /dev/null +++ b/src/main/kotlin/space/autistic/radio/reflection/MemoryReflection.kt @@ -0,0 +1,14 @@ +package space.autistic.radio.reflection + +import com.dylibso.chicory.runtime.ByteBufferMemory +import java.lang.invoke.MethodHandles +import java.nio.ByteBuffer + +fun ByteBufferMemory.getBuffer(): ByteBuffer { + return MemoryReflection.buffer.get(this) as ByteBuffer +} + +object MemoryReflection { + val buffer = MethodHandles.privateLookupIn(ByteBufferMemory::class.java, MethodHandles.lookup()) + .findVarHandle(ByteBufferMemory::class.java, "buffer", ByteBuffer::class.java) +} \ No newline at end of file diff --git a/src/main/resources/assets/pirate-radio/icon.png b/src/main/resources/assets/pirate-radio/icon.png new file mode 100644 index 0000000..62adcdd --- /dev/null +++ b/src/main/resources/assets/pirate-radio/icon.png Binary files differdiff --git a/src/main/resources/assets/pirate-radio/lang/en_us.json b/src/main/resources/assets/pirate-radio/lang/en_us.json new file mode 100644 index 0000000..9627729 --- /dev/null +++ b/src/main/resources/assets/pirate-radio/lang/en_us.json @@ -0,0 +1,10 @@ +{ + "item.pirate-radio.sbc": "Raspberry Pi", + "item.pirate-radio.wire": "Piece of Wire", + "item.pirate-radio.powerbank": "Powerbank", + "item.pirate-radio.storage-card": "SD Card", + "item.pirate-radio.disposable-transmitter": "Disposable Pirate Radio Transmitter", + "item.pirate-radio.fm-receiver": "FM Receiver", + "entity.pirate-radio.electronics-trader": "Microcenter", + "pirate-radio.fm-receiver": "FM Receiver" +} \ No newline at end of file diff --git a/src/main/resources/assets/pirate-radio/textures/item/powerbank.png b/src/main/resources/assets/pirate-radio/textures/item/powerbank.png new file mode 100644 index 0000000..0f1685f --- /dev/null +++ b/src/main/resources/assets/pirate-radio/textures/item/powerbank.png Binary files differdiff --git a/src/main/resources/assets/pirate-radio/textures/item/sbc.png b/src/main/resources/assets/pirate-radio/textures/item/sbc.png new file mode 100644 index 0000000..38a90a4 --- /dev/null +++ b/src/main/resources/assets/pirate-radio/textures/item/sbc.png Binary files differdiff --git a/src/main/resources/assets/pirate-radio/textures/item/storage-card.png b/src/main/resources/assets/pirate-radio/textures/item/storage-card.png new file mode 100644 index 0000000..bf4b60b --- /dev/null +++ b/src/main/resources/assets/pirate-radio/textures/item/storage-card.png Binary files differdiff --git a/src/main/resources/assets/pirate-radio/textures/item/wire.png b/src/main/resources/assets/pirate-radio/textures/item/wire.png new file mode 100644 index 0000000..8b5b330 --- /dev/null +++ b/src/main/resources/assets/pirate-radio/textures/item/wire.png Binary files differdiff --git a/src/main/resources/fabric.mod.json b/src/main/resources/fabric.mod.json new file mode 100644 index 0000000..71f2518 --- /dev/null +++ b/src/main/resources/fabric.mod.json @@ -0,0 +1,44 @@ +{ + "schemaVersion": 1, + "id": "pirate-radio", + "version": "${version}", + "name": "Pirate Radio", + "description": "This is an example description! Tell everyone what your mod is about!", + "authors": [ + "Me!" + ], + "contact": { + "homepage": "https://fabricmc.net/", + "sources": "https://github.com/FabricMC/fabric-example-mod" + }, + "license": "LGPL-2.1-or-later", + "icon": "assets/pirate-radio/icon.png", + "environment": "*", + "entrypoints": { + "main": [ + { + "value": "space.autistic.radio.PirateRadio", + "adapter": "kotlin" + } + ], + "client": [ + { + "value": "space.autistic.radio.client.PirateRadioClient", + "adapter": "kotlin" + } + ], + "fabric-datagen": [ + { + "value": "space.autistic.radio.client.PirateRadioDataGenerator", + "adapter": "kotlin" + } + ] + }, + "depends": { + "fabricloader": ">=0.16.10", + "minecraft": "~1.21.1", + "java": ">=21", + "fabric-api": "*", + "fabric-language-kotlin": "*" + } +} \ No newline at end of file diff --git a/src/test/kotlin/space/autistic/radio/complex/ComplexKtTest.kt b/src/test/kotlin/space/autistic/radio/complex/ComplexKtTest.kt new file mode 100644 index 0000000..a4dfe91 --- /dev/null +++ b/src/test/kotlin/space/autistic/radio/complex/ComplexKtTest.kt @@ -0,0 +1,13 @@ +package space.autistic.radio.complex + +import org.joml.Vector2f +import org.junit.jupiter.api.Assertions.* +import kotlin.test.Test + +class ComplexKtTest { + @Test + fun testI() { + assertEquals(I.cmul(I), Vector2f(-1f, 0f)) + assertNotSame(I, I) + } +} \ No newline at end of file diff --git a/src/test/kotlin/space/autistic/radio/fmsim/TestAsserts.kt b/src/test/kotlin/space/autistic/radio/fmsim/TestAsserts.kt new file mode 100644 index 0000000..8a4862c --- /dev/null +++ b/src/test/kotlin/space/autistic/radio/fmsim/TestAsserts.kt @@ -0,0 +1,13 @@ +package space.autistic.radio.fmsim + +import kotlin.test.Test + +class TestAsserts { + @Test + fun testFmFullSim() { + // initialize and flush an FM modulator + // if anything asserts, this should catch it + val fmFullModulator = FmFullModulator() + fmFullModulator.flush(1f) {} + } +} \ No newline at end of file |