diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 00000000..26d33521 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml diff --git a/.idea/PythonPlantsVsZombies.iml b/.idea/PythonPlantsVsZombies.iml new file mode 100644 index 00000000..d0876a78 --- /dev/null +++ b/.idea/PythonPlantsVsZombies.iml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml new file mode 100644 index 00000000..105ce2da --- /dev/null +++ b/.idea/inspectionProfiles/profiles_settings.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 00000000..2a2c1b9b --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 00000000..35c0ec56 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 00000000..35eb1ddf --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/backend/__init__.py b/backend/__init__.py new file mode 100644 index 00000000..9ea1dc3a --- /dev/null +++ b/backend/__init__.py @@ -0,0 +1,6 @@ +# 导入Flask应用和扩展 +from .app import app, db +from .models import User, Role, Permission, Log, Resource + +# 导入创建初始数据的函数 +from .models import create_initial_data \ No newline at end of file diff --git a/backend/__pycache__/__init__.cpython-312.pyc b/backend/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 00000000..4fc768ea Binary files /dev/null and b/backend/__pycache__/__init__.cpython-312.pyc differ diff --git a/backend/__pycache__/app.cpython-312.pyc b/backend/__pycache__/app.cpython-312.pyc new file mode 100644 index 00000000..707451d9 Binary files /dev/null and b/backend/__pycache__/app.cpython-312.pyc differ diff --git a/backend/__pycache__/models.cpython-312.pyc b/backend/__pycache__/models.cpython-312.pyc new file mode 100644 index 00000000..3e098054 Binary files /dev/null and b/backend/__pycache__/models.cpython-312.pyc differ diff --git a/backend/app.db b/backend/app.db new file mode 100644 index 00000000..a80497e4 Binary files /dev/null and b/backend/app.db differ diff --git a/backend/app.py b/backend/app.py new file mode 100644 index 00000000..7dad97c7 --- /dev/null +++ b/backend/app.py @@ -0,0 +1,44 @@ +from flask import Flask, jsonify +from flask_sqlalchemy import SQLAlchemy +from flask_migrate import Migrate +from flask_jwt_extended import JWTManager +from flask_cors import CORS +import os + +# 创建Flask应用 +app = Flask(__name__) +CORS(app) + +# 配置应用 +app.config['SECRET_KEY'] = os.environ.get('SECRET_KEY') or 'your-secret-key-here' +app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///' + os.path.join(os.path.abspath(os.path.dirname(__file__)), 'app.db') +app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False + +# 配置JWT +app.config['JWT_SECRET_KEY'] = os.environ.get('JWT_SECRET_KEY') or 'your-jwt-secret-key-here' +app.config['JWT_ACCESS_TOKEN_EXPIRES'] = 3600 # 1小时 +app.config['JWT_REFRESH_TOKEN_EXPIRES'] = 604800 # 7天 + +# 初始化扩展 +db = SQLAlchemy(app) +migrate = Migrate(app, db) +jwt = JWTManager(app) + +# 导入模型 +from .models import User, Role, Permission, Log, Resource + +# 导入路由 +from .routes import auth, logs, resources + +# 注册蓝图 +app.register_blueprint(auth.bp, url_prefix='/api/auth') +app.register_blueprint(logs.bp, url_prefix='/api/logs') +app.register_blueprint(resources.bp, url_prefix='/api/resources') + +# 根路由 +@app.route('/') +def index(): + return jsonify({'message': 'Welcome to Plants vs Zombies API'}) + +if __name__ == '__main__': + app.run(debug=True) \ No newline at end of file diff --git a/backend/migrations/README b/backend/migrations/README new file mode 100644 index 00000000..0e048441 --- /dev/null +++ b/backend/migrations/README @@ -0,0 +1 @@ +Single-database configuration for Flask. diff --git a/backend/migrations/__pycache__/env.cpython-312.pyc b/backend/migrations/__pycache__/env.cpython-312.pyc new file mode 100644 index 00000000..2abc36f4 Binary files /dev/null and b/backend/migrations/__pycache__/env.cpython-312.pyc differ diff --git a/backend/migrations/alembic.ini b/backend/migrations/alembic.ini new file mode 100644 index 00000000..ec9d45c2 --- /dev/null +++ b/backend/migrations/alembic.ini @@ -0,0 +1,50 @@ +# A generic, single database configuration. + +[alembic] +# template used to generate migration files +# file_template = %%(rev)s_%%(slug)s + +# set to 'true' to run the environment during +# the 'revision' command, regardless of autogenerate +# revision_environment = false + + +# Logging configuration +[loggers] +keys = root,sqlalchemy,alembic,flask_migrate + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARN +handlers = console +qualname = + +[logger_sqlalchemy] +level = WARN +handlers = +qualname = sqlalchemy.engine + +[logger_alembic] +level = INFO +handlers = +qualname = alembic + +[logger_flask_migrate] +level = INFO +handlers = +qualname = flask_migrate + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(levelname)-5.5s [%(name)s] %(message)s +datefmt = %H:%M:%S diff --git a/backend/migrations/env.py b/backend/migrations/env.py new file mode 100644 index 00000000..4c970927 --- /dev/null +++ b/backend/migrations/env.py @@ -0,0 +1,113 @@ +import logging +from logging.config import fileConfig + +from flask import current_app + +from alembic import context + +# this is the Alembic Config object, which provides +# access to the values within the .ini file in use. +config = context.config + +# Interpret the config file for Python logging. +# This line sets up loggers basically. +fileConfig(config.config_file_name) +logger = logging.getLogger('alembic.env') + + +def get_engine(): + try: + # this works with Flask-SQLAlchemy<3 and Alchemical + return current_app.extensions['migrate'].db.get_engine() + except (TypeError, AttributeError): + # this works with Flask-SQLAlchemy>=3 + return current_app.extensions['migrate'].db.engine + + +def get_engine_url(): + try: + return get_engine().url.render_as_string(hide_password=False).replace( + '%', '%%') + except AttributeError: + return str(get_engine().url).replace('%', '%%') + + +# add your model's MetaData object here +# for 'autogenerate' support +# from myapp import mymodel +# target_metadata = mymodel.Base.metadata +config.set_main_option('sqlalchemy.url', get_engine_url()) +target_db = current_app.extensions['migrate'].db + +# other values from the config, defined by the needs of env.py, +# can be acquired: +# my_important_option = config.get_main_option("my_important_option") +# ... etc. + + +def get_metadata(): + if hasattr(target_db, 'metadatas'): + return target_db.metadatas[None] + return target_db.metadata + + +def run_migrations_offline(): + """Run migrations in 'offline' mode. + + This configures the context with just a URL + and not an Engine, though an Engine is acceptable + here as well. By skipping the Engine creation + we don't even need a DBAPI to be available. + + Calls to context.execute() here emit the given string to the + script output. + + """ + url = config.get_main_option("sqlalchemy.url") + context.configure( + url=url, target_metadata=get_metadata(), literal_binds=True + ) + + with context.begin_transaction(): + context.run_migrations() + + +def run_migrations_online(): + """Run migrations in 'online' mode. + + In this scenario we need to create an Engine + and associate a connection with the context. + + """ + + # this callback is used to prevent an auto-migration from being generated + # when there are no changes to the schema + # reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html + def process_revision_directives(context, revision, directives): + if getattr(config.cmd_opts, 'autogenerate', False): + script = directives[0] + if script.upgrade_ops.is_empty(): + directives[:] = [] + logger.info('No changes in schema detected.') + + conf_args = current_app.extensions['migrate'].configure_args + if conf_args.get("process_revision_directives") is None: + conf_args["process_revision_directives"] = process_revision_directives + + connectable = get_engine() + + with connectable.connect() as connection: + context.configure( + connection=connection, + target_metadata=get_metadata(), + **conf_args + ) + + with context.begin_transaction(): + context.run_migrations() + + +if context.is_offline_mode(): + run_migrations_offline() +else: + run_migrations_online() diff --git a/backend/migrations/script.py.mako b/backend/migrations/script.py.mako new file mode 100644 index 00000000..2c015630 --- /dev/null +++ b/backend/migrations/script.py.mako @@ -0,0 +1,24 @@ +"""${message} + +Revision ID: ${up_revision} +Revises: ${down_revision | comma,n} +Create Date: ${create_date} + +""" +from alembic import op +import sqlalchemy as sa +${imports if imports else ""} + +# revision identifiers, used by Alembic. +revision = ${repr(up_revision)} +down_revision = ${repr(down_revision)} +branch_labels = ${repr(branch_labels)} +depends_on = ${repr(depends_on)} + + +def upgrade(): + ${upgrades if upgrades else "pass"} + + +def downgrade(): + ${downgrades if downgrades else "pass"} diff --git a/backend/migrations/versions/0016b73ba5fb_initial_migration.py b/backend/migrations/versions/0016b73ba5fb_initial_migration.py new file mode 100644 index 00000000..807f0ae6 --- /dev/null +++ b/backend/migrations/versions/0016b73ba5fb_initial_migration.py @@ -0,0 +1,102 @@ +"""Initial migration + +Revision ID: 0016b73ba5fb +Revises: +Create Date: 2025-11-03 19:21:31.818139 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '0016b73ba5fb' +down_revision = None +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('permission', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('name', sa.String(length=50), nullable=False), + sa.Column('description', sa.String(length=255), nullable=True), + sa.Column('created_at', sa.DateTime(), nullable=True), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('name') + ) + op.create_table('role', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('name', sa.String(length=50), nullable=False), + sa.Column('description', sa.String(length=255), nullable=True), + sa.Column('created_at', sa.DateTime(), nullable=True), + sa.Column('updated_at', sa.DateTime(), nullable=True), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('name') + ) + op.create_table('user', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('username', sa.String(length=80), nullable=False), + sa.Column('email', sa.String(length=120), nullable=False), + sa.Column('password_hash', sa.String(length=128), nullable=False), + sa.Column('is_verified', sa.Boolean(), nullable=True), + sa.Column('created_at', sa.DateTime(), nullable=True), + sa.Column('updated_at', sa.DateTime(), nullable=True), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('email'), + sa.UniqueConstraint('username') + ) + op.create_table('log', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('user_id', sa.Integer(), nullable=False), + sa.Column('title', sa.String(length=100), nullable=False), + sa.Column('content', sa.Text(), nullable=False), + sa.Column('tags', sa.String(length=255), nullable=True), + sa.Column('importance', sa.Integer(), nullable=True), + sa.Column('created_at', sa.DateTime(), nullable=True), + sa.Column('updated_at', sa.DateTime(), nullable=True), + sa.ForeignKeyConstraint(['user_id'], ['user.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_table('resource', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('user_id', sa.Integer(), nullable=False), + sa.Column('name', sa.String(length=100), nullable=False), + sa.Column('type', sa.String(length=50), nullable=False), + sa.Column('file_path', sa.String(length=255), nullable=False), + sa.Column('thumbnail_path', sa.String(length=255), nullable=True), + sa.Column('description', sa.String(length=255), nullable=True), + sa.Column('is_active', sa.Boolean(), nullable=True), + sa.Column('created_at', sa.DateTime(), nullable=True), + sa.Column('updated_at', sa.DateTime(), nullable=True), + sa.ForeignKeyConstraint(['user_id'], ['user.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_table('roles_permissions', + sa.Column('role_id', sa.Integer(), nullable=False), + sa.Column('permission_id', sa.Integer(), nullable=False), + sa.ForeignKeyConstraint(['permission_id'], ['permission.id'], ), + sa.ForeignKeyConstraint(['role_id'], ['role.id'], ), + sa.PrimaryKeyConstraint('role_id', 'permission_id') + ) + op.create_table('roles_users', + sa.Column('user_id', sa.Integer(), nullable=False), + sa.Column('role_id', sa.Integer(), nullable=False), + sa.ForeignKeyConstraint(['role_id'], ['role.id'], ), + sa.ForeignKeyConstraint(['user_id'], ['user.id'], ), + sa.PrimaryKeyConstraint('user_id', 'role_id') + ) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table('roles_users') + op.drop_table('roles_permissions') + op.drop_table('resource') + op.drop_table('log') + op.drop_table('user') + op.drop_table('role') + op.drop_table('permission') + # ### end Alembic commands ### diff --git a/backend/migrations/versions/__pycache__/0016b73ba5fb_initial_migration.cpython-312.pyc b/backend/migrations/versions/__pycache__/0016b73ba5fb_initial_migration.cpython-312.pyc new file mode 100644 index 00000000..20d2e5af Binary files /dev/null and b/backend/migrations/versions/__pycache__/0016b73ba5fb_initial_migration.cpython-312.pyc differ diff --git a/backend/models.py b/backend/models.py new file mode 100644 index 00000000..d886e52a --- /dev/null +++ b/backend/models.py @@ -0,0 +1,158 @@ +from datetime import datetime +from .app import db +from werkzeug.security import generate_password_hash, check_password_hash +from flask_jwt_extended import create_access_token, create_refresh_token + +# 用户角色关联表 +roles_users = db.Table('roles_users', + db.Column('user_id', db.Integer, db.ForeignKey('user.id'), primary_key=True), + db.Column('role_id', db.Integer, db.ForeignKey('role.id'), primary_key=True) +) + +# 角色权限关联表 +roles_permissions = db.Table('roles_permissions', + db.Column('role_id', db.Integer, db.ForeignKey('role.id'), primary_key=True), + db.Column('permission_id', db.Integer, db.ForeignKey('permission.id'), primary_key=True) +) + +class User(db.Model): + id = db.Column(db.Integer, primary_key=True) + username = db.Column(db.String(80), unique=True, nullable=False) + email = db.Column(db.String(120), unique=True, nullable=False) + password_hash = db.Column(db.String(128), nullable=False) + is_verified = db.Column(db.Boolean, default=False) + created_at = db.Column(db.DateTime, default=datetime.utcnow) + updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) + + # 关联角色 + roles = db.relationship('Role', secondary=roles_users, lazy='subquery', + backref=db.backref('users', lazy=True)) + + def __repr__(self): + return f'' + + def set_password(self, password): + self.password_hash = generate_password_hash(password) + + def check_password(self, password): + return check_password_hash(self.password_hash, password) + + def get_access_token(self): + return create_access_token(identity=self.id) + + def get_refresh_token(self): + return create_refresh_token(identity=self.id) + + def has_permission(self, permission_name): + for role in self.roles: + for permission in role.permissions: + if permission.name == permission_name: + return True + return False + +class Role(db.Model): + id = db.Column(db.Integer, primary_key=True) + name = db.Column(db.String(50), unique=True, nullable=False) + description = db.Column(db.String(255)) + created_at = db.Column(db.DateTime, default=datetime.utcnow) + updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) + + # 关联权限 + permissions = db.relationship('Permission', secondary=roles_permissions, lazy='subquery', + backref=db.backref('roles', lazy=True)) + + def __repr__(self): + return f'' + +class Permission(db.Model): + id = db.Column(db.Integer, primary_key=True) + name = db.Column(db.String(50), unique=True, nullable=False) + description = db.Column(db.String(255)) + created_at = db.Column(db.DateTime, default=datetime.utcnow) + + def __repr__(self): + return f'' + +class Log(db.Model): + id = db.Column(db.Integer, primary_key=True) + user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False) + title = db.Column(db.String(100), nullable=False) + content = db.Column(db.Text, nullable=False) + tags = db.Column(db.String(255)) + importance = db.Column(db.Integer, default=1) # 1-5 + created_at = db.Column(db.DateTime, default=datetime.utcnow) + updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) + + # 关联用户 + user = db.relationship('User', backref=db.backref('logs', lazy=True)) + + def __repr__(self): + return f'' + +class Resource(db.Model): + id = db.Column(db.Integer, primary_key=True) + user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False) + name = db.Column(db.String(100), nullable=False) + type = db.Column(db.String(50), nullable=False) # skin, model, etc. + file_path = db.Column(db.String(255), nullable=False) + thumbnail_path = db.Column(db.String(255)) + description = db.Column(db.String(255)) + is_active = db.Column(db.Boolean, default=False) + created_at = db.Column(db.DateTime, default=datetime.utcnow) + updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) + + # 关联用户 + user = db.relationship('User', backref=db.backref('resources', lazy=True)) + + def __repr__(self): + return f'' + +# 创建初始数据 +def create_initial_data(): + # 创建权限 + permissions = [ + ('log_create', 'Create log'), + ('log_read', 'Read log'), + ('log_update', 'Update log'), + ('log_delete', 'Delete log'), + ('resource_upload', 'Upload resource'), + ('resource_read', 'Read resource'), + ('resource_update', 'Update resource'), + ('resource_delete', 'Delete resource'), + ('user_manage', 'Manage user'), + ('role_manage', 'Manage role'), + ] + + for name, description in permissions: + permission = Permission.query.filter_by(name=name).first() + if not permission: + permission = Permission(name=name, description=description) + db.session.add(permission) + + # 创建角色 + admin_role = Role.query.filter_by(name='admin').first() + if not admin_role: + admin_role = Role(name='admin', description='Administrator role') + # 赋予所有权限 + admin_role.permissions = Permission.query.all() + db.session.add(admin_role) + + user_role = Role.query.filter_by(name='user').first() + if not user_role: + user_role = Role(name='user', description='Regular user role') + # 赋予基本权限 + user_role.permissions = Permission.query.filter_by(name.in_([ + 'log_create', 'log_read', 'log_update', 'log_delete', + 'resource_upload', 'resource_read', 'resource_update', 'resource_delete' + ])).all() + db.session.add(user_role) + + # 创建管理员用户 + admin_user = User.query.filter_by(username='admin').first() + if not admin_user: + admin_user = User(username='admin', email='admin@example.com', is_verified=True) + admin_user.set_password('admin123') + admin_user.roles.append(admin_role) + db.session.add(admin_user) + + db.session.commit() \ No newline at end of file diff --git a/backend/requirements.txt b/backend/requirements.txt new file mode 100644 index 00000000..3a7bc3bd --- /dev/null +++ b/backend/requirements.txt @@ -0,0 +1,9 @@ +Flask==2.3.3 +Flask-SQLAlchemy==3.0.5 +Flask-Migrate==4.0.5 +Flask-JWT-Extended==4.5.3 +Flask-CORS==4.0.0 +Werkzeug==2.3.7 +itsdangerous==2.1.2 +Pillow==10.0.0 +python-dotenv==1.0.0 \ No newline at end of file diff --git a/backend/routes/__init__.py b/backend/routes/__init__.py new file mode 100644 index 00000000..ece10e7e --- /dev/null +++ b/backend/routes/__init__.py @@ -0,0 +1,4 @@ +# 导入路由蓝图 +from .auth import bp as auth_bp +from .logs import bp as logs_bp +from .resources import bp as resources_bp \ No newline at end of file diff --git a/backend/routes/__pycache__/__init__.cpython-312.pyc b/backend/routes/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 00000000..1f3c90b9 Binary files /dev/null and b/backend/routes/__pycache__/__init__.cpython-312.pyc differ diff --git a/backend/routes/__pycache__/auth.cpython-312.pyc b/backend/routes/__pycache__/auth.cpython-312.pyc new file mode 100644 index 00000000..2041980e Binary files /dev/null and b/backend/routes/__pycache__/auth.cpython-312.pyc differ diff --git a/backend/routes/__pycache__/logs.cpython-312.pyc b/backend/routes/__pycache__/logs.cpython-312.pyc new file mode 100644 index 00000000..551e1b32 Binary files /dev/null and b/backend/routes/__pycache__/logs.cpython-312.pyc differ diff --git a/backend/routes/__pycache__/resources.cpython-312.pyc b/backend/routes/__pycache__/resources.cpython-312.pyc new file mode 100644 index 00000000..83fbc294 Binary files /dev/null and b/backend/routes/__pycache__/resources.cpython-312.pyc differ diff --git a/backend/routes/auth.py b/backend/routes/auth.py new file mode 100644 index 00000000..855637d3 --- /dev/null +++ b/backend/routes/auth.py @@ -0,0 +1,224 @@ +from flask import Blueprint, request, jsonify +from backend.app import db +from backend.models import User, Role +from flask_jwt_extended import jwt_required, get_jwt_identity +from werkzeug.security import generate_password_hash, check_password_hash +from itsdangerous import URLSafeTimedSerializer +import smtplib +from email.mime.text import MIMEText +from email.mime.multipart import MIMEMultipart +import os + +bp = Blueprint('auth', __name__) + +# 配置邮箱 +MAIL_SERVER = os.environ.get('MAIL_SERVER') or 'smtp.gmail.com' +MAIL_PORT = int(os.environ.get('MAIL_PORT') or 587) +MAIL_USE_TLS = os.environ.get('MAIL_USE_TLS') or True +MAIL_USERNAME = os.environ.get('MAIL_USERNAME') or 'your-email@gmail.com' +MAIL_PASSWORD = os.environ.get('MAIL_PASSWORD') or 'your-email-password' +MAIL_DEFAULT_SENDER = os.environ.get('MAIL_DEFAULT_SENDER') or 'your-email@gmail.com' + +# 创建序列化器 +serializer = URLSafeTimedSerializer(os.environ.get('SECRET_KEY') or 'your-secret-key-here') + +@bp.route('/register', methods=['POST']) +def register(): + data = request.get_json() + + if not data or not data.get('username') or not data.get('email') or not data.get('password'): + return jsonify({'message': 'Missing required fields'}), 400 + + # 检查用户名是否已存在 + if User.query.filter_by(username=data.get('username')).first(): + return jsonify({'message': 'Username already exists'}), 409 + + # 检查邮箱是否已存在 + if User.query.filter_by(email=data.get('email')).first(): + return jsonify({'message': 'Email already exists'}), 409 + + # 创建新用户 + user = User( + username=data.get('username'), + email=data.get('email') + ) + user.set_password(data.get('password')) + + # 赋予默认角色 + default_role = Role.query.filter_by(name='user').first() + if default_role: + user.roles.append(default_role) + + db.session.add(user) + db.session.commit() + + # 发送验证邮件 + send_verification_email(user) + + return jsonify({'message': 'User created successfully. Please check your email to verify your account.'}), 201 + +@bp.route('/verify/', methods=['GET']) +def verify_email(token): + try: + email = serializer.loads(token, salt='email-verification-salt', max_age=3600) # 1小时有效期 + except: + return jsonify({'message': 'Invalid or expired token'}), 400 + + user = User.query.filter_by(email=email).first() + if not user: + return jsonify({'message': 'User not found'}), 404 + + if user.is_verified: + return jsonify({'message': 'Email already verified'}), 400 + + user.is_verified = True + db.session.commit() + + return jsonify({'message': 'Email verified successfully'}), 200 + +@bp.route('/login', methods=['POST']) +def login(): + data = request.get_json() + + if not data or not data.get('email') or not data.get('password'): + return jsonify({'message': 'Missing required fields'}), 400 + + user = User.query.filter_by(email=data.get('email')).first() + if not user or not user.check_password(data.get('password')): + return jsonify({'message': 'Invalid email or password'}), 401 + + if not user.is_verified: + return jsonify({'message': 'Please verify your email first'}), 403 + + # 创建访问令牌和刷新令牌 + access_token = user.get_access_token() + refresh_token = user.get_refresh_token() + + return jsonify({ + 'access_token': access_token, + 'refresh_token': refresh_token, + 'user': { + 'id': user.id, + 'username': user.username, + 'email': user.email, + 'roles': [role.name for role in user.roles] + } + }), 200 + +@bp.route('/refresh', methods=['POST']) +@jwt_required(refresh=True) +def refresh(): + current_user_id = get_jwt_identity() + user = User.query.get(current_user_id) + + if not user: + return jsonify({'message': 'User not found'}), 404 + + access_token = user.get_access_token() + + return jsonify({'access_token': access_token}), 200 + +@bp.route('/forgot-password', methods=['POST']) +def forgot_password(): + data = request.get_json() + + if not data or not data.get('email'): + return jsonify({'message': 'Missing required fields'}), 400 + + user = User.query.filter_by(email=data.get('email')).first() + if not user: + return jsonify({'message': 'User not found'}), 404 + + # 发送重置密码邮件 + send_password_reset_email(user) + + return jsonify({'message': 'Password reset email sent. Please check your email.'}), 200 + +@bp.route('/reset-password/', methods=['POST']) +def reset_password(token): + try: + email = serializer.loads(token, salt='password-reset-salt', max_age=3600) # 1小时有效期 + except: + return jsonify({'message': 'Invalid or expired token'}), 400 + + user = User.query.filter_by(email=email).first() + if not user: + return jsonify({'message': 'User not found'}), 404 + + data = request.get_json() + if not data or not data.get('password') or not data.get('confirm_password'): + return jsonify({'message': 'Missing required fields'}), 400 + + if data.get('password') != data.get('confirm_password'): + return jsonify({'message': 'Passwords do not match'}), 400 + + user.set_password(data.get('password')) + db.session.commit() + + return jsonify({'message': 'Password reset successfully'}), 200 + +@bp.route('/me', methods=['GET']) +@jwt_required() +def get_current_user(): + current_user_id = get_jwt_identity() + user = User.query.get(current_user_id) + + if not user: + return jsonify({'message': 'User not found'}), 404 + + return jsonify({ + 'id': user.id, + 'username': user.username, + 'email': user.email, + 'is_verified': user.is_verified, + 'roles': [role.name for role in user.roles], + 'created_at': user.created_at, + 'updated_at': user.updated_at + }), 200 + +# 发送验证邮件 +def send_verification_email(user): + token = serializer.dumps(user.email, salt='email-verification-salt') + verification_url = f'http://localhost:5000/api/auth/verify/{token}' + + subject = 'Verify your email for Plants vs Zombies' + body = f'Hi {user.username},\n\nPlease click the link below to verify your email:\n{verification_url}\n\nThis link will expire in 1 hour.\n\nThanks,\nThe Plants vs Zombies Team' + + send_email(user.email, subject, body) + +# 发送重置密码邮件 +def send_password_reset_email(user): + token = serializer.dumps(user.email, salt='password-reset-salt') + reset_url = f'http://localhost:5000/api/auth/reset-password/{token}' + + subject = 'Reset your password for Plants vs Zombies' + body = f"Hi {user.username},\n\nYou requested a password reset. Please click the link below to reset your password:\n{reset_url}\n\nThis link will expire in 1 hour.\n\nIf you didn't request this, you can safely ignore this email.\n\nThanks,\nThe Plants vs Zombies Team" + + send_email(user.email, subject, body) + +# 发送邮件 +def send_email(to, subject, body): + try: + # 创建邮件消息 + msg = MIMEMultipart() + msg['From'] = MAIL_DEFAULT_SENDER + msg['To'] = to + msg['Subject'] = subject + + # 添加邮件正文 + msg.attach(MIMEText(body, 'plain')) + + # 连接到SMTP服务器 + server = smtplib.SMTP(MAIL_SERVER, MAIL_PORT) + server.starttls() + server.login(MAIL_USERNAME, MAIL_PASSWORD) + + # 发送邮件 + server.send_message(msg) + + # 关闭连接 + server.quit() + except Exception as e: + print(f'Error sending email: {str(e)}') + # 在这里可以添加日志记录 + pass \ No newline at end of file diff --git a/backend/routes/logs.py b/backend/routes/logs.py new file mode 100644 index 00000000..11ff54ab --- /dev/null +++ b/backend/routes/logs.py @@ -0,0 +1,221 @@ +from flask import Blueprint, request, jsonify +from backend.app import db +from backend.models import Log, User +from flask_jwt_extended import jwt_required, get_jwt_identity +from datetime import datetime + +bp = Blueprint('logs', __name__) + +@bp.route('/logs', methods=['POST']) +@jwt_required() +def create_log(): + current_user_id = get_jwt_identity() + user = User.query.get(current_user_id) + + if not user: + return jsonify({'message': 'User not found'}), 404 + + if not user.has_permission('log_create'): + return jsonify({'message': 'Permission denied'}), 403 + + data = request.get_json() + + if not data or not data.get('title') or not data.get('content'): + return jsonify({'message': 'Missing required fields'}), 400 + + # 创建新日志 + log = Log( + user_id=current_user_id, + title=data.get('title'), + content=data.get('content'), + tags=data.get('tags'), + importance=data.get('importance', 1) + ) + + db.session.add(log) + db.session.commit() + + return jsonify({ + 'message': 'Log created successfully', + 'log': { + 'id': log.id, + 'title': log.title, + 'content': log.content, + 'tags': log.tags, + 'importance': log.importance, + 'created_at': log.created_at, + 'updated_at': log.updated_at + } + }), 201 + +@bp.route('/logs', methods=['GET']) +@jwt_required() +def get_logs(): + current_user_id = get_jwt_identity() + user = User.query.get(current_user_id) + + if not user: + return jsonify({'message': 'User not found'}), 404 + + if not user.has_permission('log_read'): + return jsonify({'message': 'Permission denied'}), 403 + + # 获取查询参数 + page = request.args.get('page', 1, type=int) + per_page = request.args.get('per_page', 10, type=int) + sort_by = request.args.get('sort_by', 'created_at') + sort_order = request.args.get('sort_order', 'desc') + tags = request.args.get('tags') + importance = request.args.get('importance', type=int) + start_date = request.args.get('start_date') + end_date = request.args.get('end_date') + + # 构建查询 + query = Log.query.filter_by(user_id=current_user_id) + + # 过滤标签 + if tags: + tag_list = tags.split(',') + for tag in tag_list: + query = query.filter(Log.tags.like(f'%{tag}%')) + + # 过滤重要程度 + if importance: + query = query.filter_by(importance=importance) + + # 过滤日期范围 + if start_date: + try: + start = datetime.strptime(start_date, '%Y-%m-%d') + query = query.filter(Log.created_at >= start) + except ValueError: + return jsonify({'message': 'Invalid start date format. Use YYYY-MM-DD'}), 400 + + if end_date: + try: + end = datetime.strptime(end_date, '%Y-%m-%d') + query = query.filter(Log.created_at <= end) + except ValueError: + return jsonify({'message': 'Invalid end date format. Use YYYY-MM-DD'}), 400 + + # 排序 + if sort_order == 'desc': + query = query.order_by(getattr(Log, sort_by).desc()) + else: + query = query.order_by(getattr(Log, sort_by).asc()) + + # 分页 + paginated_logs = query.paginate(page=page, per_page=per_page, error_out=False) + + # 格式化结果 + logs = [] + for log in paginated_logs.items: + logs.append({ + 'id': log.id, + 'title': log.title, + 'content': log.content, + 'tags': log.tags, + 'importance': log.importance, + 'created_at': log.created_at, + 'updated_at': log.updated_at + }) + + return jsonify({ + 'logs': logs, + 'total': paginated_logs.total, + 'pages': paginated_logs.pages, + 'current_page': paginated_logs.page + }), 200 + +@bp.route('/logs/', methods=['GET']) +@jwt_required() +def get_log(log_id): + current_user_id = get_jwt_identity() + user = User.query.get(current_user_id) + + if not user: + return jsonify({'message': 'User not found'}), 404 + + if not user.has_permission('log_read'): + return jsonify({'message': 'Permission denied'}), 403 + + log = Log.query.filter_by(id=log_id, user_id=current_user_id).first() + + if not log: + return jsonify({'message': 'Log not found'}), 404 + + return jsonify({ + 'id': log.id, + 'title': log.title, + 'content': log.content, + 'tags': log.tags, + 'importance': log.importance, + 'created_at': log.created_at, + 'updated_at': log.updated_at + }), 200 + +@bp.route('/logs/', methods=['PUT']) +@jwt_required() +def update_log(log_id): + current_user_id = get_jwt_identity() + user = User.query.get(current_user_id) + + if not user: + return jsonify({'message': 'User not found'}), 404 + + if not user.has_permission('log_update'): + return jsonify({'message': 'Permission denied'}), 403 + + log = Log.query.filter_by(id=log_id, user_id=current_user_id).first() + + if not log: + return jsonify({'message': 'Log not found'}), 404 + + data = request.get_json() + + # 更新日志信息 + if data.get('title'): + log.title = data.get('title') + if data.get('content'): + log.content = data.get('content') + if data.get('tags') is not None: + log.tags = data.get('tags') + if data.get('importance') is not None: + log.importance = data.get('importance') + + db.session.commit() + + return jsonify({ + 'message': 'Log updated successfully', + 'log': { + 'id': log.id, + 'title': log.title, + 'content': log.content, + 'tags': log.tags, + 'importance': log.importance, + 'created_at': log.created_at, + 'updated_at': log.updated_at + } + }), 200 + +@bp.route('/logs/', methods=['DELETE']) +@jwt_required() +def delete_log(log_id): + current_user_id = get_jwt_identity() + user = User.query.get(current_user_id) + + if not user: + return jsonify({'message': 'User not found'}), 404 + + if not user.has_permission('log_delete'): + return jsonify({'message': 'Permission denied'}), 403 + + log = Log.query.filter_by(id=log_id, user_id=current_user_id).first() + + if not log: + return jsonify({'message': 'Log not found'}), 404 + + db.session.delete(log) + db.session.commit() + + return jsonify({'message': 'Log deleted successfully'}), 200 \ No newline at end of file diff --git a/backend/routes/resources.py b/backend/routes/resources.py new file mode 100644 index 00000000..ac0e0013 --- /dev/null +++ b/backend/routes/resources.py @@ -0,0 +1,330 @@ +from flask import Blueprint, request, jsonify, send_from_directory +from backend.app import db +from backend.models import Resource, User +from flask_jwt_extended import jwt_required, get_jwt_identity +import os +from werkzeug.utils import secure_filename +from PIL import Image + +bp = Blueprint('resources', __name__) + +# 配置上传设置 +UPLOAD_FOLDER = os.path.join(os.path.abspath(os.path.dirname(__file__)), '..', 'uploads') +THUMBNAIL_FOLDER = os.path.join(UPLOAD_FOLDER, 'thumbnails') +ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif', 'bmp', 'obj', 'fbx', 'glb', 'gltf'} +MAX_CONTENT_LENGTH = 100 * 1024 * 1024 # 100MB + +# 创建上传目录 +if not os.path.exists(UPLOAD_FOLDER): + os.makedirs(UPLOAD_FOLDER) +if not os.path.exists(THUMBNAIL_FOLDER): + os.makedirs(THUMBNAIL_FOLDER) + +# 检查文件扩展名是否允许 +def allowed_file(filename): + return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS + +# 生成缩略图 +def generate_thumbnail(image_path, thumbnail_path, size=(150, 150)): + try: + with Image.open(image_path) as img: + img.thumbnail(size) + img.save(thumbnail_path) + return True + except Exception as e: + print(f'Error generating thumbnail: {str(e)}') + return False + +@bp.route('/resources', methods=['POST']) +@jwt_required() +def upload_resource(): + current_user_id = get_jwt_identity() + user = User.query.get(current_user_id) + + if not user: + return jsonify({'message': 'User not found'}), 404 + + if not user.has_permission('resource_upload'): + return jsonify({'message': 'Permission denied'}), 403 + + # 检查请求中是否包含文件部分 + if 'file' not in request.files: + return jsonify({'message': 'No file part'}), 400 + + file = request.files['file'] + + # 如果用户没有选择文件,浏览器会提交一个空文件 + if file.filename == '': + return jsonify({'message': 'No selected file'}), 400 + + # 检查文件类型 + if not allowed_file(file.filename): + return jsonify({'message': 'File type not allowed'}), 400 + + # 获取其他表单数据 + name = request.form.get('name', file.filename.rsplit('.', 1)[0]) + type = request.form.get('type', 'other') + description = request.form.get('description', '') + + # 安全地保存文件名 + filename = secure_filename(file.filename) + + # 为每个用户创建单独的目录 + user_upload_folder = os.path.join(UPLOAD_FOLDER, str(current_user_id)) + if not os.path.exists(user_upload_folder): + os.makedirs(user_upload_folder) + + # 保存文件 + file_path = os.path.join(user_upload_folder, filename) + file.save(file_path) + + # 生成缩略图(仅对图片文件) + thumbnail_path = None + if filename.rsplit('.', 1)[1].lower() in {'png', 'jpg', 'jpeg', 'gif', 'bmp'}: + thumbnail_filename = f'thumbnail_{filename}' + thumbnail_path = os.path.join(THUMBNAIL_FOLDER, str(current_user_id), thumbnail_filename) + + # 为每个用户创建单独的缩略图目录 + user_thumbnail_folder = os.path.join(THUMBNAIL_FOLDER, str(current_user_id)) + if not os.path.exists(user_thumbnail_folder): + os.makedirs(user_thumbnail_folder) + + # 生成缩略图 + generate_thumbnail(file_path, thumbnail_path) + + # 创建资源记录 + resource = Resource( + user_id=current_user_id, + name=name, + type=type, + file_path=file_path, + thumbnail_path=thumbnail_path, + description=description + ) + + db.session.add(resource) + db.session.commit() + + return jsonify({ + 'message': 'Resource uploaded successfully', + 'resource': { + 'id': resource.id, + 'name': resource.name, + 'type': resource.type, + 'description': resource.description, + 'is_active': resource.is_active, + 'created_at': resource.created_at, + 'updated_at': resource.updated_at + } + }), 201 + +@bp.route('/resources', methods=['GET']) +@jwt_required() +def get_resources(): + current_user_id = get_jwt_identity() + user = User.query.get(current_user_id) + + if not user: + return jsonify({'message': 'User not found'}), 404 + + if not user.has_permission('resource_read'): + return jsonify({'message': 'Permission denied'}), 403 + + # 获取查询参数 + page = request.args.get('page', 1, type=int) + per_page = request.args.get('per_page', 10, type=int) + sort_by = request.args.get('sort_by', 'created_at') + sort_order = request.args.get('sort_order', 'desc') + type = request.args.get('type') + is_active = request.args.get('is_active', type=bool) + + # 构建查询 + query = Resource.query.filter_by(user_id=current_user_id) + + # 过滤类型 + if type: + query = query.filter_by(type=type) + + # 过滤激活状态 + if is_active is not None: + query = query.filter_by(is_active=is_active) + + # 排序 + if sort_order == 'desc': + query = query.order_by(getattr(Resource, sort_by).desc()) + else: + query = query.order_by(getattr(Resource, sort_by).asc()) + + # 分页 + paginated_resources = query.paginate(page=page, per_page=per_page, error_out=False) + + # 格式化结果 + resources = [] + for resource in paginated_resources.items: + resources.append({ + 'id': resource.id, + 'name': resource.name, + 'type': resource.type, + 'description': resource.description, + 'is_active': resource.is_active, + 'created_at': resource.created_at, + 'updated_at': resource.updated_at + }) + + return jsonify({ + 'resources': resources, + 'total': paginated_resources.total, + 'pages': paginated_resources.pages, + 'current_page': paginated_resources.page + }), 200 + +@bp.route('/resources/', methods=['GET']) +@jwt_required() +def get_resource(resource_id): + current_user_id = get_jwt_identity() + user = User.query.get(current_user_id) + + if not user: + return jsonify({'message': 'User not found'}), 404 + + if not user.has_permission('resource_read'): + return jsonify({'message': 'Permission denied'}), 403 + + resource = Resource.query.filter_by(id=resource_id, user_id=current_user_id).first() + + if not resource: + return jsonify({'message': 'Resource not found'}), 404 + + return jsonify({ + 'id': resource.id, + 'name': resource.name, + 'type': resource.type, + 'description': resource.description, + 'is_active': resource.is_active, + 'created_at': resource.created_at, + 'updated_at': resource.updated_at + }), 200 + +@bp.route('/resources/', methods=['PUT']) +@jwt_required() +def update_resource(resource_id): + current_user_id = get_jwt_identity() + user = User.query.get(current_user_id) + + if not user: + return jsonify({'message': 'User not found'}), 404 + + if not user.has_permission('resource_update'): + return jsonify({'message': 'Permission denied'}), 403 + + resource = Resource.query.filter_by(id=resource_id, user_id=current_user_id).first() + + if not resource: + return jsonify({'message': 'Resource not found'}), 404 + + data = request.form + + # 更新资源信息 + if data.get('name'): + resource.name = data.get('name') + if data.get('type'): + resource.type = data.get('type') + if data.get('description') is not None: + resource.description = data.get('description') + if data.get('is_active') is not None: + resource.is_active = data.get('is_active') == 'true' or data.get('is_active') == True + + # 检查是否有新文件上传 + if 'file' in request.files: + file = request.files['file'] + if file.filename != '' and allowed_file(file.filename): + # 删除旧文件 + if os.path.exists(resource.file_path): + os.remove(resource.file_path) + + # 删除旧缩略图 + if resource.thumbnail_path and os.path.exists(resource.thumbnail_path): + os.remove(resource.thumbnail_path) + + # 保存新文件 + filename = secure_filename(file.filename) + user_upload_folder = os.path.join(UPLOAD_FOLDER, str(current_user_id)) + file_path = os.path.join(user_upload_folder, filename) + file.save(file_path) + resource.file_path = file_path + + # 生成新的缩略图(仅对图片文件) + if filename.rsplit('.', 1)[1].lower() in {'png', 'jpg', 'jpeg', 'gif', 'bmp'}: + thumbnail_filename = f'thumbnail_{filename}' + thumbnail_path = os.path.join(THUMBNAIL_FOLDER, str(current_user_id), thumbnail_filename) + generate_thumbnail(file_path, thumbnail_path) + resource.thumbnail_path = thumbnail_path + else: + resource.thumbnail_path = None + + db.session.commit() + + return jsonify({ + 'message': 'Resource updated successfully', + 'resource': { + 'id': resource.id, + 'name': resource.name, + 'type': resource.type, + 'description': resource.description, + 'is_active': resource.is_active, + 'created_at': resource.created_at, + 'updated_at': resource.updated_at + } + }), 200 + +@bp.route('/resources/', methods=['DELETE']) +@jwt_required() +def delete_resource(resource_id): + current_user_id = get_jwt_identity() + user = User.query.get(current_user_id) + + if not user: + return jsonify({'message': 'User not found'}), 404 + + if not user.has_permission('resource_delete'): + return jsonify({'message': 'Permission denied'}), 403 + + resource = Resource.query.filter_by(id=resource_id, user_id=current_user_id).first() + + if not resource: + return jsonify({'message': 'Resource not found'}), 404 + + # 删除文件 + if os.path.exists(resource.file_path): + os.remove(resource.file_path) + + # 删除缩略图 + if resource.thumbnail_path and os.path.exists(resource.thumbnail_path): + os.remove(resource.thumbnail_path) + + db.session.delete(resource) + db.session.commit() + + return jsonify({'message': 'Resource deleted successfully'}), 200 + +@bp.route('/resources//download', methods=['GET']) +@jwt_required() +def download_resource(resource_id): + current_user_id = get_jwt_identity() + user = User.query.get(current_user_id) + + if not user: + return jsonify({'message': 'User not found'}), 404 + + if not user.has_permission('resource_read'): + return jsonify({'message': 'Permission denied'}), 403 + + resource = Resource.query.filter_by(id=resource_id, user_id=current_user_id).first() + + if not resource: + return jsonify({'message': 'Resource not found'}), 404 + + if not os.path.exists(resource.file_path): + return jsonify({'message': 'File not found'}), 404 + + return send_from_directory(os.path.dirname(resource.file_path), os.path.basename(resource.file_path), as_attachment=True) \ No newline at end of file diff --git a/ke b/ke new file mode 100644 index 00000000..e69de29b diff --git a/source/__pycache__/__init__.cpython-312.pyc b/source/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 00000000..73ec3c53 Binary files /dev/null and b/source/__pycache__/__init__.cpython-312.pyc differ diff --git a/source/__pycache__/constants.cpython-312.pyc b/source/__pycache__/constants.cpython-312.pyc new file mode 100644 index 00000000..6fcfa450 Binary files /dev/null and b/source/__pycache__/constants.cpython-312.pyc differ diff --git a/source/__pycache__/main.cpython-312.pyc b/source/__pycache__/main.cpython-312.pyc new file mode 100644 index 00000000..b84b6c22 Binary files /dev/null and b/source/__pycache__/main.cpython-312.pyc differ diff --git a/source/__pycache__/tool.cpython-312.pyc b/source/__pycache__/tool.cpython-312.pyc new file mode 100644 index 00000000..69e5afe0 Binary files /dev/null and b/source/__pycache__/tool.cpython-312.pyc differ diff --git a/source/component/__pycache__/__init__.cpython-312.pyc b/source/component/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 00000000..1f9988a3 Binary files /dev/null and b/source/component/__pycache__/__init__.cpython-312.pyc differ diff --git a/source/component/__pycache__/map.cpython-312.pyc b/source/component/__pycache__/map.cpython-312.pyc new file mode 100644 index 00000000..92b1ac8f Binary files /dev/null and b/source/component/__pycache__/map.cpython-312.pyc differ diff --git a/source/component/__pycache__/menubar.cpython-312.pyc b/source/component/__pycache__/menubar.cpython-312.pyc new file mode 100644 index 00000000..51a487c5 Binary files /dev/null and b/source/component/__pycache__/menubar.cpython-312.pyc differ diff --git a/source/component/__pycache__/plant.cpython-312.pyc b/source/component/__pycache__/plant.cpython-312.pyc new file mode 100644 index 00000000..154eaabf Binary files /dev/null and b/source/component/__pycache__/plant.cpython-312.pyc differ diff --git a/source/component/__pycache__/zombie.cpython-312.pyc b/source/component/__pycache__/zombie.cpython-312.pyc new file mode 100644 index 00000000..73ff2b1c Binary files /dev/null and b/source/component/__pycache__/zombie.cpython-312.pyc differ diff --git a/source/state/__pycache__/__init__.cpython-312.pyc b/source/state/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 00000000..58f7eae1 Binary files /dev/null and b/source/state/__pycache__/__init__.cpython-312.pyc differ diff --git a/source/state/__pycache__/level.cpython-312.pyc b/source/state/__pycache__/level.cpython-312.pyc new file mode 100644 index 00000000..2e9dd927 Binary files /dev/null and b/source/state/__pycache__/level.cpython-312.pyc differ diff --git a/source/state/__pycache__/mainmenu.cpython-312.pyc b/source/state/__pycache__/mainmenu.cpython-312.pyc new file mode 100644 index 00000000..a64d3d7a Binary files /dev/null and b/source/state/__pycache__/mainmenu.cpython-312.pyc differ diff --git a/source/state/__pycache__/screen.cpython-312.pyc b/source/state/__pycache__/screen.cpython-312.pyc new file mode 100644 index 00000000..ce3bd80d Binary files /dev/null and b/source/state/__pycache__/screen.cpython-312.pyc differ