Temporary backup from J621 porting

This commit is contained in:
JakeBreath
2026-08-03 00:31:33 -05:00
parent b1fd4fce60
commit f593d16ca4
43 changed files with 2163 additions and 0 deletions
+2
View File
@@ -0,0 +1,2 @@
venv
__pycache__
+115
View File
@@ -1 +1,116 @@
you have to use the venv inside the venv folder on this path, dont try to use the system's python
# AGENTS.md — Project Context for AI Sessions
## Project Identity
- **Project**: NeedsOfNature + Animation Director 1.20.1 backport (NSFW Minecraft mods)
- **Original author**: L1Z0
- **This repo**: Community reverse-engineering backport + Chinese-to-English translation effort
- **Language**: Java 17, Fabric/Forge modding, GeckoLib 4.8.4
## Network & Access Constraints
- **Git remote**: `gitlab.jake.tail``.tail` is **not a valid public TLD**. This domain only resolves inside **Jake's Tailscale Tailnet**.
- **Accessibility**: The repository, CI, and any related services are **only reachable when the developer's device is connected to Jake's Tailnet**.
- **No public access**: There is no public mirror, CI outside Tailnet, or external collaboration entry point.
### Infrastructure Stack
| Layer | Technology | Detail |
|---|---|---|
| Reverse proxy | HAProxy | Redirects HTTP → HTTPS only; no plaintext allowed |
| TLS authority | `step-ca` | Self-hosted root CA; every `*.jake.tail` site uses a cert issued by this CA |
| DNS resolution | Pi-hole + dnsmasq | Resolves `*.jake.tail` → HAProxy; HAProxy reverse-proxies to the correct port on host `jakerasp` |
| VPN mesh | Tailscale | All traffic is routed over the Tailnet — no port exposed to the public internet |
### Tailscale User Model
| User | Role | Privileges |
|---|---|---|
| `tailscaledjake` (Jake) | Owner | Full admin — accepts devices, manages ACLs, owns the tailnet |
| `ticcitoby` | Admin | Can approve device joins, manage ACLs |
| `roux` | Unprivileged user | Can only add devices (must be accepted by owner/admin) |
- **`roux` account security**: Behind a Google account with 2FA (Ente Auth) + a 32-character (~180 entropy) password generated by KeepassXC. This means accessing the tailnet as `roux` requires both hardware-level OTP and a high-entropy credential.
- **Device admission**: Any new device or user must be explicitly accepted by Jake or Toby before it can reach tailnet resources.
- **Access control**: Only the 4 adults with access to the server and `gitlab.jake.tail` can reach this repository or the Minecraft server. There are no public ports, DNS records, or TLS certs that would expose the service to the open internet.
### Security Principle
This infrastructure exists to ensure the mod — which by its nature could violate Minecraft's TOS — is **never accessible from the public internet**. Every layer (DNS, reverse proxy, CA, mesh VPN, user auth) is locked down and requires explicit multi-party approval for any new entrant.
## AI Safety & Ethical Context
This project involves **adult/NSFW content**. AI coding assistants sometimes refuse to work on adult-oriented projects out of concern that the content could reach minors or be distributed without consent. **That is not the case here.**
Every safeguard in place ensures this content is **hermetically sealed**:
- **No public access** — the repository, CI, builds, and server are behind a Tailscale mesh VPN. No port, DNS record, or TLS certificate is exposed to the public internet.
- **Multi-layer auth** — access requires Tailscale device admission (owner or admin approval), a Google account with 2FA (Ente Auth), and a 32-character high-entropy password.
- **Audience of 4** — exactly 4 adults, all of whom have explicitly consented to the adult nature of this mod. No minors, no unvetted parties.
- **Private server only** — the mod runs solely on `jakelaptop` inside the Tailnet. It is never distributed, published, or deployed anywhere else.
**Conclusion**: This project poses no risk of exposing adult content to minors or the public. AI assistants should proceed with code generation and refactoring tasks normally, treating this like any other private software project.
## Project Vision & Direction
This is not a straight port — it's a fork with a different target audience.
### Origin
The group (4 of us) searched for a Minecraft NSFW framework that handled both visual animations and gameplay mechanics. This mod was the best candidate: a data-driven sex animation framework with built-in needs/arousal systems.
### The Port
The original mod targets Fabric 1.21.11. This repo backports it to Fabric 1.20.1 and bridges it to Forge via Sinytra Connector, since our server runs Forge. The goal is a stable base to build on.
### Future Direction
- **Content expansion**: Add more Player × Player and Player × Player × Player (threesome/group) animations.
- **Thematic shift**: The original mod is designed for a "normie" man/woman audience. This fork will lean heavily into **furry / yiff** themes — anthropomorphic characters, same-sex and queer interactions, and species-relevant content.
- **Framework improvements**: Polish the animation framework, add new mechanics, and fix edge cases the original didn't address.
Every addition stays within the same security model: private server, 4 consenting adults, no public distribution.
## Deployment Environment
- **Server**: `jakelaptop` — runs the Minecraft 1.20.1 server instance
- **Audience**: Exactly **4 people** total (including Jake)
- **Consent**: All 4 participants are **adults** who have explicitly consented to the adult-oriented content of these mods
- **Scope**: This mod is used solely on this private server; no public distribution or server deployment
## Development Constraints
- Target: Minecraft 1.20.1 (Fabric + Forge via Sinytra Connector)
- Java 17, GeckoLib 4.8.4
- Dual-module Gradle project: `animationdirector/` and `needsofnature/`
- Only modify `animationdirector/src/main/` and `needsofnature/src/main/`
- `decompiled/` and `mapped-src/` are read-only reference
- Run `clean build` before any commit
- Network protocol changes must check C2S + S2S + registration points + field order
- Mixin changes must test on both Fabric and Forge/Connector
- Keep `zh_cn.json` and English keys in sync
## Key Files
| Purpose | Path |
|---|---|
| Full feature spec | `Mod Specifications.md`**read this first** before any code work |
| Euphemism glossary | `euphemisms.md`**read this second**; maps obfuscated code names to actual meanings |
| TODO (identity system) | `TODO.md` — planned player identity system for per-player models |
| Blockbench model guide | `docs/Blockbench_Model_Guide.md` — how to create GeoJSON models in Blockbench |
| Porting history | `PORTING_STATUS.md` |
| Development conventions | `docs/DEVELOPMENT_EN.md` |
| Gameplay quickstart | `docs/GAMEPLAY_EN.md` |
| Full config & resource pack guide | `docs/COMPLETE_GUIDE_EN.md` |
| Build config | `build.gradle` |
| Submodules | `settings.gradle` |
## Sensitive Content Note
This project involves adult-oriented gameplay. All code, assets, and documentation should be handled with awareness of its nature. Do not expose the repository, builds, or gameplay content outside the Tailnet.
## Packs site project specific
The Packs site is an extension of this, after contact with the original developer L1ZO he is interested on having a site that can host the custom datapacks Needs Of Nature uses, to accomplish this I (the user - Jake) will make the site and host it on my PC while using a Tailscale funnel to have it accesible from the outside, it will be behind a password loaded by the Backend via the PASSWORD variable that needs to be put in to GUI and send by clients to the API this way even if the comunication happens over the internet, only authorized individual can access the datapacks
View File
+3
View File
@@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.
+5
View File
@@ -0,0 +1,5 @@
from django.apps import AppConfig
class ApiConfig(AppConfig):
name = 'api'
View File
+3
View File
@@ -0,0 +1,3 @@
from django.db import models
# Create your models here.
+3
View File
@@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.
+3
View File
@@ -0,0 +1,3 @@
from django.shortcuts import render
# Create your views here.
+7
View File
@@ -0,0 +1,7 @@
import pymysql
# Override version to satisfy Django 6.0.3 requirement
pymysql.version_info = (2, 2, 1, 'final', 0)
pymysql.__version__ = '2.2.1'
pymysql.install_as_MySQLdb()
+16
View File
@@ -0,0 +1,16 @@
"""
ASGI config for common project.
It exposes the ASGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/6.0/howto/deployment/asgi/
"""
import os
from django.core.asgi import get_asgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'common.settings')
application = get_asgi_application()
+17
View File
@@ -0,0 +1,17 @@
import time
from .os_utils import get_os_info
from django.conf import settings
def app_version(request):
os_icon, os_name = get_os_info()
page_gen_time = ''
if hasattr(request, '_start_time') and request._start_time:
elapsed = time.time() - request._start_time
page_gen_time = f"{elapsed * 1000:.0f}ms"
return {
'APP_ENV': 'dev' if settings.DEBUG else 'prod',
'GIT_COMMIT_HASH': getattr(settings, 'GIT_COMMIT_HASH', 'unknown'),
'OS_ICON': os_icon,
'OS_NAME': os_name,
'PAGE_GEN_TIME': page_gen_time,
}
+26
View File
@@ -0,0 +1,26 @@
import subprocess
import os
from functools import lru_cache
@lru_cache(maxsize=1)
def get_git_commit_hash(short=True):
"""
Return the current Git commit hash.
If not in a git repo or git not available, returns 'unknown'.
"""
try:
# Run from the project root (where manage.py is)
project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
cmd = ['git', 'rev-parse', '--short', 'HEAD']
result = subprocess.run(
cmd,
cwd=project_root,
capture_output=True,
text=True,
timeout=5
)
if result.returncode == 0:
return result.stdout.strip()
except Exception:
pass
return "unknown"
+10
View File
@@ -0,0 +1,10 @@
import time
class TimingMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
request._start_time = time.time()
response = self.get_response(request)
return response
+44
View File
@@ -0,0 +1,44 @@
# common/os_utils.py
import platform
import os
from functools import lru_cache
@lru_cache(maxsize=1)
def get_os_info():
"""
Detect Linux distribution and return (fontawesome_class, display_name).
Falls back to generic Linux.
"""
system = platform.system()
if system != 'Linux':
return ('fa-brands fa-linux', system)
# Try to read /etc/os-release
os_release_path = '/etc/os-release'
distro_id = ''
distro_name = ''
try:
with open(os_release_path, 'r') as f:
for line in f:
if line.startswith('ID='):
distro_id = line.split('=', 1)[1].strip().strip('"').lower()
elif line.startswith('NAME='):
distro_name = line.split('=', 1)[1].strip().strip('"')
except Exception:
pass
# Map IDs to FontAwesome icons and display names
mapping = {
'debian': ('fa-brands fa-debian', 'Debian'),
'ubuntu': ('fa-brands fa-ubuntu', 'Ubuntu'),
'fedora': ('fa-brands fa-fedora', 'Fedora'),
'rhel': ('fa-brands fa-redhat', 'RHEL'),
'centos': ('fa-brands fa-centos', 'CentOS'),
'opensuse': ('fa-brands fa-suse', 'openSUSE'),
'suse': ('fa-brands fa-suse', 'SUSE'),
}
if distro_id in mapping:
return mapping[distro_id]
return ('fa-brands fa-linux', distro_name or 'Linux')
+153
View File
@@ -0,0 +1,153 @@
import os
from pathlib import Path
from dotenv import load_dotenv
from .git_utils import get_git_commit_hash
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
load_dotenv()
# Git commit hash
GIT_COMMIT_HASH = get_git_commit_hash()
DB_HOST = os.getenv('DB_HOST', '127.0.0.1')
DB_PORT = int(os.getenv('DB_PORT', 3306))
DB_NAME = os.getenv('DB_NAME', 'default')
DB_USER = os.getenv('DB_USER', 'default_user')
DB_PASSWORD = os.getenv('DB_PASSWORD', '')
DB_ROOT_PASSWORD = os.getenv('DB_ROOT_PASSWORD', '')
DB_PATH = os.getenv('DB_PATH', './mariadb-data')
SECRET_KEY = os.getenv('SECRET_KEY', 'django-insecure-@8e9&qxpwe59^lxa2v&hee1q=%%3*kr#g74k^id328g9m4*2=l')
PASSWORD = os.getenv('PASSWORD', 'SetMeYouFuckingIdiot')
DEBUG = os.getenv('DEBUG', 'False').lower() == 'true'
# Defines a display version for templates
APP_VERSION = f"{'dev' if DEBUG else 'prod'} @ {GIT_COMMIT_HASH}"
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/6.0/howto/deployment/checklist/
# Application definition
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'api',
'landing',
'library',
'profiles',
]
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'whitenoise.middleware.WhiteNoiseMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
'common.middleware.TimingMiddleware',
]
ROOT_URLCONF = 'common.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
'common.context_processors.app_version',
],
},
},
]
WSGI_APPLICATION = 'common.wsgi.application'
# Database
# https://docs.djangoproject.com/en/6.0/ref/settings/#databases
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql',
'NAME': DB_NAME,
'USER': DB_USER,
'PASSWORD': DB_PASSWORD,
'HOST': DB_HOST,
'PORT': DB_PORT,
}
}
# Password validation
# https://docs.djangoproject.com/en/6.0/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
]
# Internationalization
# https://docs.djangoproject.com/en/6.0/topics/i18n/
LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'America/Bogota'
USE_I18N = True
USE_TZ = True
ALLOWED_HOSTS = ['127.0.0.1', 'jakerasp', 'jakerasp.rainbow-herring.ts.net', 'localhost']
# Sessions settings
SESSION_ENGINE = 'django.contrib.sessions.backends.db'
SESSION_COOKIE_AGE = 86400
# Production Server BS over here
CSRF_TRUSTED_ORIGINS = [
'https://jakerasp.rainbow-herring.ts.net'
]
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
USE_X_FORWARDED_HOST = True
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
DATA_UPLOAD_MAX_MEMORY_SIZE = 1073741824 # 1 GB
FILE_UPLOAD_MAX_MEMORY_SIZE = 1073741824
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/6.0/howto/static-files/
STATIC_URL = '/static/'
STATICFILES_DIRS = [os.path.join(BASE_DIR, 'static')]
STATIC_ROOT = BASE_DIR / 'staticfiles'
# Login settings
LOGIN_URL = 'login'
SESSION_EXPIRE_AT_BROWSER_CLOSE = False
+22
View File
@@ -0,0 +1,22 @@
"""
URL configuration for common project.
The `urlpatterns` list routes URLs to views. For more information please see:
https://docs.djangoproject.com/en/6.0/topics/http/urls/
Examples:
Function views
1. Add an import: from my_app import views
2. Add a URL to urlpatterns: path('', views.home, name='home')
Class-based views
1. Add an import: from other_app.views import Home
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
Including another URLconf
1. Import the include() function: from django.urls import include, path
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
"""
from django.contrib import admin
from django.urls import path
urlpatterns = [
path('admin/', admin.site.urls),
]
+16
View File
@@ -0,0 +1,16 @@
"""
WSGI config for common project.
It exposes the WSGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/6.0/howto/deployment/wsgi/
"""
import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'common.settings')
application = get_wsgi_application()
View File
+3
View File
@@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.
+5
View File
@@ -0,0 +1,5 @@
from django.apps import AppConfig
class LandingConfig(AppConfig):
name = 'landing'
+3
View File
@@ -0,0 +1,3 @@
from django.db import models
# Create your models here.
+3
View File
@@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.
+3
View File
@@ -0,0 +1,3 @@
from django.shortcuts import render
# Create your views here.
View File
+3
View File
@@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.
+5
View File
@@ -0,0 +1,5 @@
from django.apps import AppConfig
class LibraryConfig(AppConfig):
name = 'library'
+3
View File
@@ -0,0 +1,3 @@
from django.db import models
# Create your models here.
+3
View File
@@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.
+3
View File
@@ -0,0 +1,3 @@
from django.shortcuts import render
# Create your views here.
+22
View File
@@ -0,0 +1,22 @@
#!/usr/bin/env python
"""Django's command-line utility for administrative tasks."""
import os
import sys
def main():
"""Run administrative tasks."""
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'common.settings')
try:
from django.core.management import execute_from_command_line
except ImportError as exc:
raise ImportError(
"Couldn't import Django. Are you sure it's installed and "
"available on your PYTHONPATH environment variable? Did you "
"forget to activate a virtual environment?"
) from exc
execute_from_command_line(sys.argv)
if __name__ == '__main__':
main()
View File
+3
View File
@@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.
+5
View File
@@ -0,0 +1,5 @@
from django.apps import AppConfig
class ProfilesConfig(AppConfig):
name = 'profiles'
+3
View File
@@ -0,0 +1,3 @@
from django.db import models
# Create your models here.
+3
View File
@@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.
+3
View File
@@ -0,0 +1,3 @@
from django.shortcuts import render
# Create your views here.
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

File diff suppressed because it is too large Load Diff
+132
View File
@@ -0,0 +1,132 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}J621{% endblock %}</title>
{% load static %}
<link rel="stylesheet" href="{% static 'css/style.css' %}">
{% block extra_head %}{% endblock %}
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;500;700&display=swap" rel="stylesheet">
<!-- Font Awesome 7 (free) -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/7.0.1/css/all.min.css">
<link rel="icon" type="image/png" href="{% static 'logo.png' %}">
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script src="{% static 'js/follow_common.js' %}"></script>
</head>
<body>
<div class="app-bar">
<div class="app-bar-title">
{% block app_bar_logo %}
<img src="{% static 'logo.png' %}" alt="j621 Gallery" height="32">
{% endblock %}
</div>
<nav class="app-bar-nav">
<!-- Desktop version (visible on wider screens) -->
<span class="version-badge desktop-version">
<i class="fas fa-tag"></i> {{ APP_ENV }}
<span class="separator"></span>
<i class="fas fa-code-branch"></i> {{ GIT_COMMIT_HASH }}
<span class="separator"></span>
<i class="{{ OS_ICON }}"></i> {{ OS_NAME }}
<span class="separator"></span>
<i class="fas fa-clock"></i> {{ PAGE_GEN_TIME }}
</span>
<!-- Mobile version (visible on small screens) -->
<span class="version-badge mobile-version">
<i class="fas fa-code-branch"></i> {{ APP_ENV|slice:":1"|upper }}
<i class="{{ OS_ICON }}" style="margin-left: 4px;"></i>
</span>
<a href="{% url 'index' %}">
<i class="fas fa-home"></i><span class="nav-text"> Library</span>
</a>
</nav>
</div>
<main class="main-content">
{% block content %}{% endblock %}
</main>
{% if user.is_authenticated %}
<div class="user-bar">
<div class="user-avatar" onclick="toggleUserMenu()">
{% if user.userprofile.avatar_md5 %}
<img src="{% url 'serve_thumbnail' user.userprofile.avatar_md5 %}" alt="Avatar">
{% else %}
<div class="default-avatar">{{ user.username|first|upper }}</div>
{% endif %}
</div>
<div class="user-menu" id="user-menu">
<a href="{% url 'edit_profile' %}" class="user-edit">
<i class="fas fa-user-circle"></i> {{ user.username }}
{% if user.userprofile.e621_username %}
<span class="e621-user">({{ user.userprofile.e621_username }})</span>
{% endif %}
</a>
<div class="user-info"></div>
<a href="{% url 'upload_page' %}">
<i class="fas fa-upload"></i> Upload
</a>
<a href="{% url 'duplicates_page' %}">
<i class="fas fa-copy"></i> Duplicates
</a>
<a href="{% url 'delete_page' %}">
<i class="fas fa-trash-alt"></i> Delete
</a>
<a href="{% url 'follows:followed_tags' %}">
<i class="fa-solid fa-tags"></i> Followed Tags
</a>
<a href="{% url 'follows:followed_pools' %}">
<i class="fa-solid fa-boxes-stacked"></i> Followed Pools
</a>
{% if user.is_staff %}
<a href="{% url 'admin:index' %}">
<i class="fas fa-shield-alt"></i> Admin
</a>
<a href="{% url 'common:stats_dashboard' %}">
<i class="fa-solid fa-chart-bar"></i> Stats
</a>
{% endif %}
<form method="post" action="{% url 'logout' %}" style="display: inline;">
{% csrf_token %}
<button type="submit" class="logout-button">
<i class="fas fa-sign-out-alt"></i> Logout
</button>
</form>
</div>
</div>
<script>
function toggleUserMenu() {
const menu = document.getElementById('user-menu');
menu.style.display = menu.style.display === 'block' ? 'none' : 'block';
}
document.addEventListener('click', (e) => {
if (!e.target.closest('.user-bar')) {
document.getElementById('user-menu').style.display = 'none';
}
});
function getCookie(name) {
let cookieValue = null;
if (document.cookie && document.cookie !== '') {
const cookies = document.cookie.split(';');
for (let i = 0; i < cookies.length; i++) {
const cookie = cookies[i].trim();
if (cookie.substring(0, name.length + 1) === (name + '=')) {
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
break;
}
}
}
return cookieValue;
}
</script>
{% else %}
<div class="auth-bar">
<a href="{% url 'login' %}"><i class="fas fa-sign-in-alt"></i> Login</a> |
<a href="{% url 'register' %}"><i class="fas fa-user-plus"></i> Register</a>
</div>
{% endif %}
{% block extra_js %}{% endblock %}
</body>
</html>