diff --git a/.devcontainer/.env b/.devcontainer/.env deleted file mode 100644 index a86b76c..0000000 --- a/.devcontainer/.env +++ /dev/null @@ -1,4 +0,0 @@ -DATABASE_HOST=mariadb -DATABASE_USER=root -DATABASE_PASSWORD=gaseous -DATABASE_DB=gaseous \ No newline at end of file diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 02815be..c0d8d84 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -2,5 +2,5 @@ FROM mcr.microsoft.com/devcontainers/dotnet:1-8.0-bookworm RUN apt-get update && apt-get install -y p7zip-full RUN mkdir -p /workspace/gaseous-server/wwwroot/emulators/EmulatorJS -RUN wget https://cdn.emulatorjs.org/releases/4.0.11.7z -RUN 7z x -y -o/workspace/gaseous-server/wwwroot/emulators/EmulatorJS 4.0.11.7z \ No newline at end of file +RUN wget https://cdn.emulatorjs.org/releases/4.0.12.7z +RUN 7z x -y -o/workspace/gaseous-server/wwwroot/emulators/EmulatorJS 4.0.12.7z \ No newline at end of file diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 2607853..4af9b06 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -36,7 +36,9 @@ "AndersEAndersen.html-class-suggestions", "george-alisson.html-preview-vscode", "ms-dotnettools.vscodeintellicode-csharp", - "Zignd.html-css-class-completion" + "Zignd.html-css-class-completion", + "PWABuilder.pwa-studio", + "ms-azuretools.vscode-docker" ] } } diff --git a/.devcontainer/docker-compose.yml b/.devcontainer/docker-compose.yml index 30d0972..95795eb 100644 --- a/.devcontainer/docker-compose.yml +++ b/.devcontainer/docker-compose.yml @@ -1,6 +1,6 @@ services: development: - build: + build: context: . dockerfile: Dockerfile volumes: @@ -11,13 +11,15 @@ services: - dbhost=${DATABASE_HOST} - dbuser=${DATABASE_USER} - dbpass=${DATABASE_PASSWORD} - - igdbclientid= - - igdbclientsecret= + - igdbclientid=${IGDB_CLIENT_ID} + - igdbclientsecret=${IGDB_CLIENT_SECRET} mariadb: hostname: mariadb image: mariadb:latest + ports: + - 3306:3306 environment: - MARIADB_ROOT_PASSWORD=${DATABASE_PASSWORD} - MARIADB_DATABASE=${DATABASE_DB} - MARIADB_USER=${DATABASE_USER} - - MARIADB_PASSWORD=${DATABASE_PASSWORD} \ No newline at end of file + - MARIADB_PASSWORD=${DATABASE_PASSWORD} diff --git a/.github/workflows/BuildDockerOnTag-Prerelease.yml b/.github/workflows/BuildDockerOnTag-Prerelease.yml index 9c1ba62..2c27b1f 100644 --- a/.github/workflows/BuildDockerOnTag-Prerelease.yml +++ b/.github/workflows/BuildDockerOnTag-Prerelease.yml @@ -9,9 +9,14 @@ on: jobs: docker: runs-on: ubuntu-latest + permissions: + packages: write + contents: read + attestations: write + id-token: write steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: submodules: 'true' - name: Install dotnet tool @@ -21,18 +26,37 @@ jobs: - name: Sign in to Nuget run: dotnet nuget add source --username michael-j-green --password ${{ secrets.NUGETKEY }} --store-password-in-clear-text --name github "https://nuget.pkg.github.com/gaseous-project/index.json" - name: Set up QEMU - uses: docker/setup-qemu-action@v2 + uses: docker/setup-qemu-action@v3 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 + uses: docker/setup-buildx-action@v3 - name: Login to Docker Hub - uses: docker/login-action@v2 + uses: docker/login-action@v3 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - - name: Build and push - uses: docker/build-push-action@v4 + - name: Login to GitHub Package Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + - name: Build and push standard image + uses: docker/build-push-action@v6 with: context: . + file: ./build/Dockerfile platforms: linux/amd64,linux/arm64 push: true - tags: gaseousgames/gaseousserver:${{ github.ref_name}} + tags: | + gaseousgames/gaseousserver:${{ github.ref_name}} + ghcr.io/gaseous-project/gaseousserver:${{ github.ref_name}} + - name: Build and push image with embedded mariadb + uses: docker/build-push-action@v6 + with: + context: . + file: ./build/Dockerfile-EmbeddedDB + platforms: linux/amd64,linux/arm64 + push: true + tags: | + gaseousgames/gaseousserver:${{ github.ref_name}}-embeddeddb + ghcr.io/gaseous-project/gaseousserver:${{ github.ref_name}}-embeddeddb \ No newline at end of file diff --git a/.github/workflows/BuildDockerOnTag-Release.yml b/.github/workflows/BuildDockerOnTag-Release.yml index ffd41e2..346ccf7 100644 --- a/.github/workflows/BuildDockerOnTag-Release.yml +++ b/.github/workflows/BuildDockerOnTag-Release.yml @@ -8,9 +8,14 @@ on: jobs: docker: runs-on: ubuntu-latest + permissions: + packages: write + contents: read + attestations: write + id-token: write steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: submodules: 'true' - name: Install dotnet tool @@ -20,18 +25,39 @@ jobs: - name: Sign in to Nuget run: dotnet nuget add source --username michael-j-green --password ${{ secrets.NUGETKEY }} --store-password-in-clear-text --name github "https://nuget.pkg.github.com/gaseous-project/index.json" - name: Set up QEMU - uses: docker/setup-qemu-action@v2 + uses: docker/setup-qemu-action@v3 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 + uses: docker/setup-buildx-action@v3 - name: Login to Docker Hub - uses: docker/login-action@v2 + uses: docker/login-action@v3 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - - name: Build and push - uses: docker/build-push-action@v4 + - name: Login to GitHub Package Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + - name: Build and push standard image + uses: docker/build-push-action@v6 with: context: . + file: ./build/Dockerfile platforms: linux/amd64,linux/arm64 push: true - tags: gaseousgames/gaseousserver:latest,gaseousgames/gaseousserver:${{ github.ref_name}} + tags: | + gaseousgames/gaseousserver:latest + gaseousgames/gaseousserver:${{ github.ref_name}} + ghcr.io/gaseous-project/gaseousserver:latest + ghcr.io/gaseous-project/gaseousserver:${{ github.ref_name}} + - name: Build and push image with embedded mariadb + uses: docker/build-push-action@v6 + with: + context: . + file: ./build/Dockerfile-EmbeddedDB + platforms: linux/amd64,linux/arm64 + push: true + tags: | + gaseousgames/gaseousserver:${{ github.ref_name}}-embeddeddb + ghcr.io/gaseous-project/gaseousserver:${{ github.ref_name}}-embeddeddb \ No newline at end of file diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index c78760b..0000000 --- a/Dockerfile +++ /dev/null @@ -1,28 +0,0 @@ -FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:8.0 AS build-env -ARG TARGETARCH -ARG BUILDPLATFORM -WORKDIR /App -EXPOSE 80 - -RUN echo "Target: $TARGETARCH" -RUN echo "Build: $BUILDPLATFORM" - -# Copy everything -COPY . ./ -# Restore as distinct layers -RUN dotnet restore "gaseous-server/gaseous-server.csproj" -a $TARGETARCH -# Build and publish a release -RUN dotnet publish "gaseous-server/gaseous-server.csproj" --use-current-runtime --self-contained true -c Release -o out -a $TARGETARCH - -# download and unzip EmulatorJS from CDN -RUN apt-get update && apt-get install -y p7zip-full -RUN mkdir -p out/wwwroot/emulators/EmulatorJS -RUN wget https://cdn.emulatorjs.org/releases/4.0.12.7z -RUN 7z x -y -oout/wwwroot/emulators/EmulatorJS 4.0.12.7z - -# Build runtime image -FROM mcr.microsoft.com/dotnet/aspnet:8.0 -ENV INDOCKER=1 -WORKDIR /App -COPY --from=build-env /App/out . -ENTRYPOINT ["dotnet", "gaseous-server.dll"] diff --git a/README.MD b/README.MD index 112cb08..4cb28a9 100644 --- a/README.MD +++ b/README.MD @@ -16,9 +16,9 @@ Version 1.7.0 and later contain user authentication, and can be exposed to the i While we do our best to stay on top of server security, if you expose the server to the internet **you do so at your own risk**. ## Screenshots -![Library](./screenshots/Library.png) -![Game](./screenshots/Game.png) -![Emulator](./screenshots/Emulator.png) +![Library](./gaseous-server/wwwroot/screenshots/Library.png) +![Game](./gaseous-server/wwwroot/screenshots/Game.png) +![Emulator](./gaseous-server/wwwroot/screenshots/Emulator.png) ## Requirements diff --git a/build/Dockerfile b/build/Dockerfile new file mode 100644 index 0000000..8132a75 --- /dev/null +++ b/build/Dockerfile @@ -0,0 +1,41 @@ +FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:8.0 AS build-env +ARG TARGETARCH +ARG BUILDPLATFORM +WORKDIR /App +EXPOSE 80 + +RUN echo "Target: $TARGETARCH" +RUN echo "Build: $BUILDPLATFORM" + +# Copy everything +COPY .. ./ +# Restore as distinct layers +RUN dotnet restore "gaseous-server/gaseous-server.csproj" -a $TARGETARCH +# Build and publish a release +RUN dotnet publish "gaseous-server/gaseous-server.csproj" --use-current-runtime --self-contained true -c Release -o out -a $TARGETARCH + +# disabled for 1.7.4 as the next version EmulatorJS is not yet available +# # update apt-get +# RUN apt-get update + +# # download and unzip EmulatorJS from CDN +# RUN apt-get install -y p7zip-full +# RUN mkdir -p out/wwwroot/emulators/EmulatorJS +# RUN wget https://cdn.emulatorjs.org/releases/4.0.12.7z +# RUN 7z x -y -oout/wwwroot/emulators/EmulatorJS 4.0.12.7z +RUN wget --recursive --no-parent https://cdn.emulatorjs.org/latest/ +RUN mkdir -p out/wwwroot/emulators/EmulatorJS +RUN cp -fr cdn.emulatorjs.org/latest/* out/wwwroot/emulators/EmulatorJS +RUN rm -Rf cdn.emulatorjs.org + +# clean up apt-get +RUN apt-get clean && rm -rf /var/lib/apt/lists + +# Build runtime image +FROM mcr.microsoft.com/dotnet/aspnet:8.0 +ENV INDOCKER=1 +WORKDIR /App +COPY --from=build-env /App/out . + +# start gaseous-server +ENTRYPOINT ["dotnet", "gaseous-server.dll"] \ No newline at end of file diff --git a/build/Dockerfile-EmbeddedDB b/build/Dockerfile-EmbeddedDB new file mode 100644 index 0000000..61362cf --- /dev/null +++ b/build/Dockerfile-EmbeddedDB @@ -0,0 +1,61 @@ +FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:8.0 AS build-env +ARG TARGETARCH +ARG BUILDPLATFORM +WORKDIR /App +EXPOSE 80 + +RUN echo "Target: $TARGETARCH" +RUN echo "Build: $BUILDPLATFORM" + +# Copy everything +COPY .. ./ +# Restore as distinct layers +RUN dotnet restore "gaseous-server/gaseous-server.csproj" -a $TARGETARCH +# Build and publish a release +RUN dotnet publish "gaseous-server/gaseous-server.csproj" --use-current-runtime --self-contained true -c Release -o out -a $TARGETARCH + +# disabled for 1.7.4 as the next version EmulatorJS is not yet available +# # update apt-get +# RUN apt-get update + +# # download and unzip EmulatorJS from CDN +# RUN apt-get install -y p7zip-full +# RUN mkdir -p out/wwwroot/emulators/EmulatorJS +# RUN wget https://cdn.emulatorjs.org/releases/4.0.12.7z +# RUN 7z x -y -oout/wwwroot/emulators/EmulatorJS 4.0.12.7z +RUN wget --recursive --no-parent https://cdn.emulatorjs.org/latest/ +RUN mkdir -p out/wwwroot/emulators/EmulatorJS +RUN cp -fr cdn.emulatorjs.org/latest/* out/wwwroot/emulators/EmulatorJS +RUN rm -Rf cdn.emulatorjs.org + +# Build runtime image +FROM mcr.microsoft.com/dotnet/aspnet:8.0 +ENV INDOCKER=1 +WORKDIR /App +COPY --from=build-env /App/out . + +# variables +ENV dbhost=localhost +ENV dbuser=root +ENV dbpass=gaseous +ENV MARIADB_ROOT_PASSWORD=$dbpass + +# install mariadb +RUN DEBIAN_FRONTEND=noninteractive && \ + apt-get update && apt-get install -y mariadb-server +RUN mkdir -p /run/mysqld +COPY ../build/mariadb.sh /usr/sbin/start-mariadb.sh +RUN chmod +x /usr/sbin/start-mariadb.sh + +# install supervisord +RUN apt-get install -y supervisor +COPY ../build/supervisord.conf /etc/supervisor/conf.d/supervisord.conf + +# clean up apt-get +RUN apt-get clean && rm -rf /var/lib/apt/lists + +# volumes +VOLUME /root/.gaseous-server /var/lib/mysql + +# start services +CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/conf.d/supervisord.conf"] \ No newline at end of file diff --git a/build/mariadb.sh b/build/mariadb.sh new file mode 100644 index 0000000..9851661 --- /dev/null +++ b/build/mariadb.sh @@ -0,0 +1,9 @@ +#!/bin/bash + +# Wait for the service to start +while ! mysqladmin ping -h localhost --silent; do + sleep 1 +done + +# Set the root password +mariadb -e "ALTER USER 'root'@'localhost' IDENTIFIED BY '$MARIADB_ROOT_PASSWORD';" \ No newline at end of file diff --git a/build/supervisord.conf b/build/supervisord.conf new file mode 100644 index 0000000..533b4c6 --- /dev/null +++ b/build/supervisord.conf @@ -0,0 +1,31 @@ +[supervisord] +user=root +nodaemon=true +logfile=/dev/null +logfile_maxbytes=0 +pidfile=/var/run/supervisord.pid +loglevel = INFO + +[program:mariadb] +command=/usr/sbin/mariadbd --user=root +autostart=true +autorestart=true +redirect_stderr=true +stdout_logfile=/dev/fd/1 +stdout_logfile_maxbytes=0 + +[program:mariadb-setup] +command=bash -c "/usr/sbin/start-mariadb.sh" +autostart=true +autorestart=false +redirect_stderr=true +stdout_logfile=/dev/fd/1 +stdout_logfile_maxbytes=0 + +[program:gaseous-server] +command=dotnet /App/gaseous-server.dll +autostart=true +autorestart=true +redirect_stderr=true +stdout_logfile=/dev/fd/1 +stdout_logfile_maxbytes=0 \ No newline at end of file diff --git a/gaseous-server/.DS_Store b/gaseous-server/.DS_Store deleted file mode 100644 index 1f84f25..0000000 Binary files a/gaseous-server/.DS_Store and /dev/null differ diff --git a/gaseous-server/Classes/Auth/Classes/IdentityUser.cs b/gaseous-server/Classes/Auth/Classes/IdentityUser.cs index 8de9b49..4a4c376 100644 --- a/gaseous-server/Classes/Auth/Classes/IdentityUser.cs +++ b/gaseous-server/Classes/Auth/Classes/IdentityUser.cs @@ -12,6 +12,6 @@ namespace Authentication { public SecurityProfileViewModel SecurityProfile { get; set; } public List UserPreferences { get; set; } - public Guid Avatar { get; set; } + public Guid ProfileId { get; set; } } } diff --git a/gaseous-server/Classes/Auth/Classes/RoleTable.cs b/gaseous-server/Classes/Auth/Classes/RoleTable.cs index b9f7d89..6b34957 100644 --- a/gaseous-server/Classes/Auth/Classes/RoleTable.cs +++ b/gaseous-server/Classes/Auth/Classes/RoleTable.cs @@ -9,7 +9,7 @@ namespace Authentication /// /// Class that represents the Role table in the MySQL Database /// - public class RoleTable + public class RoleTable { private Database _database; @@ -63,7 +63,7 @@ namespace Authentication parameters.Add("@id", roleId); DataTable table = _database.ExecuteCMD(commandText, parameters); - + if (table.Rows.Count == 0) { return null; @@ -104,7 +104,7 @@ namespace Authentication var roleName = GetRoleName(roleId); ApplicationRole? role = null; - if(roleName != null) + if (roleName != null) { role = new ApplicationRole(); role.Id = roleId; @@ -153,7 +153,7 @@ namespace Authentication string commandText = "Select Name from Roles"; var rows = _database.ExecuteCMDDict(commandText); - foreach(Dictionary row in rows) + foreach (Dictionary row in rows) { ApplicationRole role = (ApplicationRole)Activator.CreateInstance(typeof(ApplicationRole)); role.Id = (string)row["Id"]; diff --git a/gaseous-server/Classes/Auth/Classes/UserLoginsTable.cs b/gaseous-server/Classes/Auth/Classes/UserLoginsTable.cs index 2837b36..60e9c5d 100644 --- a/gaseous-server/Classes/Auth/Classes/UserLoginsTable.cs +++ b/gaseous-server/Classes/Auth/Classes/UserLoginsTable.cs @@ -82,7 +82,7 @@ namespace Authentication parameters.Add("providerKey", userLogin.ProviderKey); DataTable table = _database.ExecuteCMD(commandText, parameters); - + if (table.Rows.Count == 0) { return null; diff --git a/gaseous-server/Classes/Auth/Classes/UserRoleTable.cs b/gaseous-server/Classes/Auth/Classes/UserRoleTable.cs index 14f8e2a..61a66a4 100644 --- a/gaseous-server/Classes/Auth/Classes/UserRoleTable.cs +++ b/gaseous-server/Classes/Auth/Classes/UserRoleTable.cs @@ -35,7 +35,7 @@ namespace Authentication parameters.Add("@userId", userId); var rows = _database.ExecuteCMD(commandText, parameters).Rows; - foreach(DataRow row in rows) + foreach (DataRow row in rows) { roles.Add((string)row["Name"]); } diff --git a/gaseous-server/Classes/Auth/Classes/UserTable.cs b/gaseous-server/Classes/Auth/Classes/UserTable.cs index d9ff46d..095d628 100644 --- a/gaseous-server/Classes/Auth/Classes/UserTable.cs +++ b/gaseous-server/Classes/Auth/Classes/UserTable.cs @@ -10,7 +10,7 @@ namespace Authentication /// Class that represents the Users table in the MySQL Database /// public class UserTable - where TUser :ApplicationUser + where TUser : ApplicationUser { private Database _database; @@ -34,7 +34,7 @@ namespace Authentication Dictionary parameters = new Dictionary() { { "@id", userId } }; DataTable table = _database.ExecuteCMD(commandText, parameters); - + if (table.Rows.Count == 0) { return null; @@ -75,7 +75,7 @@ namespace Authentication public TUser GetUserById(string userId) { TUser user = null; - string commandText = "Select * from Users LEFT JOIN (SELECT UserId, Id AS AvatarId FROM UserAvatars) UserAvatars ON Users.Id = UserAvatars.UserId where Id = @id"; + string commandText = "Select * from Users LEFT JOIN (SELECT Id As ProfileId, UserId FROM UserProfiles) UserProfiles ON Users.Id = UserProfiles.UserId where Id = @id"; Dictionary parameters = new Dictionary() { { "@id", userId } }; var rows = _database.ExecuteCMDDict(commandText, parameters); @@ -89,7 +89,7 @@ namespace Authentication user.SecurityStamp = (string?)(string.IsNullOrEmpty((string?)row["SecurityStamp"]) ? null : row["SecurityStamp"]); user.ConcurrencyStamp = (string?)(string.IsNullOrEmpty((string?)row["ConcurrencyStamp"]) ? null : row["ConcurrencyStamp"]); user.Email = (string?)(string.IsNullOrEmpty((string?)row["Email"]) ? null : row["Email"]); - user.EmailConfirmed = row["EmailConfirmed"] == "1" ? true:false; + user.EmailConfirmed = row["EmailConfirmed"] == "1" ? true : false; user.PhoneNumber = (string?)(string.IsNullOrEmpty((string?)row["PhoneNumber"]) ? null : row["PhoneNumber"]); user.PhoneNumberConfirmed = row["PhoneNumberConfirmed"] == "1" ? true : false; user.NormalizedEmail = (string?)(string.IsNullOrEmpty((string?)row["NormalizedEmail"]) ? null : row["NormalizedEmail"]); @@ -97,10 +97,10 @@ namespace Authentication user.LockoutEnabled = row["LockoutEnabled"] == "1" ? true : false; user.LockoutEnd = string.IsNullOrEmpty((string?)row["LockoutEnd"]) ? DateTime.Now : DateTime.Parse((string?)row["LockoutEnd"]); user.AccessFailedCount = string.IsNullOrEmpty((string?)row["AccessFailedCount"]) ? 0 : int.Parse((string?)row["AccessFailedCount"]); - user.TwoFactorEnabled = row["TwoFactorEnabled"] == "1" ? true:false; + user.TwoFactorEnabled = row["TwoFactorEnabled"] == "1" ? true : false; user.SecurityProfile = GetSecurityProfile(user); user.UserPreferences = GetPreferences(user); - user.Avatar = string.IsNullOrEmpty((string?)row["AvatarId"]) ? Guid.Empty : Guid.Parse((string?)row["AvatarId"]); + user.ProfileId = string.IsNullOrEmpty((string?)row["ProfileId"]) ? Guid.Empty : Guid.Parse((string?)row["ProfileId"]); } return user; @@ -114,11 +114,11 @@ namespace Authentication public List GetUserByName(string normalizedUserName) { List users = new List(); - string commandText = "Select * from Users LEFT JOIN (SELECT UserId, Id AS AvatarId FROM UserAvatars) UserAvatars ON Users.Id = UserAvatars.UserId where NormalizedEmail = @name"; + string commandText = "Select * from Users LEFT JOIN (SELECT Id As ProfileId, UserId FROM UserProfiles) UserProfiles ON Users.Id = UserProfiles.UserId where NormalizedEmail = @name"; Dictionary parameters = new Dictionary() { { "@name", normalizedUserName } }; var rows = _database.ExecuteCMDDict(commandText, parameters); - foreach(Dictionary row in rows) + foreach (Dictionary row in rows) { TUser user = (TUser)Activator.CreateInstance(typeof(TUser)); user.Id = (string)row["Id"]; @@ -127,7 +127,7 @@ namespace Authentication user.SecurityStamp = (string?)(string.IsNullOrEmpty((string?)row["SecurityStamp"]) ? null : row["SecurityStamp"]); user.ConcurrencyStamp = (string?)(string.IsNullOrEmpty((string?)row["ConcurrencyStamp"]) ? null : row["ConcurrencyStamp"]); user.Email = (string?)(string.IsNullOrEmpty((string?)row["Email"]) ? null : row["Email"]); - user.EmailConfirmed = row["EmailConfirmed"] == "1" ? true:false; + user.EmailConfirmed = row["EmailConfirmed"] == "1" ? true : false; user.PhoneNumber = (string?)(string.IsNullOrEmpty((string?)row["PhoneNumber"]) ? null : row["PhoneNumber"]); user.PhoneNumberConfirmed = row["PhoneNumberConfirmed"] == "1" ? true : false; user.NormalizedEmail = (string?)(string.IsNullOrEmpty((string?)row["NormalizedEmail"]) ? null : row["NormalizedEmail"]); @@ -135,10 +135,10 @@ namespace Authentication user.LockoutEnabled = row["LockoutEnabled"] == "1" ? true : false; user.LockoutEnd = string.IsNullOrEmpty((string?)row["LockoutEnd"]) ? DateTime.Now : DateTime.Parse((string?)row["LockoutEnd"]); user.AccessFailedCount = string.IsNullOrEmpty((string?)row["AccessFailedCount"]) ? 0 : int.Parse((string?)row["AccessFailedCount"]); - user.TwoFactorEnabled = row["TwoFactorEnabled"] == "1" ? true:false; + user.TwoFactorEnabled = row["TwoFactorEnabled"] == "1" ? true : false; user.SecurityProfile = GetSecurityProfile(user); user.UserPreferences = GetPreferences(user); - user.Avatar = string.IsNullOrEmpty((string?)row["AvatarId"]) ? Guid.Empty : Guid.Parse((string?)row["AvatarId"]); + user.ProfileId = string.IsNullOrEmpty((string?)row["ProfileId"]) ? Guid.Empty : Guid.Parse((string?)row["ProfileId"]); users.Add(user); } @@ -148,10 +148,10 @@ namespace Authentication public List GetUsers() { List users = new List(); - string commandText = "Select * from Users LEFT JOIN (SELECT UserId, Id AS AvatarId FROM UserAvatars) UserAvatars ON Users.Id = UserAvatars.UserId order by NormalizedUserName"; - + string commandText = "Select * from Users LEFT JOIN (SELECT Id As ProfileId, UserId FROM UserProfiles) UserProfiles ON Users.Id = UserProfiles.UserId order by NormalizedUserName"; + var rows = _database.ExecuteCMDDict(commandText); - foreach(Dictionary row in rows) + foreach (Dictionary row in rows) { TUser user = (TUser)Activator.CreateInstance(typeof(TUser)); user.Id = (string)row["Id"]; @@ -160,7 +160,7 @@ namespace Authentication user.SecurityStamp = (string?)(string.IsNullOrEmpty((string?)row["SecurityStamp"]) ? null : row["SecurityStamp"]); user.ConcurrencyStamp = (string?)(string.IsNullOrEmpty((string?)row["ConcurrencyStamp"]) ? null : row["ConcurrencyStamp"]); user.Email = (string?)(string.IsNullOrEmpty((string?)row["Email"]) ? null : row["Email"]); - user.EmailConfirmed = row["EmailConfirmed"] == "1" ? true:false; + user.EmailConfirmed = row["EmailConfirmed"] == "1" ? true : false; user.PhoneNumber = (string?)(string.IsNullOrEmpty((string?)row["PhoneNumber"]) ? null : row["PhoneNumber"]); user.PhoneNumberConfirmed = row["PhoneNumberConfirmed"] == "1" ? true : false; user.NormalizedEmail = (string?)(string.IsNullOrEmpty((string?)row["NormalizedEmail"]) ? null : row["NormalizedEmail"]); @@ -168,10 +168,10 @@ namespace Authentication user.LockoutEnabled = row["LockoutEnabled"] == "1" ? true : false; user.LockoutEnd = string.IsNullOrEmpty((string?)row["LockoutEnd"]) ? DateTime.Now : DateTime.Parse((string?)row["LockoutEnd"]); user.AccessFailedCount = string.IsNullOrEmpty((string?)row["AccessFailedCount"]) ? 0 : int.Parse((string?)row["AccessFailedCount"]); - user.TwoFactorEnabled = row["TwoFactorEnabled"] == "1" ? true:false; + user.TwoFactorEnabled = row["TwoFactorEnabled"] == "1" ? true : false; user.SecurityProfile = GetSecurityProfile(user); user.UserPreferences = GetPreferences(user); - user.Avatar = string.IsNullOrEmpty((string?)row["AvatarId"]) ? Guid.Empty : Guid.Parse((string?)row["AvatarId"]); + user.ProfileId = string.IsNullOrEmpty((string?)row["ProfileId"]) ? Guid.Empty : Guid.Parse((string?)row["ProfileId"]); users.Add(user); } @@ -258,10 +258,11 @@ namespace Authentication /// public int Insert(TUser user) { - string commandText = @"Insert into Users (UserName, Id, PasswordHash, SecurityStamp, ConcurrencyStamp, Email, EmailConfirmed, PhoneNumber, PhoneNumberConfirmed, NormalizedEmail, NormalizedUserName, AccessFailedCount, LockoutEnabled, LockoutEnd, TwoFactorEnabled) values (@name, @id, @pwdHash, @SecStamp, @concurrencystamp, @email ,@emailconfirmed ,@phonenumber, @phonenumberconfirmed, @normalizedemail, @normalizedusername, @accesscount, @lockoutenabled, @lockoutenddate, @twofactorenabled);"; + string commandText = @"Insert into Users (UserName, Id, PasswordHash, SecurityStamp, ConcurrencyStamp, Email, EmailConfirmed, PhoneNumber, PhoneNumberConfirmed, NormalizedEmail, NormalizedUserName, AccessFailedCount, LockoutEnabled, LockoutEnd, TwoFactorEnabled) values (@name, @id, @pwdHash, @SecStamp, @concurrencystamp, @email ,@emailconfirmed ,@phonenumber, @phonenumberconfirmed, @normalizedemail, @normalizedusername, @accesscount, @lockoutenabled, @lockoutenddate, @twofactorenabled); Insert into UserProfiles (Id, UserId, DisplayName, Quip, UnstructuredData) values (@profileId, @id, @email, '', '{}');"; Dictionary parameters = new Dictionary(); parameters.Add("@name", user.UserName); parameters.Add("@id", user.Id); + parameters.Add("@profileId", Guid.NewGuid()); parameters.Add("@pwdHash", user.PasswordHash); parameters.Add("@SecStamp", user.SecurityStamp); parameters.Add("@concurrencystamp", user.ConcurrencyStamp); @@ -292,7 +293,7 @@ namespace Authentication /// private int Delete(string userId) { - string commandText = "Delete from Users where Id = @userId; Delete from User_Settings where Id = @userId; Delete from GameState where UserId = @userId;"; + string commandText = "Delete from Users where Id = @userId; Delete from User_Settings where Id = @userId; Delete from UserProfiles where UserId = @userId; Delete from GameState where UserId = @userId;"; Dictionary parameters = new Dictionary(); parameters.Add("@userId", userId); @@ -376,7 +377,7 @@ namespace Authentication Dictionary parameters = new Dictionary(); parameters.Add("Id", user.Id); parameters.Add("SecurityProfile", Newtonsoft.Json.JsonConvert.SerializeObject(securityProfile)); - + return _database.ExecuteCMD(commandText, parameters).Rows.Count; } @@ -408,7 +409,7 @@ namespace Authentication List userPreferences = GetPreferences(user); Database db = new Database(Database.databaseType.MySql, Config.DatabaseConfiguration.ConnectionString); - + foreach (UserPreferenceViewModel modelItem in model) { bool prefItemFound = false; @@ -449,7 +450,7 @@ namespace Authentication { { "userid", user.Id } }; - + if (bytes.Length == 0) { sql = "DELETE FROM UserAvatars WHERE UserId = @userid"; diff --git a/gaseous-server/Classes/Auth/Models/ProfileViewModel.cs b/gaseous-server/Classes/Auth/Models/ProfileViewModel.cs index ce6e9d1..3b3763f 100644 --- a/gaseous-server/Classes/Auth/Models/ProfileViewModel.cs +++ b/gaseous-server/Classes/Auth/Models/ProfileViewModel.cs @@ -8,8 +8,9 @@ namespace Authentication public List Roles { get; set; } public SecurityProfileViewModel SecurityProfile { get; set; } public List UserPreferences { get; set; } - public Guid Avatar { get; set; } - public string HighestRole { + public Guid ProfileId { get; set; } + public string HighestRole + { get { string _highestRole = ""; diff --git a/gaseous-server/Classes/Auth/Models/UserViewModel.cs b/gaseous-server/Classes/Auth/Models/UserViewModel.cs index e1a6b09..0510011 100644 --- a/gaseous-server/Classes/Auth/Models/UserViewModel.cs +++ b/gaseous-server/Classes/Auth/Models/UserViewModel.cs @@ -8,8 +8,9 @@ namespace Authentication public DateTimeOffset? LockoutEnd { get; set; } public List Roles { get; set; } public SecurityProfileViewModel SecurityProfile { get; set; } - public Guid Avatar { get; set; } - public string HighestRole { + public Guid ProfileId { get; set; } + public string HighestRole + { get { string _highestRole = ""; diff --git a/gaseous-server/Classes/Common.cs b/gaseous-server/Classes/Common.cs index 4df3f74..3118396 100644 --- a/gaseous-server/Classes/Common.cs +++ b/gaseous-server/Classes/Common.cs @@ -45,11 +45,13 @@ namespace gaseous_server.Classes { var xmlStream = File.OpenRead(FileName); + Logging.Log(Logging.LogType.Information, "Hash File", "Generating MD5 hash for file: " + FileName); var md5 = MD5.Create(); byte[] md5HashByte = md5.ComputeHash(xmlStream); string md5Hash = BitConverter.ToString(md5HashByte).Replace("-", "").ToLowerInvariant(); _md5hash = md5Hash; + Logging.Log(Logging.LogType.Information, "Hash File", "Generating SHA1 hash for file: " + FileName); var sha1 = SHA1.Create(); xmlStream.Position = 0; byte[] sha1HashByte = sha1.ComputeHash(xmlStream); diff --git a/gaseous-server/Classes/Config.cs b/gaseous-server/Classes/Config.cs index 9ce5cca..cf36e76 100644 --- a/gaseous-server/Classes/Config.cs +++ b/gaseous-server/Classes/Config.cs @@ -197,11 +197,6 @@ namespace gaseous_server.Classes { string SettingName = (string)dataRow["Setting"]; - if (SettingName.StartsWith("LastRun_")) - { - Console.WriteLine("Break"); - } - if (AppSettings.ContainsKey(SettingName)) { AppSettings.Remove(SettingName); diff --git a/gaseous-server/Classes/Database.cs b/gaseous-server/Classes/Database.cs index 555ef3a..31e0171 100644 --- a/gaseous-server/Classes/Database.cs +++ b/gaseous-server/Classes/Database.cs @@ -23,7 +23,7 @@ namespace gaseous_server.Classes _schema_version = value; } } - + public Database() { @@ -70,39 +70,41 @@ namespace gaseous_server.Classes public void InitDB() { - // load resources - var assembly = Assembly.GetExecutingAssembly(); + // load resources + var assembly = Assembly.GetExecutingAssembly(); - switch (_ConnectorType) + DatabaseMemoryCacheOptions? CacheOptions = new DatabaseMemoryCacheOptions(false); + + switch (_ConnectorType) { case databaseType.MySql: // check if the database exists first - first run must have permissions to create a database string sql = "CREATE DATABASE IF NOT EXISTS `" + Config.DatabaseConfiguration.DatabaseName + "`;"; Dictionary dbDict = new Dictionary(); Logging.Log(Logging.LogType.Information, "Database", "Creating database if it doesn't exist"); - ExecuteCMD(sql, dbDict, 30, "server=" + Config.DatabaseConfiguration.HostName + ";port=" + Config.DatabaseConfiguration.Port + ";userid=" + Config.DatabaseConfiguration.UserName + ";password=" + Config.DatabaseConfiguration.Password); + ExecuteCMD(sql, dbDict, CacheOptions, 30, "server=" + Config.DatabaseConfiguration.HostName + ";port=" + Config.DatabaseConfiguration.Port + ";userid=" + Config.DatabaseConfiguration.UserName + ";password=" + Config.DatabaseConfiguration.Password); // check if schema version table is in place - if not, create the schema version table sql = "SELECT TABLE_SCHEMA, TABLE_NAME FROM information_schema.TABLES WHERE TABLE_SCHEMA = '" + Config.DatabaseConfiguration.DatabaseName + "' AND TABLE_NAME = 'schema_version';"; - DataTable SchemaVersionPresent = ExecuteCMD(sql, dbDict); + DataTable SchemaVersionPresent = ExecuteCMD(sql, dbDict, CacheOptions); if (SchemaVersionPresent.Rows.Count == 0) { - // no schema table present - create it - Logging.Log(Logging.LogType.Information, "Database", "Schema version table doesn't exist. Creating it."); - sql = "CREATE TABLE `schema_version` (`schema_version` INT NOT NULL, PRIMARY KEY (`schema_version`)); INSERT INTO `schema_version` (`schema_version`) VALUES (0);"; - ExecuteCMD(sql, dbDict); + // no schema table present - create it + Logging.Log(Logging.LogType.Information, "Database", "Schema version table doesn't exist. Creating it."); + sql = "CREATE TABLE `schema_version` (`schema_version` INT NOT NULL, PRIMARY KEY (`schema_version`)); INSERT INTO `schema_version` (`schema_version`) VALUES (0);"; + ExecuteCMD(sql, dbDict, CacheOptions); } sql = "SELECT schema_version FROM schema_version;"; dbDict = new Dictionary(); - DataTable SchemaVersion = ExecuteCMD(sql, dbDict); + DataTable SchemaVersion = ExecuteCMD(sql, dbDict, CacheOptions); int OuterSchemaVer = (int)SchemaVersion.Rows[0][0]; if (OuterSchemaVer == 0) { OuterSchemaVer = 1000; } - for (int i = OuterSchemaVer; i < 10000; i++) + for (int i = OuterSchemaVer; i < 10000; i++) { string resourceName = "gaseous_server.Support.Database.MySQL.gaseous-" + i + ".sql"; string dbScript = ""; @@ -113,39 +115,39 @@ namespace gaseous_server.Classes using (Stream stream = assembly.GetManifestResourceStream(resourceName)) using (StreamReader reader = new StreamReader(stream)) { - dbScript = reader.ReadToEnd(); + dbScript = reader.ReadToEnd(); // apply script sql = "SELECT schema_version FROM schema_version;"; dbDict = new Dictionary(); - SchemaVersion = ExecuteCMD(sql, dbDict); + SchemaVersion = ExecuteCMD(sql, dbDict, CacheOptions); if (SchemaVersion.Rows.Count == 0) { - // something is broken here... where's the table? - Logging.Log(Logging.LogType.Critical, "Database", "Schema table missing! This shouldn't happen!"); - throw new Exception("schema_version table is missing!"); + // something is broken here... where's the table? + Logging.Log(Logging.LogType.Critical, "Database", "Schema table missing! This shouldn't happen!"); + throw new Exception("schema_version table is missing!"); } else { int SchemaVer = (int)SchemaVersion.Rows[0][0]; - Logging.Log(Logging.LogType.Information, "Database", "Schema version is " + SchemaVer); + Logging.Log(Logging.LogType.Information, "Database", "Schema version is " + SchemaVer); // update schema version variable Database.schema_version = SchemaVer; - if (SchemaVer < i) + if (SchemaVer < i) { try { // run pre-upgrade code DatabaseMigration.PreUpgradeScript(i, _ConnectorType); - + // apply schema! Logging.Log(Logging.LogType.Information, "Database", "Updating schema to version " + i); - ExecuteCMD(dbScript, dbDict, 180); + ExecuteCMD(dbScript, dbDict, CacheOptions, 180); sql = "UPDATE schema_version SET schema_version=@schemaver"; dbDict = new Dictionary(); dbDict.Add("schemaver", i); - ExecuteCMD(sql, dbDict); + ExecuteCMD(sql, dbDict, CacheOptions); // run post-upgrade code DatabaseMigration.PostUpgradeScript(i, _ConnectorType); @@ -162,47 +164,91 @@ namespace gaseous_server.Classes } } } - } - Logging.Log(Logging.LogType.Information, "Database", "Database setup complete"); - break; + } + Logging.Log(Logging.LogType.Information, "Database", "Database setup complete"); + break; } } - public DataTable ExecuteCMD(string Command) + public DataTable ExecuteCMD(string Command) { + DatabaseMemoryCacheOptions? CacheOptions = null; + Dictionary dbDict = new Dictionary(); - return _ExecuteCMD(Command, dbDict, 30, ""); + return _ExecuteCMD(Command, dbDict, CacheOptions, 30, ""); } - public DataTable ExecuteCMD(string Command, Dictionary Parameters) - { - return _ExecuteCMD(Command, Parameters, 30, ""); - } + public DataTable ExecuteCMD(string Command, DatabaseMemoryCacheOptions? CacheOptions) + { + Dictionary dbDict = new Dictionary(); + return _ExecuteCMD(Command, dbDict, CacheOptions, 30, ""); + } - public DataTable ExecuteCMD(string Command, Dictionary Parameters, int Timeout = 30, string ConnectionString = "") - { - return _ExecuteCMD(Command, Parameters, Timeout, ConnectionString); - } + public DataTable ExecuteCMD(string Command, Dictionary Parameters) + { + DatabaseMemoryCacheOptions? CacheOptions = null; + + return _ExecuteCMD(Command, Parameters, CacheOptions, 30, ""); + } + + public DataTable ExecuteCMD(string Command, Dictionary Parameters, DatabaseMemoryCacheOptions? CacheOptions) + { + return _ExecuteCMD(Command, Parameters, CacheOptions, 30, ""); + } + + public DataTable ExecuteCMD(string Command, Dictionary Parameters, int Timeout = 30, string ConnectionString = "") + { + DatabaseMemoryCacheOptions? CacheOptions = null; + + return _ExecuteCMD(Command, Parameters, CacheOptions, Timeout, ConnectionString); + } + + public DataTable ExecuteCMD(string Command, Dictionary Parameters, DatabaseMemoryCacheOptions? CacheOptions, int Timeout = 30, string ConnectionString = "") + { + return _ExecuteCMD(Command, Parameters, CacheOptions, Timeout, ConnectionString); + } public List> ExecuteCMDDict(string Command) { + DatabaseMemoryCacheOptions? CacheOptions = null; + Dictionary dbDict = new Dictionary(); - return _ExecuteCMDDict(Command, dbDict, 30, ""); + return _ExecuteCMDDict(Command, dbDict, CacheOptions, 30, ""); } - public List> ExecuteCMDDict(string Command, Dictionary Parameters) - { - return _ExecuteCMDDict(Command, Parameters, 30, ""); - } - - public List> ExecuteCMDDict(string Command, Dictionary Parameters, int Timeout = 30, string ConnectionString = "") - { - return _ExecuteCMDDict(Command, Parameters, Timeout, ConnectionString); - } - - private List> _ExecuteCMDDict(string Command, Dictionary Parameters, int Timeout = 30, string ConnectionString = "") + public List> ExecuteCMDDict(string Command, DatabaseMemoryCacheOptions? CacheOptions) { - DataTable dataTable = _ExecuteCMD(Command, Parameters, Timeout, ConnectionString); + Dictionary dbDict = new Dictionary(); + return _ExecuteCMDDict(Command, dbDict, CacheOptions, 30, ""); + } + + public List> ExecuteCMDDict(string Command, Dictionary Parameters) + { + DatabaseMemoryCacheOptions? CacheOptions = null; + + return _ExecuteCMDDict(Command, Parameters, CacheOptions, 30, ""); + } + + public List> ExecuteCMDDict(string Command, Dictionary Parameters, DatabaseMemoryCacheOptions? CacheOptions) + { + return _ExecuteCMDDict(Command, Parameters, CacheOptions, 30, ""); + } + + public List> ExecuteCMDDict(string Command, Dictionary Parameters, int Timeout = 30, string ConnectionString = "") + { + DatabaseMemoryCacheOptions? CacheOptions = null; + + return _ExecuteCMDDict(Command, Parameters, CacheOptions, Timeout, ConnectionString); + } + + public List> ExecuteCMDDict(string Command, Dictionary Parameters, DatabaseMemoryCacheOptions? CacheOptions, int Timeout = 30, string ConnectionString = "") + { + return _ExecuteCMDDict(Command, Parameters, CacheOptions, Timeout, ConnectionString); + } + + private List> _ExecuteCMDDict(string Command, Dictionary Parameters, DatabaseMemoryCacheOptions? CacheOptions, int Timeout = 30, string ConnectionString = "") + { + DataTable dataTable = _ExecuteCMD(Command, Parameters, CacheOptions, Timeout, ConnectionString); // convert datatable to dictionary List> rows = new List>(); @@ -228,18 +274,49 @@ namespace gaseous_server.Classes return rows; } - private DataTable _ExecuteCMD(string Command, Dictionary Parameters, int Timeout = 30, string ConnectionString = "") - { - if (ConnectionString == "") { ConnectionString = _ConnectionString; } - switch (_ConnectorType) - { - case databaseType.MySql: - MySQLServerConnector conn = new MySQLServerConnector(ConnectionString); - return (DataTable)conn.ExecCMD(Command, Parameters, Timeout); - default: - return new DataTable(); - } - } + private DataTable _ExecuteCMD(string Command, Dictionary Parameters, DatabaseMemoryCacheOptions? CacheOptions, int Timeout = 30, string ConnectionString = "") + { + string CacheKey = Command + string.Join(";", Parameters.Select(x => string.Join("=", x.Key, x.Value))); + + if (CacheOptions is object && CacheOptions.CacheEnabled) + { + object? CachedData = DatabaseMemoryCache.GetCacheObject(CacheKey); + if (CachedData is object) + { + return (DataTable)CachedData; + } + } + + // purge cache if command contains "INSERT", "UPDATE", "DELETE", or "ALTER" + if ( + Command.Contains("INSERT", StringComparison.InvariantCultureIgnoreCase) || + Command.Contains("UPDATE", StringComparison.InvariantCultureIgnoreCase) || + Command.Contains("DELETE", StringComparison.InvariantCultureIgnoreCase) || + Command.Contains("ALTER", StringComparison.InvariantCultureIgnoreCase) + ) + { + // exclude logging events from purging the cache + if (!Command.StartsWith("INSERT INTO SERVERLOGS", StringComparison.InvariantCultureIgnoreCase)) + { + DatabaseMemoryCache.ClearCache(); + } + } + + if (ConnectionString == "") { ConnectionString = _ConnectionString; } + switch (_ConnectorType) + { + case databaseType.MySql: + MySQLServerConnector conn = new MySQLServerConnector(ConnectionString); + DataTable RetTable = conn.ExecCMD(Command, Parameters, Timeout); + if (CacheOptions is object && CacheOptions.CacheEnabled) + { + DatabaseMemoryCache.SetCacheObject(CacheKey, RetTable, CacheOptions.ExpirationSeconds); + } + return RetTable; + default: + return new DataTable(); + } + } public int ExecuteNonQuery(string Command) { @@ -247,52 +324,194 @@ namespace gaseous_server.Classes return _ExecuteNonQuery(Command, dbDict, 30, ""); } - public int ExecuteNonQuery(string Command, Dictionary Parameters) - { - return _ExecuteNonQuery(Command, Parameters, 30, ""); - } + public int ExecuteNonQuery(string Command, Dictionary Parameters) + { + return _ExecuteNonQuery(Command, Parameters, 30, ""); + } - public int ExecuteNonQuery(string Command, Dictionary Parameters, int Timeout = 30, string ConnectionString = "") - { + public int ExecuteNonQuery(string Command, Dictionary Parameters, int Timeout = 30, string ConnectionString = "") + { return _ExecuteNonQuery(Command, Parameters, Timeout, ConnectionString); - } + } - private int _ExecuteNonQuery(string Command, Dictionary Parameters, int Timeout = 30, string ConnectionString = "") - { - if (ConnectionString == "") { ConnectionString = _ConnectionString; } - switch (_ConnectorType) - { - case databaseType.MySql: - MySQLServerConnector conn = new MySQLServerConnector(ConnectionString); - int retVal = conn.ExecNonQuery(Command, Parameters, Timeout); - return retVal; - default: - return 0; - } - } + private int _ExecuteNonQuery(string Command, Dictionary Parameters, int Timeout = 30, string ConnectionString = "") + { + if (ConnectionString == "") { ConnectionString = _ConnectionString; } + switch (_ConnectorType) + { + case databaseType.MySql: + MySQLServerConnector conn = new MySQLServerConnector(ConnectionString); + int retVal = conn.ExecNonQuery(Command, Parameters, Timeout); + return retVal; + default: + return 0; + } + } - public void ExecuteTransactionCMD(List CommandList, int Timeout = 60) - { - object conn; - switch (_ConnectorType) - { - case databaseType.MySql: - { - var commands = new List>(); - foreach (SQLTransactionItem CommandItem in CommandList) - { - var nCmd = new Dictionary(); - nCmd.Add("sql", CommandItem.SQLCommand); - nCmd.Add("values", CommandItem.Parameters); - commands.Add(nCmd); - } + public void ExecuteTransactionCMD(List CommandList, int Timeout = 60) + { + object conn; + switch (_ConnectorType) + { + case databaseType.MySql: + { + var commands = new List>(); + foreach (SQLTransactionItem CommandItem in CommandList) + { + var nCmd = new Dictionary(); + nCmd.Add("sql", CommandItem.SQLCommand); + nCmd.Add("values", CommandItem.Parameters); + commands.Add(nCmd); + } - conn = new MySQLServerConnector(_ConnectionString); - ((MySQLServerConnector)conn).TransactionExecCMD(commands, Timeout); - break; - } - } - } + conn = new MySQLServerConnector(_ConnectionString); + ((MySQLServerConnector)conn).TransactionExecCMD(commands, Timeout); + break; + } + } + } + + public static class DatabaseMemoryCache + { + private class MemoryCacheItem + { + public MemoryCacheItem(object CacheObject) + { + cacheObject = CacheObject; + } + + public MemoryCacheItem(object CacheObject, int ExpirationSeconds) + { + cacheObject = CacheObject; + expirationSeconds = ExpirationSeconds; + } + + /// + /// The time the object was added to the cache in ticks + /// + public long addedTime { get; } = Environment.TickCount64; + + /// + /// The time the object will expire in ticks + /// + public long expirationTime + { + get + { + return addedTime + _expirationTicks; + } + } + + /// + /// The number of seconds the object will be cached + /// + public int expirationSeconds + { + get + { + return (int)TimeSpan.FromTicks(_expirationTicks).Seconds; + } + set + { + _expirationTicks = (long)TimeSpan.FromSeconds(value).Ticks; + } + } + + private long _expirationTicks = (long)TimeSpan.FromSeconds(2).Ticks; + + /// + /// The object to be cached + /// + public object cacheObject { get; set; } + } + private static Dictionary MemoryCache = new Dictionary(); + private static Timer CacheTimer = new Timer(CacheTimerCallback, null, 0, 1000); + + private static void CacheTimerCallback(object? state) + { + ClearExpiredCache(); + } + + public static object? GetCacheObject(string CacheKey) + { + if (MemoryCache.ContainsKey(CacheKey)) + { + if (MemoryCache[CacheKey].expirationTime < Environment.TickCount) + { + MemoryCache.Remove(CacheKey); + return null; + } + else + { + return MemoryCache[CacheKey].cacheObject; + } + } + else + { + return null; + } + } + public static void SetCacheObject(string CacheKey, object CacheObject, int ExpirationSeconds = 2) + { + try + { + if (MemoryCache.ContainsKey(CacheKey)) + { + MemoryCache.Remove(CacheKey); + } + MemoryCache.Add(CacheKey, new MemoryCacheItem(CacheObject, ExpirationSeconds)); + } + catch (Exception ex) + { + Logging.Log(Logging.LogType.Debug, "Database", "Error while setting cache object", ex); + ClearCache(); + } + } + public static void RemoveCacheObject(string CacheKey) + { + if (MemoryCache.ContainsKey(CacheKey)) + { + MemoryCache.Remove(CacheKey); + } + } + public static void ClearCache() + { + MemoryCache.Clear(); + } + private static void ClearExpiredCache() + { + try + { + long currTime = Environment.TickCount64; + + Dictionary ExpiredItems = MemoryCache; + foreach (string key in ExpiredItems.Keys) + { + if (MemoryCache[key].expirationTime < currTime) + { + Console.WriteLine("\x1b[95mPurging expired cache item " + key + ". Added: " + MemoryCache[key].addedTime + ". Expired: " + MemoryCache[key].expirationTime); + MemoryCache.Remove(key); + } + } + } + catch (Exception ex) + { + Logging.Log(Logging.LogType.Debug, "Database", "Error while clearing expired cache", ex); + } + } + } + + public class DatabaseMemoryCacheOptions + { + public DatabaseMemoryCacheOptions(bool CacheEnabled = false, int ExpirationSeconds = 1) + { + this.CacheEnabled = CacheEnabled; + this.ExpirationSeconds = ExpirationSeconds; + } + + public bool CacheEnabled { get; set; } + public int ExpirationSeconds { get; set; } + } public int GetDatabaseSchemaVersion() { @@ -309,7 +528,7 @@ namespace gaseous_server.Classes { return (int)SchemaVersion.Rows[0][0]; } - + default: return 0; @@ -319,17 +538,17 @@ namespace gaseous_server.Classes public bool TestConnection() { switch (_ConnectorType) - { - case databaseType.MySql: - MySQLServerConnector conn = new MySQLServerConnector(_ConnectionString); - return conn.TestConnection(); - default: - return false; - } + { + case databaseType.MySql: + MySQLServerConnector conn = new MySQLServerConnector(_ConnectionString); + return conn.TestConnection(); + default: + return false; + } } - public class SQLTransactionItem - { + public class SQLTransactionItem + { public SQLTransactionItem() { @@ -341,11 +560,11 @@ namespace gaseous_server.Classes this.Parameters = Parameters; } - public string? SQLCommand; - public Dictionary? Parameters = new Dictionary(); - } + public string? SQLCommand; + public Dictionary? Parameters = new Dictionary(); + } - private partial class MySQLServerConnector + private partial class MySQLServerConnector { private string DBConn = ""; @@ -358,8 +577,8 @@ namespace gaseous_server.Classes { DataTable RetTable = new DataTable(); - Logging.Log(Logging.LogType.Debug, "Database", "Connecting to database", null, true); - using (MySqlConnection conn = new MySqlConnection(DBConn)) + Logging.Log(Logging.LogType.Debug, "Database", "Connecting to database", null, true); + using (MySqlConnection conn = new MySqlConnection(DBConn)) { conn.Open(); @@ -384,7 +603,9 @@ namespace gaseous_server.Classes Logging.Log(Logging.LogType.Debug, "Database", "Parameters: " + dictValues, null, true); } RetTable.Load(cmd.ExecuteReader()); - } catch (Exception ex) { + } + catch (Exception ex) + { Logging.Log(Logging.LogType.Critical, "Database", "Error while executing '" + SQL + "'", ex); Trace.WriteLine("Error executing " + SQL); Trace.WriteLine("Full exception: " + ex.ToString()); @@ -397,12 +618,12 @@ namespace gaseous_server.Classes return RetTable; } - public int ExecNonQuery(string SQL, Dictionary< string, object> Parameters, int Timeout) + public int ExecNonQuery(string SQL, Dictionary Parameters, int Timeout) { int result = 0; - Logging.Log(Logging.LogType.Debug, "Database", "Connecting to database", null, true); - using (MySqlConnection conn = new MySqlConnection(DBConn)) + Logging.Log(Logging.LogType.Debug, "Database", "Connecting to database", null, true); + using (MySqlConnection conn = new MySqlConnection(DBConn)) { conn.Open(); @@ -427,7 +648,9 @@ namespace gaseous_server.Classes Logging.Log(Logging.LogType.Debug, "Database", "Parameters: " + dictValues, null, true); } result = cmd.ExecuteNonQuery(); - } catch (Exception ex) { + } + catch (Exception ex) + { Logging.Log(Logging.LogType.Critical, "Database", "Error while executing '" + SQL + "'", ex); Trace.WriteLine("Error executing " + SQL); Trace.WriteLine("Full exception: " + ex.ToString()); @@ -440,10 +663,10 @@ namespace gaseous_server.Classes return result; } - public void TransactionExecCMD(List> Parameters, int Timeout) - { - using (MySqlConnection conn = new MySqlConnection(DBConn)) - { + public void TransactionExecCMD(List> Parameters, int Timeout) + { + using (MySqlConnection conn = new MySqlConnection(DBConn)) + { conn.Open(); var command = conn.CreateCommand(); MySqlTransaction transaction; @@ -460,28 +683,28 @@ namespace gaseous_server.Classes transaction.Commit(); conn.Close(); } - } + } - private MySqlCommand buildcommand(MySqlConnection Conn, string SQL, Dictionary Parameters, int Timeout) - { - var cmd = new MySqlCommand(); - cmd.Connection = Conn; - cmd.CommandText = SQL; - cmd.CommandTimeout = Timeout; - { - var withBlock = cmd.Parameters; - if (Parameters is object) - { - if (Parameters.Count > 0) - { - foreach (string param in Parameters.Keys) - withBlock.AddWithValue(param, Parameters[param]); - } - } - } + private MySqlCommand buildcommand(MySqlConnection Conn, string SQL, Dictionary Parameters, int Timeout) + { + var cmd = new MySqlCommand(); + cmd.Connection = Conn; + cmd.CommandText = SQL; + cmd.CommandTimeout = Timeout; + { + var withBlock = cmd.Parameters; + if (Parameters is object) + { + if (Parameters.Count > 0) + { + foreach (string param in Parameters.Keys) + withBlock.AddWithValue(param, Parameters[param]); + } + } + } - return cmd; - } + return cmd; + } public bool TestConnection() { @@ -499,7 +722,7 @@ namespace gaseous_server.Classes } } } - } - } + } + } } diff --git a/gaseous-server/Classes/DatabaseMigration.cs b/gaseous-server/Classes/DatabaseMigration.cs index b701076..48a8f11 100644 --- a/gaseous-server/Classes/DatabaseMigration.cs +++ b/gaseous-server/Classes/DatabaseMigration.cs @@ -153,6 +153,35 @@ namespace gaseous_server.Classes // this is a safe background task BackgroundUpgradeTargetSchemaVersions.Add(1022); break; + + case 1023: + // create profiles for all existing users + sql = "SELECT * FROM Users;"; + data = db.ExecuteCMD(sql); + foreach (DataRow row in data.Rows) + { + // get legacy avatar from UserAvatars table + sql = "SELECT Avatar FROM UserAvatars WHERE UserId = @userid;"; + dbDict = new Dictionary + { + { "userid", row["Id"] } + }; + DataTable avatarData = db.ExecuteCMD(sql, dbDict); + + sql = "INSERT INTO UserProfiles (Id, UserId, DisplayName, Quip, Avatar, AvatarExtension, UnstructuredData) VALUES (@id, @userid, @displayname, @quip, @avatar, @avatarextension, @data);"; + dbDict = new Dictionary + { + { "id", Guid.NewGuid() }, + { "userid", row["Id"] }, + { "displayname", row["Email"] }, + { "quip", "" }, + { "avatar", avatarData.Rows.Count > 0 ? avatarData.Rows[0]["Avatar"] : null }, + { "avatarextension", avatarData.Rows.Count > 0 ? ".jpg" : null }, + { "data", "{}" } + }; + db.ExecuteNonQuery(sql, dbDict); + } + break; } break; } @@ -279,9 +308,10 @@ namespace gaseous_server.Classes Database db = new Database(Database.databaseType.MySql, Config.DatabaseConfiguration.ConnectionString); string sql = "SELECT * FROM Games_Roms WHERE RomDataVersion = 1;"; DataTable data = db.ExecuteCMD(sql); + long count = 1; foreach (DataRow row in data.Rows) { - Logging.Log(Logging.LogType.Information, "Database Migration", "Updating ROM table for ROM: " + (string)row["Name"]); + Logging.Log(Logging.LogType.Information, "Database Migration", "Updating ROM table for ROM (" + count + " / " + data.Rows.Count + "): " + (string)row["Name"]); GameLibrary.LibraryItem library = GameLibrary.GetLibrary((int)row["LibraryId"]); Common.hashObject hash = new Common.hashObject() @@ -300,6 +330,8 @@ namespace gaseous_server.Classes Game game = Games.GetGame((long)row["GameId"], false, false, false); ImportGame.StoreROM(library, hash, game, platform, signature, (string)row["Path"], (long)row["Id"]); + + count += 1; } } } diff --git a/gaseous-server/Classes/FileSignature.cs b/gaseous-server/Classes/FileSignature.cs index c27e9de..68f38b4 100644 --- a/gaseous-server/Classes/FileSignature.cs +++ b/gaseous-server/Classes/FileSignature.cs @@ -1,5 +1,6 @@ using System.Collections.Concurrent; using System.IO.Compression; +using System.Net; using gaseous_server.Classes.Metadata; using HasheousClient.Models; using Microsoft.CodeAnalysis.CSharp.Syntax; @@ -192,37 +193,45 @@ namespace gaseous_server.Classes Logging.Log(Logging.LogType.Information, "Import Game", "Checking signature for file: " + GameFileImportPath + "\nMD5 hash: " + hash.md5hash + "\nSHA1 hash: " + hash.sha1hash); - gaseous_server.Models.Signatures_Games discoveredSignature = new gaseous_server.Models.Signatures_Games(); + gaseous_server.Models.Signatures_Games? discoveredSignature = null; - // do database search first - gaseous_server.Models.Signatures_Games? dbSignature = _GetFileSignatureFromDatabase(hash, ImageName, ImageExtension, ImageSize, GameFileImportPath); - - if (dbSignature != null) + // begin signature search + switch (Config.MetadataConfiguration.SignatureSource) { - // local signature found - Logging.Log(Logging.LogType.Information, "Import Game", "Signature found in local database for game: " + dbSignature.Game.Name); - discoveredSignature = dbSignature; + case HasheousClient.Models.MetadataModel.SignatureSources.LocalOnly: + Logging.Log(Logging.LogType.Information, "Import Game", "Hasheous disabled - searching local database only"); + + discoveredSignature = _GetFileSignatureFromDatabase(hash, ImageName, ImageExtension, ImageSize, GameFileImportPath); + + break; + + case HasheousClient.Models.MetadataModel.SignatureSources.Hasheous: + Logging.Log(Logging.LogType.Information, "Import Game", "Hasheous enabled - searching Hashesous and then local database if not found"); + + discoveredSignature = _GetFileSignatureFromHasheous(hash, ImageName, ImageExtension, ImageSize, GameFileImportPath); + + if (discoveredSignature == null) + { + Logging.Log(Logging.LogType.Information, "Import Game", "Signature not found in Hasheous - checking local database"); + + discoveredSignature = _GetFileSignatureFromDatabase(hash, ImageName, ImageExtension, ImageSize, GameFileImportPath); + } + else + { + Logging.Log(Logging.LogType.Information, "Import Game", "Signature retrieved from Hasheous for game: " + discoveredSignature.Game.Name); + } + break; + } - else + + if (discoveredSignature == null) { - // no local signature attempt to pull from Hasheous - dbSignature = _GetFileSignatureFromHasheous(hash, ImageName, ImageExtension, ImageSize, GameFileImportPath); + // construct a signature from file data + Logging.Log(Logging.LogType.Information, "Import Game", "Signature not found in local database or Hasheous (if enabled) - generating from file data"); - if (dbSignature != null) - { - // signature retrieved from Hasheous - Logging.Log(Logging.LogType.Information, "Import Game", "Signature retrieved from Hasheous for game: " + dbSignature.Game.Name); + discoveredSignature = _GetFileSignatureFromFileData(hash, ImageName, ImageExtension, ImageSize, GameFileImportPath); - discoveredSignature = dbSignature; - } - else - { - // construct a signature from file data - dbSignature = _GetFileSignatureFromFileData(hash, ImageName, ImageExtension, ImageSize, GameFileImportPath); - Logging.Log(Logging.LogType.Information, "Import Game", "Signature generated from provided file for game: " + dbSignature.Game.Name); - - discoveredSignature = dbSignature; - } + Logging.Log(Logging.LogType.Information, "Import Game", "Signature generated from provided file for game: " + discoveredSignature.Game.Name); } gaseous_server.Models.PlatformMapping.GetIGDBPlatformMapping(ref discoveredSignature, ImageExtension, false); @@ -290,65 +299,87 @@ namespace gaseous_server.Classes MD5 = hash.md5hash, SHA1 = hash.sha1hash }); + + if (HasheousResult != null) + { + if (HasheousResult.Signature != null) + { + gaseous_server.Models.Signatures_Games signature = new Models.Signatures_Games(); + string gameJson = Newtonsoft.Json.JsonConvert.SerializeObject(HasheousResult.Signature.Game); + string romJson = Newtonsoft.Json.JsonConvert.SerializeObject(HasheousResult.Signature.Rom); + signature.Game = Newtonsoft.Json.JsonConvert.DeserializeObject(gameJson); + signature.Rom = Newtonsoft.Json.JsonConvert.DeserializeObject(romJson); + + // get platform metadata + if (HasheousResult.Platform != null) + { + if (HasheousResult.Platform.metadata.Count > 0) + { + foreach (HasheousClient.Models.MetadataItem metadataResult in HasheousResult.Platform.metadata) + { + if (metadataResult.Id.Length > 0) + { + switch (metadataResult.Source) + { + case HasheousClient.Models.MetadataSources.IGDB: + signature.Flags.IGDBPlatformId = (long)Platforms.GetPlatform(metadataResult.Id, false).Id; + break; + } + } + } + } + } + + // get game metadata + if (HasheousResult.Metadata != null) + { + if (HasheousResult.Metadata.Count > 0) + { + foreach (HasheousClient.Models.MetadataItem metadataResult in HasheousResult.Metadata) + { + if (metadataResult.Id.Length > 0) + { + switch (metadataResult.Source) + { + case HasheousClient.Models.MetadataSources.IGDB: + signature.Flags.IGDBGameId = (long)Games.GetGame(metadataResult.Id, false, false, false).Id; + break; + } + } + } + } + } + + return signature; + } + } + } + catch (AggregateException aggEx) + { + foreach (Exception ex in aggEx.InnerExceptions) + { + // get exception type + if (ex is HttpRequestException) + { + if (ex.Message.Contains("404 (Not Found)")) + { + Logging.Log(Logging.LogType.Information, "Get Signature", "No signature found in Hasheous"); + } + else + { + Logging.Log(Logging.LogType.Warning, "Get Signature", "Error retrieving signature from Hasheous", ex); + } + } + else + { + Logging.Log(Logging.LogType.Warning, "Get Signature", "Error retrieving signature from Hasheous", ex); + + } + } } catch (Exception ex) { - Logging.Log(Logging.LogType.Warning, "Import Game", "An error occurred while importing " + ImageName, ex); - return null; - } - - if (HasheousResult != null) - { - if (HasheousResult.Signature != null) - { - gaseous_server.Models.Signatures_Games signature = new Models.Signatures_Games(); - string gameJson = Newtonsoft.Json.JsonConvert.SerializeObject(HasheousResult.Signature.Game); - string romJson = Newtonsoft.Json.JsonConvert.SerializeObject(HasheousResult.Signature.Rom); - signature.Game = Newtonsoft.Json.JsonConvert.DeserializeObject(gameJson); - signature.Rom = Newtonsoft.Json.JsonConvert.DeserializeObject(romJson); - - // get platform metadata - if (HasheousResult.Platform != null) - { - if (HasheousResult.Platform.metadata.Count > 0) - { - foreach (HasheousClient.Models.MetadataItem metadataResult in HasheousResult.Platform.metadata) - { - if (metadataResult.Id.Length > 0) - { - switch (metadataResult.Source) - { - case HasheousClient.Models.MetadataSources.IGDB: - signature.Flags.IGDBPlatformId = (long)Platforms.GetPlatform(metadataResult.Id, false).Id; - break; - } - } - } - } - } - - // get game metadata - if (HasheousResult.Metadata != null) - { - if (HasheousResult.Metadata.Count > 0) - { - foreach (HasheousClient.Models.MetadataItem metadataResult in HasheousResult.Metadata) - { - if (metadataResult.Id.Length > 0) - { - switch (metadataResult.Source) - { - case HasheousClient.Models.MetadataSources.IGDB: - signature.Flags.IGDBGameId = (long)Games.GetGame(metadataResult.Id, false, false, false).Id; - break; - } - } - } - } - } - - return signature; - } + Logging.Log(Logging.LogType.Warning, "Get Signature", "Error retrieving signature from Hasheous", ex); } } diff --git a/gaseous-server/Classes/Filters.cs b/gaseous-server/Classes/Filters.cs index 7bdaf7b..f58291a 100644 --- a/gaseous-server/Classes/Filters.cs +++ b/gaseous-server/Classes/Filters.cs @@ -15,7 +15,7 @@ namespace gaseous_server.Classes // platforms List platforms = new List(); - + string ageRestriction_Platform = "AgeGroup.AgeGroupId <= " + (int)MaximumAgeRestriction; string ageRestriction_Generic = "view_Games.AgeGroupId <= " + (int)MaximumAgeRestriction; if (IncludeUnrated == true) @@ -25,8 +25,8 @@ namespace gaseous_server.Classes } string sql = "SELECT Platform.Id, Platform.`Name`, COUNT(Game.Id) AS GameCount FROM (SELECT DISTINCT Game.Id, Games_Roms.PlatformId, COUNT(Games_Roms.Id) AS RomCount FROM Game LEFT JOIN AgeGroup ON Game.Id = AgeGroup.GameId LEFT JOIN Games_Roms ON Game.Id = Games_Roms.GameId WHERE (" + ageRestriction_Platform + ") GROUP BY Game.Id , Games_Roms.PlatformId HAVING RomCount > 0) Game JOIN Platform ON Game.PlatformId = Platform.Id GROUP BY Platform.`Name`;"; - - DataTable dbResponse = db.ExecuteCMD(sql); + + DataTable dbResponse = db.ExecuteCMD(sql, new Database.DatabaseMemoryCacheOptions(CacheEnabled: true, ExpirationSeconds: 300)); foreach (DataRow dr in dbResponse.Rows) { @@ -83,7 +83,7 @@ namespace gaseous_server.Classes // age groups List agegroupings = new List(); sql = "SELECT Game.AgeGroupId, COUNT(Game.Id) AS GameCount FROM (SELECT DISTINCT Game.Id, AgeGroup.AgeGroupId, COUNT(Games_Roms.Id) AS RomCount FROM Game LEFT JOIN AgeGroup ON Game.Id = AgeGroup.GameId LEFT JOIN Games_Roms ON Game.Id = Games_Roms.GameId WHERE (" + ageRestriction_Platform + ") GROUP BY Game.Id HAVING RomCount > 0) Game GROUP BY Game.AgeGroupId ORDER BY Game.AgeGroupId DESC"; - dbResponse = db.ExecuteCMD(sql); + dbResponse = db.ExecuteCMD(sql, new Database.DatabaseMemoryCacheOptions(CacheEnabled: true, ExpirationSeconds: 300)); foreach (DataRow dr in dbResponse.Rows) { @@ -114,7 +114,7 @@ namespace gaseous_server.Classes string sql = "SELECT .Id, .`Name`, COUNT(Game.Id) AS GameCount FROM (SELECT DISTINCT Game.Id, AgeGroup.AgeGroupId, COUNT(Games_Roms.Id) AS RomCount FROM Game LEFT JOIN AgeGroup ON Game.Id = AgeGroup.GameId LEFT JOIN Games_Roms ON Game.Id = Games_Roms.GameId WHERE (" + AgeRestriction + ") GROUP BY Game.Id HAVING RomCount > 0) Game JOIN Relation_Game_s ON Game.Id = Relation_Game_s.GameId JOIN ON Relation_Game_s.sId = .Id GROUP BY .`Name` ORDER BY .`Name`;"; sql = sql.Replace("", Name); - DataTable dbResponse = db.ExecuteCMD(sql); + DataTable dbResponse = db.ExecuteCMD(sql, new Database.DatabaseMemoryCacheOptions(CacheEnabled: true, ExpirationSeconds: 300)); return dbResponse; } diff --git a/gaseous-server/Classes/GameLibrary.cs b/gaseous-server/Classes/GameLibrary.cs index 161c1d6..0b57b85 100644 --- a/gaseous-server/Classes/GameLibrary.cs +++ b/gaseous-server/Classes/GameLibrary.cs @@ -7,37 +7,37 @@ using Microsoft.CodeAnalysis.FlowAnalysis.DataFlow; namespace gaseous_server { - public static class GameLibrary - { + public static class GameLibrary + { // exceptions public class PathExists : Exception - { + { public PathExists(string path) : base("The library path " + path + " already exists.") - {} + { } } public class PathNotFound : Exception { public PathNotFound(string path) : base("The path " + path + " does not exist.") - {} + { } } public class LibraryNotFound : Exception { public LibraryNotFound(int LibraryId) : base("Library id " + LibraryId + " does not exist.") - {} + { } } public class CannotDeleteDefaultLibrary : Exception { public CannotDeleteDefaultLibrary() : base("Unable to delete the default library.") - {} + { } } public class CannotDeleteLibraryWhileScanIsActive : Exception { public CannotDeleteLibraryWhileScanIsActive() : base("Unable to delete library while a library scan is active. Wait for all scans to complete and try again") - {} + { } } // code @@ -66,7 +66,7 @@ namespace gaseous_server { List libraryItems = new List(); Database db = new Database(Database.databaseType.MySql, Config.DatabaseConfiguration.ConnectionString); - string sql = "SELECT * FROM GameLibraries"; + string sql = "SELECT * FROM GameLibraries ORDER BY `Name`;"; DataTable data = db.ExecuteCMD(sql); foreach (DataRow row in data.Rows) { @@ -113,7 +113,7 @@ namespace gaseous_server dbDict.Add("path", PathName); dbDict.Add("defaultplatform", DefaultPlatformId); DataTable data = db.ExecuteCMD(sql, dbDict); - + int newLibraryId = (int)(long)data.Rows[0][0]; Logging.Log(Logging.LogType.Information, "Library Management", "Created library " + Name + " at directory " + PathName); @@ -129,10 +129,10 @@ namespace gaseous_server if (library.IsDefaultLibrary == false) { // check for active library scans - foreach(ProcessQueue.QueueItem item in ProcessQueue.QueueItems) + foreach (ProcessQueue.QueueItem item in ProcessQueue.QueueItems) { if ( - (item.ItemType == ProcessQueue.QueueItemType.LibraryScan && item.ItemState == ProcessQueue.QueueItemState.Running) || + (item.ItemType == ProcessQueue.QueueItemType.LibraryScan && item.ItemState == ProcessQueue.QueueItemState.Running) || (item.ItemType == ProcessQueue.QueueItemType.LibraryScanWorker && item.ItemState == ProcessQueue.QueueItemState.Running) ) { @@ -174,7 +174,7 @@ namespace gaseous_server throw new LibraryNotFound(LibraryId); } } - + public class LibraryItem { public LibraryItem(int Id, string Name, string Path, long DefaultPlatformId, bool IsDefaultLibrary) diff --git a/gaseous-server/Classes/ImportGames.cs b/gaseous-server/Classes/ImportGames.cs index e4a1267..dd0dcfc 100644 --- a/gaseous-server/Classes/ImportGames.cs +++ b/gaseous-server/Classes/ImportGames.cs @@ -47,8 +47,11 @@ namespace gaseous_server.Classes } } - public void ImportGameFile(string GameFileImportPath, IGDB.Models.Platform? OverridePlatform) + public Dictionary ImportGameFile(string GameFileImportPath, IGDB.Models.Platform? OverridePlatform) { + Dictionary RetVal = new Dictionary(); + RetVal.Add("path", Path.GetFileName(GameFileImportPath)); + Database db = new Database(Database.databaseType.MySql, Config.DatabaseConfiguration.ConnectionString); string sql = ""; Dictionary dbDict = new Dictionary(); @@ -67,6 +70,8 @@ namespace gaseous_server.Classes if (IsBios == null) { // file is a rom + RetVal.Add("type", "rom"); + // check to make sure we don't already have this file imported sql = "SELECT COUNT(Id) AS count FROM Games_Roms WHERE MD5=@md5 AND SHA1=@sha1"; dbDict.Add("md5", hash.md5hash); @@ -94,6 +99,8 @@ namespace gaseous_server.Classes { Logging.Log(Logging.LogType.Warning, "Import Game", " " + GameFileImportPath + " already in database - skipping import"); } + + RetVal.Add("status", "duplicate"); } else { @@ -122,17 +129,27 @@ namespace gaseous_server.Classes IGDB.Models.Game determinedGame = SearchForGame(discoveredSignature, discoveredSignature.Flags.IGDBPlatformId, true); // add to database - StoreROM(GameLibrary.GetDefaultLibrary, hash, determinedGame, determinedPlatform, discoveredSignature, GameFileImportPath); + long RomId = StoreROM(GameLibrary.GetDefaultLibrary, hash, determinedGame, determinedPlatform, discoveredSignature, GameFileImportPath); + + // build return value + RetVal.Add("romid", RomId); + RetVal.Add("platform", determinedPlatform); + RetVal.Add("game", determinedGame); + RetVal.Add("signature", discoveredSignature); + RetVal.Add("status", "imported"); } } else { // file is a bios - if (IsBios.WebEmulator != null) + RetVal.Add("type", "bios"); + RetVal.Add("status", "notimported"); + + foreach (Classes.Bios.BiosItem biosItem in Classes.Bios.GetBios()) { - foreach (Classes.Bios.BiosItem biosItem in Classes.Bios.GetBios()) + if (biosItem.Available == false) { - if (biosItem.Available == false && biosItem.hash == hash.md5hash) + if (biosItem.hash == hash.md5hash) { string biosPath = biosItem.biosPath.Replace(biosItem.filename, ""); if (!Directory.Exists(biosPath)) @@ -142,12 +159,26 @@ namespace gaseous_server.Classes File.Move(GameFileImportPath, biosItem.biosPath, true); - break; + RetVal.Add("name", biosItem.filename); + RetVal.Add("platform", Platforms.GetPlatform(biosItem.platformid, false, false)); + RetVal["status"] = "imported"; + + return RetVal; + } + } + else + { + if (biosItem.hash == hash.md5hash) + { + RetVal["status"] = "duplicate"; + return RetVal; } } } } } + + return RetVal; } public static IGDB.Models.Game SearchForGame(gaseous_server.Models.Signatures_Games Signature, long PlatformId, bool FullDownload) diff --git a/gaseous-server/Classes/Logging.cs b/gaseous-server/Classes/Logging.cs index 9688e49..d18ff80 100644 --- a/gaseous-server/Classes/Logging.cs +++ b/gaseous-server/Classes/Logging.cs @@ -3,10 +3,11 @@ using System.Data; using System.Diagnostics; using System.Reflection; using System.Reflection.Metadata.Ecma335; +using Microsoft.VisualStudio.Web.CodeGenerators.Mvc.Templates.BlazorIdentity.Pages; namespace gaseous_server.Classes { - public class Logging - { + public class Logging + { private static DateTime lastDiskRetentionSweep = DateTime.UtcNow; public static bool WriteToDiskOnly { get; set; } = false; @@ -21,8 +22,13 @@ namespace gaseous_server.Classes ExceptionValue = Common.ReturnValueIfNull(ExceptionValue, "").ToString() }; + _ = Task.Run(() => WriteLogAsync(logItem, LogToDiskOnly)); + } + + static async Task WriteLogAsync(LogItem logItem, bool LogToDiskOnly) + { bool AllowWrite = false; - if (EventType == LogType.Debug) + if (logItem.EventType == LogType.Debug) { if (Config.LoggingConfiguration.DebugLogging == true) { @@ -42,26 +48,32 @@ namespace gaseous_server.Classes { TraceOutput += Environment.NewLine + logItem.ExceptionValue.ToString(); } - switch(logItem.EventType) { + string consoleColour = ""; + switch (logItem.EventType) + { case LogType.Information: - Console.ForegroundColor = ConsoleColor.Blue; + // Console.ForegroundColor = ConsoleColor.Blue; + consoleColour = "\u001b[1;34m]"; break; case LogType.Warning: - Console.ForegroundColor = ConsoleColor.Yellow; + // Console.ForegroundColor = ConsoleColor.Yellow; + consoleColour = "\u001b[1;33m]"; break; case LogType.Critical: - Console.ForegroundColor = ConsoleColor.Red; + // Console.ForegroundColor = ConsoleColor.Red; + consoleColour = "\u001b[1;31m]"; break; case LogType.Debug: - Console.ForegroundColor = ConsoleColor.Magenta; + // Console.ForegroundColor = ConsoleColor.Magenta; + consoleColour = "\u001b[1;36m]"; break; } - Console.WriteLine(TraceOutput); - Console.ResetColor(); + Console.WriteLine(consoleColour + TraceOutput); + // Console.ResetColor(); if (WriteToDiskOnly == true) { @@ -143,7 +155,7 @@ namespace gaseous_server.Classes db.ExecuteCMD(sql, dbDict); } catch (Exception ex) - { + { LogToDisk(logItem, TraceOutput, ex); } } @@ -162,9 +174,9 @@ namespace gaseous_server.Classes foreach (string file in files) { FileInfo fi = new FileInfo(file); - if (fi.LastAccessTime < DateTime.Now.AddDays(Config.LoggingConfiguration.LogRetention * -1)) - { - fi.Delete(); + if (fi.LastAccessTime < DateTime.Now.AddDays(Config.LoggingConfiguration.LogRetention * -1)) + { + fi.Delete(); } } } @@ -176,7 +188,7 @@ namespace gaseous_server.Classes { // dump the error File.AppendAllText(Config.LogFilePath, logItem.EventTime.ToString("yyyyMMdd HHmmss") + ": " + logItem.EventType.ToString() + ": " + logItem.Process + ": " + logItem.Message + Environment.NewLine + exception.ToString()); - + // something went wrong writing to the db File.AppendAllText(Config.LogFilePath, logItem.EventTime.ToString("yyyyMMdd HHmmss") + ": The following event was unable to be written to the log database:"); @@ -185,7 +197,7 @@ namespace gaseous_server.Classes File.AppendAllText(Config.LogFilePath, TraceOutput); } - static public List GetLogs(LogsViewModel model) + static public List GetLogs(LogsViewModel model) { Database db = new Database(Database.databaseType.MySql, Config.DatabaseConfiguration.ConnectionString); Dictionary dbDict = new Dictionary(); @@ -278,7 +290,7 @@ namespace gaseous_server.Classes { whereClause = "WHERE " + whereClause; } - + sql = "SELECT ServerLogs.Id, ServerLogs.EventTime, ServerLogs.EventType, ServerLogs.`Process`, ServerLogs.Message, ServerLogs.Exception, ServerLogs.CorrelationId, ServerLogs.CallingProcess, Users.Email FROM ServerLogs LEFT JOIN Users ON ServerLogs.CallingUser = Users.Id " + whereClause + " ORDER BY ServerLogs.Id DESC LIMIT @PageSize OFFSET @PageNumber;"; } else @@ -287,7 +299,7 @@ namespace gaseous_server.Classes { whereClause = "AND " + whereClause; } - + sql = "SELECT ServerLogs.Id, ServerLogs.EventTime, ServerLogs.EventType, ServerLogs.`Process`, ServerLogs.Message, ServerLogs.Exception, ServerLogs.CorrelationId, ServerLogs.CallingProcess, Users.Email FROM ServerLogs LEFT JOIN Users ON ServerLogs.CallingUser = Users.Id WHERE ServerLogs.Id < @StartIndex " + whereClause + " ORDER BY ServerLogs.Id DESC LIMIT @PageSize OFFSET @PageNumber;"; } DataTable dataTable = db.ExecuteCMD(sql, dbDict); diff --git a/gaseous-server/Classes/Maintenance.cs b/gaseous-server/Classes/Maintenance.cs index a2db20b..00b153c 100644 --- a/gaseous-server/Classes/Maintenance.cs +++ b/gaseous-server/Classes/Maintenance.cs @@ -5,7 +5,7 @@ using Microsoft.VisualStudio.Web.CodeGeneration; namespace gaseous_server.Classes { - public class Maintenance : QueueItemStatus + public class Maintenance : QueueItemStatus { const int MaxFileAge = 30; @@ -16,6 +16,7 @@ namespace gaseous_server.Classes Dictionary dbDict = new Dictionary(); // remove any entries from the library that have an invalid id + Logging.Log(Logging.LogType.Information, "Maintenance", "Removing any entries from the library that have an invalid id"); string LibraryWhereClause = ""; foreach (GameLibrary.LibraryItem library in GameLibrary.GetLibraries) { @@ -33,9 +34,29 @@ namespace gaseous_server.Classes } // delete old logs - sql = "DELETE FROM ServerLogs WHERE EventTime < @EventRetentionDate;"; + Logging.Log(Logging.LogType.Information, "Maintenance", "Removing logs older than " + Config.LoggingConfiguration.LogRetention + " days"); + long deletedCount = 1; + long deletedEventCount = 0; + long maxLoops = 10000; + sql = "DELETE FROM ServerLogs WHERE EventTime < @EventRetentionDate LIMIT 1000; SELECT ROW_COUNT() AS Count;"; dbDict.Add("EventRetentionDate", DateTime.UtcNow.AddDays(Config.LoggingConfiguration.LogRetention * -1)); - db.ExecuteCMD(sql, dbDict); + while (deletedCount > 0) + { + DataTable deletedCountTable = db.ExecuteCMD(sql, dbDict); + deletedCount = (long)deletedCountTable.Rows[0][0]; + deletedEventCount += deletedCount; + + Logging.Log(Logging.LogType.Information, "Maintenance", "Deleted " + deletedCount + " log entries"); + + // check if we've hit the limit + maxLoops -= 1; + if (maxLoops <= 0) + { + Logging.Log(Logging.LogType.Warning, "Maintenance", "Hit the maximum number of loops for deleting logs. Stopping."); + break; + } + } + Logging.Log(Logging.LogType.Information, "Maintenance", "Deleted " + deletedEventCount + " log entries"); // delete files and directories older than 7 days in PathsToClean List PathsToClean = new List(); @@ -87,7 +108,7 @@ namespace gaseous_server.Classes SetStatus(StatusCounter, tables.Rows.Count, "Optimising table " + row[0].ToString()); sql = "OPTIMIZE TABLE " + row[0].ToString(); - DataTable response = db.ExecuteCMD(sql); + DataTable response = db.ExecuteCMD(sql, new Dictionary(), 240); foreach (DataRow responseRow in response.Rows) { string retVal = ""; diff --git a/gaseous-server/Classes/Metadata/AgeGroups.cs b/gaseous-server/Classes/Metadata/AgeGroups.cs index 8dae05e..bc7e49d 100644 --- a/gaseous-server/Classes/Metadata/AgeGroups.cs +++ b/gaseous-server/Classes/Metadata/AgeGroups.cs @@ -46,7 +46,7 @@ namespace gaseous_server.Classes.Metadata default: throw new Exception("How did you get here?"); } - + return RetVal; } } @@ -73,7 +73,7 @@ namespace gaseous_server.Classes.Metadata // compile the ratings values into the ratings groups AgeRestrictionGroupings highestAgeGroup = GetAgeGroupFromAgeRatings(ageRatings); - + // return the compiled ratings group AgeGroup ageGroup = new AgeGroup(); ageGroup.Id = game.Id; @@ -86,7 +86,7 @@ namespace gaseous_server.Classes.Metadata { ageGroup.AgeGroupId = highestAgeGroup; } - + return ageGroup; } else @@ -95,7 +95,7 @@ namespace gaseous_server.Classes.Metadata ageGroup.Id = game.Id; ageGroup.GameId = game.Id; ageGroup.AgeGroupId = null; - + return ageGroup; } } @@ -105,11 +105,11 @@ namespace gaseous_server.Classes.Metadata ageGroup.Id = game.Id; ageGroup.GameId = game.Id; ageGroup.AgeGroupId = null; - + return ageGroup; } } - + return null; } @@ -121,7 +121,7 @@ namespace gaseous_server.Classes.Metadata { foreach (KeyValuePair ageGroupItem in AgeGroupingsFlat) { - + PropertyInfo[] groupProps = typeof(AgeGroupItem).GetProperties(); foreach (PropertyInfo property in groupProps) { @@ -142,7 +142,7 @@ namespace gaseous_server.Classes.Metadata } } } - + return highestAgeGroup; } @@ -158,8 +158,8 @@ namespace gaseous_server.Classes.Metadata get { return new Dictionary>{ - { - AgeRestrictionGroupings.Adult, new List{ Adult_Item, Mature_Item, Teen_Item, Child_Item } + { + AgeRestrictionGroupings.Adult, new List{ Adult_Item, Mature_Item, Teen_Item, Child_Item } }, { AgeRestrictionGroupings.Mature, new List{ Mature_Item, Teen_Item, Child_Item } @@ -167,7 +167,7 @@ namespace gaseous_server.Classes.Metadata { AgeRestrictionGroupings.Teen, new List{ Teen_Item, Child_Item } }, - { + { AgeRestrictionGroupings.Child, new List{ Child_Item } } }; @@ -216,44 +216,48 @@ namespace gaseous_server.Classes.Metadata } } - readonly static AgeGroupItem Adult_Item = new AgeGroupItem{ - ACB = new List{ AgeRatingTitle.ACB_R18, AgeRatingTitle.ACB_RC }, - CERO = new List{ AgeRatingTitle.CERO_Z }, - CLASS_IND = new List{ AgeRatingTitle.CLASS_IND_Eighteen }, - ESRB = new List{ AgeRatingTitle.RP, AgeRatingTitle.AO }, - GRAC = new List{ AgeRatingTitle.GRAC_Eighteen }, - PEGI = new List{ AgeRatingTitle.Eighteen}, - USK = new List{ AgeRatingTitle.USK_18} + readonly static AgeGroupItem Adult_Item = new AgeGroupItem + { + ACB = new List { AgeRatingTitle.ACB_R18, AgeRatingTitle.ACB_RC }, + CERO = new List { AgeRatingTitle.CERO_Z }, + CLASS_IND = new List { AgeRatingTitle.CLASS_IND_Eighteen }, + ESRB = new List { AgeRatingTitle.RP, AgeRatingTitle.AO }, + GRAC = new List { AgeRatingTitle.GRAC_Eighteen }, + PEGI = new List { AgeRatingTitle.Eighteen }, + USK = new List { AgeRatingTitle.USK_18 } }; - readonly static AgeGroupItem Mature_Item = new AgeGroupItem{ - ACB = new List{ AgeRatingTitle.ACB_M, AgeRatingTitle.ACB_MA15 }, - CERO = new List{ AgeRatingTitle.CERO_C, AgeRatingTitle.CERO_D }, - CLASS_IND = new List{ AgeRatingTitle.CLASS_IND_Sixteen }, - ESRB = new List{ AgeRatingTitle.M }, - GRAC = new List{ AgeRatingTitle.GRAC_Fifteen }, - PEGI = new List{ AgeRatingTitle.Sixteen}, - USK = new List{ AgeRatingTitle.USK_16} + readonly static AgeGroupItem Mature_Item = new AgeGroupItem + { + ACB = new List { AgeRatingTitle.ACB_M, AgeRatingTitle.ACB_MA15 }, + CERO = new List { AgeRatingTitle.CERO_C, AgeRatingTitle.CERO_D }, + CLASS_IND = new List { AgeRatingTitle.CLASS_IND_Sixteen }, + ESRB = new List { AgeRatingTitle.M }, + GRAC = new List { AgeRatingTitle.GRAC_Fifteen }, + PEGI = new List { AgeRatingTitle.Sixteen }, + USK = new List { AgeRatingTitle.USK_16 } }; - readonly static AgeGroupItem Teen_Item = new AgeGroupItem{ - ACB = new List{ AgeRatingTitle.ACB_PG }, - CERO = new List{ AgeRatingTitle.CERO_B }, - CLASS_IND = new List{ AgeRatingTitle.CLASS_IND_Twelve, AgeRatingTitle.CLASS_IND_Fourteen }, - ESRB = new List{ AgeRatingTitle.T }, - GRAC = new List{ AgeRatingTitle.GRAC_Twelve }, - PEGI = new List{ AgeRatingTitle.Twelve}, - USK = new List{ AgeRatingTitle.USK_12} + readonly static AgeGroupItem Teen_Item = new AgeGroupItem + { + ACB = new List { AgeRatingTitle.ACB_PG }, + CERO = new List { AgeRatingTitle.CERO_B }, + CLASS_IND = new List { AgeRatingTitle.CLASS_IND_Twelve, AgeRatingTitle.CLASS_IND_Fourteen }, + ESRB = new List { AgeRatingTitle.T }, + GRAC = new List { AgeRatingTitle.GRAC_Twelve }, + PEGI = new List { AgeRatingTitle.Twelve }, + USK = new List { AgeRatingTitle.USK_12 } }; - readonly static AgeGroupItem Child_Item = new AgeGroupItem{ - ACB = new List{ AgeRatingTitle.ACB_G }, - CERO = new List{ AgeRatingTitle.CERO_A }, - CLASS_IND = new List{ AgeRatingTitle.CLASS_IND_L, AgeRatingTitle.CLASS_IND_Ten }, - ESRB = new List{ AgeRatingTitle.E, AgeRatingTitle.E10 }, - GRAC = new List{ AgeRatingTitle.GRAC_All }, - PEGI = new List{ AgeRatingTitle.Three, AgeRatingTitle.Seven}, - USK = new List{ AgeRatingTitle.USK_0, AgeRatingTitle.USK_6} + readonly static AgeGroupItem Child_Item = new AgeGroupItem + { + ACB = new List { AgeRatingTitle.ACB_G }, + CERO = new List { AgeRatingTitle.CERO_A }, + CLASS_IND = new List { AgeRatingTitle.CLASS_IND_L, AgeRatingTitle.CLASS_IND_Ten }, + ESRB = new List { AgeRatingTitle.EC, AgeRatingTitle.E, AgeRatingTitle.E10 }, + GRAC = new List { AgeRatingTitle.GRAC_All }, + PEGI = new List { AgeRatingTitle.Three, AgeRatingTitle.Seven }, + USK = new List { AgeRatingTitle.USK_0, AgeRatingTitle.USK_6 } }; public class AgeGroupItem diff --git a/gaseous-server/Classes/Metadata/Artworks.cs b/gaseous-server/Classes/Metadata/Artworks.cs index ff90cb9..f93b507 100644 --- a/gaseous-server/Classes/Metadata/Artworks.cs +++ b/gaseous-server/Classes/Metadata/Artworks.cs @@ -5,7 +5,7 @@ using IGDB.Models; namespace gaseous_server.Classes.Metadata { - public class Artworks + public class Artworks { const string fieldList = "fields alpha_channel,animated,checksum,game,height,image_id,url,width;"; @@ -68,13 +68,19 @@ namespace gaseous_server.Classes.Metadata returnValue = await GetObjectFromServer(WhereClause, ImagePath); Storage.NewCacheValue(returnValue); forceImageDownload = true; - break; + break; case Storage.CacheStatus.Expired: try { returnValue = await GetObjectFromServer(WhereClause, ImagePath); Storage.NewCacheValue(returnValue, true); - forceImageDownload = true; + + // check if old value is different from the new value - only download if it's different + Artwork oldImage = Storage.GetCacheValue(returnValue, "id", (long)searchValue); + if (oldImage.ImageId != returnValue.ImageId) + { + forceImageDownload = true; + } } catch (Exception ex) { @@ -120,6 +126,6 @@ namespace gaseous_server.Classes.Metadata return result; } - } + } } diff --git a/gaseous-server/Classes/Metadata/Communications.cs b/gaseous-server/Classes/Metadata/Communications.cs index fb14572..dd0080f 100644 --- a/gaseous-server/Classes/Metadata/Communications.cs +++ b/gaseous-server/Classes/Metadata/Communications.cs @@ -191,10 +191,11 @@ namespace gaseous_server.Classes.Metadata } try - { + { if (InRateLimitAvoidanceMode == true) { // sleep for a moment to help avoid hitting the rate limiter + Logging.Log(Logging.LogType.Information, "API Connection: Endpoint:" + Endpoint, "IGDB rate limit hit. Pausing API communications for " + RateLimitAvoidanceWait + " milliseconds to avoid rate limiter."); Thread.Sleep(RateLimitAvoidanceWait); } @@ -203,7 +204,7 @@ namespace gaseous_server.Classes.Metadata // increment rate limiter avoidance call count RateLimitAvoidanceCallCount += 1; - + return results; } catch (ApiException apiEx) @@ -219,15 +220,15 @@ namespace gaseous_server.Classes.Metadata else { Logging.Log(Logging.LogType.Information, "API Connection", "IGDB API rate limit hit while accessing endpoint " + Endpoint, apiEx); - + RetryAttempts += 1; return await IGDBAPI(Endpoint, Fields, Query); } - + case HttpStatusCode.Unauthorized: Logging.Log(Logging.LogType.Information, "API Connection", "IGDB API unauthorised error while accessing endpoint " + Endpoint + ". Waiting " + RateLimitAvoidanceWait + " milliseconds and resetting IGDB client.", apiEx); - + Thread.Sleep(RateLimitAvoidanceWait); igdb = new IGDBClient( @@ -245,7 +246,7 @@ namespace gaseous_server.Classes.Metadata throw; } } - catch(Exception ex) + catch (Exception ex) { Logging.Log(Logging.LogType.Warning, "API Connection", "Exception when accessing endpoint " + Endpoint, ex); throw; @@ -260,7 +261,7 @@ namespace gaseous_server.Classes.Metadata public Task DownloadFile(Uri uri, string DestinationFile) { var result = _DownloadFile(uri, DestinationFile); - + return result; } @@ -355,7 +356,7 @@ namespace gaseous_server.Classes.Metadata int width = 0; int height = 0; - + switch (size) { case IGDBAPI_ImageSize.screenshot_small: @@ -363,7 +364,7 @@ namespace gaseous_server.Classes.Metadata width = 235; height = 128; break; - + case IGDBAPI_ImageSize.screenshot_thumb: // 165x90 width = 165; @@ -393,6 +394,7 @@ namespace gaseous_server.Classes.Metadata if (InRateLimitAvoidanceMode == true) { // sleep for a moment to help avoid hitting the rate limiter + Logging.Log(Logging.LogType.Information, "API Connection: Fetch Image", "IGDB rate limit hit. Pausing API communications for " + RateLimitAvoidanceWait + " milliseconds to avoid rate limiter."); Thread.Sleep(RateLimitAvoidanceWait); } @@ -412,7 +414,7 @@ namespace gaseous_server.Classes.Metadata { await comms.IGDBAPI_GetImage(imageSizes, ImageId, ImagePath); } - + } catch (HttpRequestException ex) { @@ -497,14 +499,14 @@ namespace gaseous_server.Classes.Metadata public async Task IGDBAPI_GetImage(List ImageSizes, string ImageId, string OutputPath) { string urlTemplate = "https://images.igdb.com/igdb/image/upload/t_{size}/{hash}.jpg"; - + foreach (IGDBAPI_ImageSize ImageSize in ImageSizes) { string url = urlTemplate.Replace("{size}", Common.GetDescription(ImageSize)).Replace("{hash}", ImageId); string newOutputPath = Path.Combine(OutputPath, Common.GetDescription(ImageSize)); string OutputFile = ImageId + ".jpg"; string fullPath = Path.Combine(newOutputPath, OutputFile); - + await _DownloadFile(new Uri(url), fullPath); } } diff --git a/gaseous-server/Classes/Metadata/CompanyLogos.cs b/gaseous-server/Classes/Metadata/CompanyLogos.cs index ac0f28d..ac14620 100644 --- a/gaseous-server/Classes/Metadata/CompanyLogos.cs +++ b/gaseous-server/Classes/Metadata/CompanyLogos.cs @@ -76,7 +76,13 @@ namespace gaseous_server.Classes.Metadata { returnValue = await GetObjectFromServer(WhereClause, ImagePath); Storage.NewCacheValue(returnValue, true); - forceImageDownload = true; + + // check if old value is different from the new value - only download if it's different + CompanyLogo oldImage = Storage.GetCacheValue(returnValue, "id", (long)searchValue); + if (oldImage.ImageId != returnValue.ImageId) + { + forceImageDownload = true; + } } catch (Exception ex) { diff --git a/gaseous-server/Classes/Metadata/Covers.cs b/gaseous-server/Classes/Metadata/Covers.cs index 08d22c3..9069f53 100644 --- a/gaseous-server/Classes/Metadata/Covers.cs +++ b/gaseous-server/Classes/Metadata/Covers.cs @@ -7,7 +7,7 @@ using Microsoft.CodeAnalysis.Elfie.Model.Strings; namespace gaseous_server.Classes.Metadata { - public class Covers + public class Covers { const string fieldList = "fields alpha_channel,animated,checksum,game,game_localization,height,image_id,url,width;"; @@ -70,13 +70,19 @@ namespace gaseous_server.Classes.Metadata returnValue = await GetObjectFromServer(WhereClause, ImagePath); Storage.NewCacheValue(returnValue); forceImageDownload = true; - break; + break; case Storage.CacheStatus.Expired: try { returnValue = await GetObjectFromServer(WhereClause, ImagePath); Storage.NewCacheValue(returnValue, true); - forceImageDownload = true; + + // check if old value is different from the new value - only download if it's different + Cover oldCover = Storage.GetCacheValue(returnValue, "id", (long)searchValue); + if (oldCover.ImageId != returnValue.ImageId) + { + forceImageDownload = true; + } } catch (Exception ex) { @@ -135,6 +141,6 @@ namespace gaseous_server.Classes.Metadata return result; } - } + } } diff --git a/gaseous-server/Classes/Metadata/Games.cs b/gaseous-server/Classes/Metadata/Games.cs index e524907..e659755 100644 --- a/gaseous-server/Classes/Metadata/Games.cs +++ b/gaseous-server/Classes/Metadata/Games.cs @@ -1,5 +1,6 @@ using System; using System.Data; +using gaseous_server.Models; using IGDB; using IGDB.Models; @@ -7,7 +8,7 @@ namespace gaseous_server.Classes.Metadata { public class Games { - const string fieldList = "fields age_ratings,aggregated_rating,aggregated_rating_count,alternative_names,artworks,bundles,category,checksum,collection,collections,cover,created_at,dlcs,expanded_games,expansions,external_games,first_release_date,follows,forks,franchise,franchises,game_engines,game_localizations,game_modes,genres,hypes,involved_companies,keywords,language_supports,multiplayer_modes,name,parent_game,platforms,player_perspectives,ports,rating,rating_count,release_dates,remakes,remasters,screenshots,similar_games,slug,standalone_expansions,status,storyline,summary,tags,themes,total_rating,total_rating_count,updated_at,url,version_parent,version_title,videos,websites;"; + const string fieldList = "fields age_ratings,aggregated_rating,aggregated_rating_count,alternative_names,artworks,bundles,category,checksum,collections,cover,created_at,dlcs,expanded_games,expansions,external_games,first_release_date,follows,forks,franchise,franchises,game_engines,game_localizations,game_modes,genres,hypes,involved_companies,keywords,language_supports,multiplayer_modes,name,parent_game,platforms,player_perspectives,ports,rating,rating_count,release_dates,remakes,remasters,screenshots,similar_games,slug,standalone_expansions,status,storyline,summary,tags,themes,total_rating,total_rating_count,updated_at,url,version_parent,version_title,videos,websites;"; public Games() { @@ -127,17 +128,17 @@ namespace gaseous_server.Classes.Metadata private static void UpdateSubClasses(Game Game, bool getAllMetadata, bool followSubGames, bool forceRefresh) { // required metadata - if (Game.Cover != null) - { - try - { - Cover GameCover = Covers.GetCover(Game.Cover.Id, Config.LibraryConfiguration.LibraryMetadataDirectory_Game(Game), forceRefresh); - } - catch (Exception ex) - { - Logging.Log(Logging.LogType.Critical, "Game Metadata", "Unable to fetch cover artwork.", ex); - } - } + // if (Game.Cover != null) + // { + // try + // { + // Cover GameCover = Covers.GetCover(Game.Cover.Id, Config.LibraryConfiguration.LibraryMetadataDirectory_Game(Game), forceRefresh); + // } + // catch (Exception ex) + // { + // Logging.Log(Logging.LogType.Critical, "Game Metadata", "Unable to fetch cover artwork.", ex); + // } + // } if (Game.Genres != null) { @@ -504,24 +505,155 @@ namespace gaseous_server.Classes.Metadata } } - public static List> GetAvailablePlatforms(long GameId) + public static List GetAvailablePlatforms(string UserId, long GameId) { Database db = new Database(Database.databaseType.MySql, Config.DatabaseConfiguration.ConnectionString); - string sql = "SELECT DISTINCT Games_Roms.PlatformId, Platform.`Name` FROM Games_Roms LEFT JOIN Platform ON Games_Roms.PlatformId = Platform.Id WHERE Games_Roms.GameId = @gameid ORDER BY Platform.`Name`;"; - Dictionary dbDict = new Dictionary(); - dbDict.Add("gameid", GameId); + string sql = @" +SELECT DISTINCT + Games_Roms.GameId, + Games_Roms.PlatformId, + Platform.`Name`, + User_RecentPlayedRoms.UserId AS MostRecentUserId, + User_RecentPlayedRoms.RomId AS MostRecentRomId, + CASE User_RecentPlayedRoms.IsMediaGroup + WHEN 0 THEN GMR.`Name` + WHEN 1 THEN 'Media Group' + ELSE NULL + END AS `MostRecentRomName`, + User_RecentPlayedRoms.IsMediaGroup AS MostRecentRomIsMediaGroup, + User_GameFavouriteRoms.UserId AS FavouriteUserId, + User_GameFavouriteRoms.RomId AS FavouriteRomId, + CASE User_GameFavouriteRoms.IsMediaGroup + WHEN 0 THEN GFV.`Name` + WHEN 1 THEN 'Media Group' + ELSE NULL + END AS `FavouriteRomName`, + User_GameFavouriteRoms.IsMediaGroup AS FavouriteRomIsMediaGroup +FROM + Games_Roms + LEFT JOIN + Platform ON Games_Roms.PlatformId = Platform.Id + LEFT JOIN + User_RecentPlayedRoms ON User_RecentPlayedRoms.UserId = @userid + AND User_RecentPlayedRoms.GameId = Games_Roms.GameId + AND User_RecentPlayedRoms.PlatformId = Games_Roms.PlatformId + LEFT JOIN + User_GameFavouriteRoms ON User_GameFavouriteRoms.UserId = @userid + AND User_GameFavouriteRoms.GameId = Games_Roms.GameId + AND User_GameFavouriteRoms.PlatformId = Games_Roms.PlatformId + LEFT JOIN + Games_Roms AS GMR ON GMR.Id = User_RecentPlayedRoms.RomId + LEFT JOIN + Games_Roms AS GFV ON GFV.Id = User_GameFavouriteRoms.RomId +WHERE + Games_Roms.GameId = @gameid +ORDER BY Platform.`Name`;"; + Dictionary dbDict = new Dictionary + { + { "gameid", GameId }, + { "userid", UserId } + }; DataTable data = db.ExecuteCMD(sql, dbDict); - List> platforms = new List>(); + PlatformMapping platformMapping = new PlatformMapping(); + List platforms = new List(); foreach (DataRow row in data.Rows) { - KeyValuePair valuePair = new KeyValuePair((long)row["PlatformId"], (string)row["Name"]); + IGDB.Models.Platform platform = Platforms.GetPlatform((long)row["PlatformId"]); + PlatformMapping.UserEmulatorConfiguration? emulatorConfiguration = platformMapping.GetUserEmulator(UserId, GameId, (long)platform.Id); + + if (emulatorConfiguration == null) + { + if (platform.Id != 0) + { + Models.PlatformMapping.PlatformMapItem platformMap = PlatformMapping.GetPlatformMap((long)platform.Id); + emulatorConfiguration = new PlatformMapping.UserEmulatorConfiguration + { + EmulatorType = platformMap.WebEmulator.Type, + Core = platformMap.WebEmulator.Core, + EnableBIOSFiles = platformMap.EnabledBIOSHashes + }; + } + } + + long? LastPlayedRomId = null; + bool? LastPlayedIsMediagroup = false; + string? LastPlayedRomName = null; + if (row["MostRecentRomId"] != DBNull.Value) + { + LastPlayedRomId = (long?)row["MostRecentRomId"]; + LastPlayedIsMediagroup = (bool)row["MostRecentRomIsMediaGroup"]; + LastPlayedRomName = (string)row["MostRecentRomName"]; + } + + long? FavouriteRomId = null; + bool? FavouriteRomIsMediagroup = false; + string? FavouriteRomName = null; + if (row["FavouriteRomId"] != DBNull.Value) + { + FavouriteRomId = (long?)row["FavouriteRomId"]; + FavouriteRomIsMediagroup = (bool)row["FavouriteRomIsMediaGroup"]; + FavouriteRomName = (string)row["FavouriteRomName"]; + } + + AvailablePlatformItem valuePair = new AvailablePlatformItem + { + Id = platform.Id, + Name = platform.Name, + Category = platform.Category, + emulatorConfiguration = emulatorConfiguration, + LastPlayedRomId = LastPlayedRomId, + LastPlayedRomIsMediagroup = LastPlayedIsMediagroup, + LastPlayedRomName = LastPlayedRomName, + FavouriteRomId = FavouriteRomId, + FavouriteRomIsMediagroup = FavouriteRomIsMediagroup, + FavouriteRomName = FavouriteRomName + }; platforms.Add(valuePair); } return platforms; } + public static void GameSetFavouriteRom(string UserId, long GameId, long PlatformId, long RomId, bool IsMediaGroup) + { + Database db = new Database(Database.databaseType.MySql, Config.DatabaseConfiguration.ConnectionString); + string sql = "DELETE FROM User_GameFavouriteRoms WHERE UserId = @userid AND GameId = @gameid AND PlatformId = @platformid; INSERT INTO User_GameFavouriteRoms (UserId, GameId, PlatformId, RomId, IsMediaGroup) VALUES (@userid, @gameid, @platformid, @romid, @ismediagroup);"; + Dictionary dbDict = new Dictionary + { + { "userid", UserId }, + { "gameid", GameId }, + { "platformid", PlatformId }, + { "romid", RomId }, + { "ismediagroup", IsMediaGroup } + }; + db.ExecuteCMD(sql, dbDict); + } + + public static void GameClearFavouriteRom(string UserId, long GameId, long PlatformId) + { + Database db = new Database(Database.databaseType.MySql, Config.DatabaseConfiguration.ConnectionString); + string sql = "DELETE FROM User_GameFavouriteRoms WHERE UserId = @userid AND GameId = @gameid AND PlatformId = @platformid;"; + Dictionary dbDict = new Dictionary + { + { "userid", UserId }, + { "gameid", GameId }, + { "platformid", PlatformId } + }; + db.ExecuteCMD(sql, dbDict); + } + + public class AvailablePlatformItem : IGDB.Models.Platform + { + public PlatformMapping.UserEmulatorConfiguration emulatorConfiguration { get; set; } + public long? LastPlayedRomId { get; set; } + public bool? LastPlayedRomIsMediagroup { get; set; } + public string? LastPlayedRomName { get; set; } + public long? FavouriteRomId { get; set; } + public bool? FavouriteRomIsMediagroup { get; set; } + public string? FavouriteRomName { get; set; } + } + public enum SearchType { where = 0, @@ -542,6 +674,7 @@ namespace gaseous_server.Classes.Metadata this.Id = gameObject.Id; this.Name = gameObject.Name; this.Slug = gameObject.Slug; + this.Summary = gameObject.Summary; this.TotalRating = gameObject.TotalRating; this.TotalRatingCount = gameObject.TotalRatingCount; this.Cover = gameObject.Cover; @@ -567,6 +700,7 @@ namespace gaseous_server.Classes.Metadata public long Index { get; set; } public string Name { get; set; } public string Slug { get; set; } + public string Summary { get; set; } public double? TotalRating { get; set; } public int? TotalRatingCount { get; set; } public bool HasSavedGame { get; set; } = false; diff --git a/gaseous-server/Classes/Metadata/PlatformLogos.cs b/gaseous-server/Classes/Metadata/PlatformLogos.cs index 0ff0604..909f7f9 100644 --- a/gaseous-server/Classes/Metadata/PlatformLogos.cs +++ b/gaseous-server/Classes/Metadata/PlatformLogos.cs @@ -76,7 +76,13 @@ namespace gaseous_server.Classes.Metadata { returnValue = await GetObjectFromServer(WhereClause, ImagePath); Storage.NewCacheValue(returnValue, true); - forceImageDownload = true; + + // check if old value is different from the new value - only download if it's different + PlatformLogo oldImage = Storage.GetCacheValue(returnValue, "id", (long)searchValue); + if (oldImage.ImageId != returnValue.ImageId) + { + forceImageDownload = true; + } } catch (Exception ex) { diff --git a/gaseous-server/Classes/Metadata/PlatformVersions.cs b/gaseous-server/Classes/Metadata/PlatformVersions.cs index 8e378b3..59722b4 100644 --- a/gaseous-server/Classes/Metadata/PlatformVersions.cs +++ b/gaseous-server/Classes/Metadata/PlatformVersions.cs @@ -5,15 +5,15 @@ using IGDB.Models; namespace gaseous_server.Classes.Metadata { - public class PlatformVersions - { + public class PlatformVersions + { const string fieldList = "fields checksum,companies,connectivity,cpu,graphics,main_manufacturer,media,memory,name,online,os,output,platform_logo,platform_version_release_dates,resolutions,slug,sound,storage,summary,url;"; public PlatformVersions() - { - } + { + } - public static PlatformVersion? GetPlatformVersion(long Id, Platform ParentPlatform) + public static PlatformVersion? GetPlatformVersion(long Id, Platform ParentPlatform, bool GetImages = false) { if (Id == 0) { @@ -21,18 +21,18 @@ namespace gaseous_server.Classes.Metadata } else { - Task RetVal = _GetPlatformVersion(SearchUsing.id, Id, ParentPlatform); + Task RetVal = _GetPlatformVersion(SearchUsing.id, Id, ParentPlatform, GetImages); return RetVal.Result; } } - public static PlatformVersion GetPlatformVersion(string Slug, Platform ParentPlatform) + public static PlatformVersion GetPlatformVersion(string Slug, Platform ParentPlatform, bool GetImages = false) { - Task RetVal = _GetPlatformVersion(SearchUsing.slug, Slug, ParentPlatform); + Task RetVal = _GetPlatformVersion(SearchUsing.slug, Slug, ParentPlatform, GetImages); return RetVal.Result; } - private static async Task _GetPlatformVersion(SearchUsing searchUsing, object searchValue, Platform ParentPlatform) + private static async Task _GetPlatformVersion(SearchUsing searchUsing, object searchValue, Platform ParentPlatform, bool GetImages) { // check database first Storage.CacheStatus? cacheStatus = new Storage.CacheStatus(); @@ -67,7 +67,7 @@ namespace gaseous_server.Classes.Metadata if (returnValue != null) { Storage.NewCacheValue(returnValue); - UpdateSubClasses(ParentPlatform, returnValue); + UpdateSubClasses(ParentPlatform, returnValue, GetImages); } return returnValue; case Storage.CacheStatus.Expired: @@ -75,7 +75,7 @@ namespace gaseous_server.Classes.Metadata { returnValue = await GetObjectFromServer(WhereClause); Storage.NewCacheValue(returnValue, true); - UpdateSubClasses(ParentPlatform, returnValue); + UpdateSubClasses(ParentPlatform, returnValue, GetImages); } catch (Exception ex) { @@ -90,17 +90,20 @@ namespace gaseous_server.Classes.Metadata } } - private static void UpdateSubClasses(Platform ParentPlatform, PlatformVersion platformVersion) + private static void UpdateSubClasses(Platform ParentPlatform, PlatformVersion platformVersion, bool GetImages) { - if (platformVersion.PlatformLogo != null) + if (GetImages == true) { - try + if (platformVersion.PlatformLogo != null) { - PlatformLogo platformLogo = PlatformLogos.GetPlatformLogo(platformVersion.PlatformLogo.Id, Path.Combine(Config.LibraryConfiguration.LibraryMetadataDirectory_Platform(ParentPlatform), "Versions", platformVersion.Slug)); - } - catch (Exception ex) - { - Logging.Log(Logging.LogType.Warning, "Platform Update", "Unable to fetch platform logo", ex); + try + { + PlatformLogo platformLogo = PlatformLogos.GetPlatformLogo(platformVersion.PlatformLogo.Id, Path.Combine(Config.LibraryConfiguration.LibraryMetadataDirectory_Platform(ParentPlatform), "Versions", platformVersion.Slug)); + } + catch (Exception ex) + { + Logging.Log(Logging.LogType.Warning, "Platform Update", "Unable to fetch platform logo", ex); + } } } } diff --git a/gaseous-server/Classes/Metadata/Platforms.cs b/gaseous-server/Classes/Metadata/Platforms.cs index 2ab8ebd..e02ffda 100644 --- a/gaseous-server/Classes/Metadata/Platforms.cs +++ b/gaseous-server/Classes/Metadata/Platforms.cs @@ -15,7 +15,7 @@ namespace gaseous_server.Classes.Metadata } - public static Platform? GetPlatform(long Id, bool forceRefresh = false) + public static Platform? GetPlatform(long Id, bool forceRefresh = false, bool GetImages = false) { if (Id == 0) { @@ -41,7 +41,7 @@ namespace gaseous_server.Classes.Metadata { try { - Task RetVal = _GetPlatform(SearchUsing.id, Id, forceRefresh); + Task RetVal = _GetPlatform(SearchUsing.id, Id, forceRefresh, GetImages); return RetVal.Result; } catch (Exception ex) @@ -52,13 +52,13 @@ namespace gaseous_server.Classes.Metadata } } - public static Platform GetPlatform(string Slug, bool forceRefresh = false) + public static Platform GetPlatform(string Slug, bool forceRefresh = false, bool GetImages = false) { - Task RetVal = _GetPlatform(SearchUsing.slug, Slug, forceRefresh); + Task RetVal = _GetPlatform(SearchUsing.slug, Slug, forceRefresh, GetImages); return RetVal.Result; } - private static async Task _GetPlatform(SearchUsing searchUsing, object searchValue, bool forceRefresh) + private static async Task _GetPlatform(SearchUsing searchUsing, object searchValue, bool forceRefresh, bool GetImages) { // check database first Storage.CacheStatus? cacheStatus = new Storage.CacheStatus(); @@ -99,7 +99,7 @@ namespace gaseous_server.Classes.Metadata case Storage.CacheStatus.NotPresent: returnValue = await GetObjectFromServer(WhereClause); Storage.NewCacheValue(returnValue); - UpdateSubClasses(returnValue); + UpdateSubClasses(returnValue, GetImages); AddPlatformMapping(returnValue); return returnValue; case Storage.CacheStatus.Expired: @@ -107,7 +107,7 @@ namespace gaseous_server.Classes.Metadata { returnValue = await GetObjectFromServer(WhereClause); Storage.NewCacheValue(returnValue, true); - UpdateSubClasses(returnValue); + UpdateSubClasses(returnValue, GetImages); AddPlatformMapping(returnValue); return returnValue; } @@ -123,7 +123,7 @@ namespace gaseous_server.Classes.Metadata } } - private static void UpdateSubClasses(Platform platform) + private static void UpdateSubClasses(Platform platform, bool GetImages) { if (platform.Versions != null) { @@ -133,15 +133,18 @@ namespace gaseous_server.Classes.Metadata } } - if (platform.PlatformLogo != null) + if (GetImages == true) { - try + if (platform.PlatformLogo != null) { - PlatformLogo platformLogo = PlatformLogos.GetPlatformLogo(platform.PlatformLogo.Id, Config.LibraryConfiguration.LibraryMetadataDirectory_Platform(platform)); - } - catch (Exception ex) - { - Logging.Log(Logging.LogType.Warning, "Platform Update", "Unable to fetch platform logo", ex); + try + { + PlatformLogo platformLogo = PlatformLogos.GetPlatformLogo(platform.PlatformLogo.Id, Config.LibraryConfiguration.LibraryMetadataDirectory_Platform(platform)); + } + catch (Exception ex) + { + Logging.Log(Logging.LogType.Warning, "Platform Update", "Unable to fetch platform logo", ex); + } } } } diff --git a/gaseous-server/Classes/Metadata/Screenshots.cs b/gaseous-server/Classes/Metadata/Screenshots.cs index ce7f456..05e5fc3 100644 --- a/gaseous-server/Classes/Metadata/Screenshots.cs +++ b/gaseous-server/Classes/Metadata/Screenshots.cs @@ -5,7 +5,7 @@ using IGDB.Models; namespace gaseous_server.Classes.Metadata { - public class Screenshots + public class Screenshots { const string fieldList = "fields alpha_channel,animated,checksum,game,height,image_id,url,width;"; @@ -68,13 +68,19 @@ namespace gaseous_server.Classes.Metadata returnValue = await GetObjectFromServer(WhereClause, ImagePath); Storage.NewCacheValue(returnValue); forceImageDownload = true; - break; + break; case Storage.CacheStatus.Expired: try { returnValue = await GetObjectFromServer(WhereClause, ImagePath); Storage.NewCacheValue(returnValue, true); - forceImageDownload = true; + + // check if old value is different from the new value - only download if it's different + Screenshot oldImage = Storage.GetCacheValue(returnValue, "id", (long)searchValue); + if (oldImage.ImageId != returnValue.ImageId) + { + forceImageDownload = true; + } } catch (Exception ex) { @@ -120,6 +126,6 @@ namespace gaseous_server.Classes.Metadata return result; } - } + } } diff --git a/gaseous-server/Classes/Metadata/Storage.cs b/gaseous-server/Classes/Metadata/Storage.cs index d95b6c0..03fdc68 100644 --- a/gaseous-server/Classes/Metadata/Storage.cs +++ b/gaseous-server/Classes/Metadata/Storage.cs @@ -7,19 +7,19 @@ using Microsoft.Extensions.Caching.Memory; namespace gaseous_server.Classes.Metadata { - public class Storage - { - public enum CacheStatus - { - NotPresent, - Current, - Expired - } + public class Storage + { + public enum CacheStatus + { + NotPresent, + Current, + Expired + } - public static CacheStatus GetCacheStatus(string Endpoint, string Slug) - { + public static CacheStatus GetCacheStatus(string Endpoint, string Slug) + { return _GetCacheStatus(Endpoint, "slug", Slug); - } + } public static CacheStatus GetCacheStatus(string Endpoint, long Id) { @@ -47,124 +47,124 @@ namespace gaseous_server.Classes.Metadata } private static CacheStatus _GetCacheStatus(string Endpoint, string SearchField, object SearchValue) - { + { Database db = new Database(Database.databaseType.MySql, Config.DatabaseConfiguration.ConnectionString); - string sql = "SELECT lastUpdated FROM " + Endpoint + " WHERE " + SearchField + " = @" + SearchField; + string sql = "SELECT lastUpdated FROM " + Endpoint + " WHERE " + SearchField + " = @" + SearchField; - Dictionary dbDict = new Dictionary(); - dbDict.Add("Endpoint", Endpoint); - dbDict.Add(SearchField, SearchValue); - - DataTable dt = db.ExecuteCMD(sql, dbDict); - if (dt.Rows.Count == 0) - { - // no data stored for this item, or lastUpdated - return CacheStatus.NotPresent; - } - else - { - DateTime CacheExpiryTime = DateTime.UtcNow.AddHours(-168); - if ((DateTime)dt.Rows[0]["lastUpdated"] < CacheExpiryTime) - { - return CacheStatus.Expired; - } - else - { - return CacheStatus.Current; - } - } + Dictionary dbDict = new Dictionary(); + dbDict.Add("Endpoint", Endpoint); + dbDict.Add(SearchField, SearchValue); + + DataTable dt = db.ExecuteCMD(sql, dbDict); + if (dt.Rows.Count == 0) + { + // no data stored for this item, or lastUpdated + return CacheStatus.NotPresent; + } + else + { + DateTime CacheExpiryTime = DateTime.UtcNow.AddHours(-168); + if ((DateTime)dt.Rows[0]["lastUpdated"] < CacheExpiryTime) + { + return CacheStatus.Expired; + } + else + { + return CacheStatus.Current; + } + } } - public static void NewCacheValue(object ObjectToCache, bool UpdateRecord = false) - { - // get the object type name - string ObjectTypeName = ObjectToCache.GetType().Name; + public static void NewCacheValue(object ObjectToCache, bool UpdateRecord = false) + { + // get the object type name + string ObjectTypeName = ObjectToCache.GetType().Name; - // build dictionary - string objectJson = Newtonsoft.Json.JsonConvert.SerializeObject(ObjectToCache); - Dictionary objectDict = Newtonsoft.Json.JsonConvert.DeserializeObject>(objectJson); + // build dictionary + string objectJson = Newtonsoft.Json.JsonConvert.SerializeObject(ObjectToCache); + Dictionary objectDict = Newtonsoft.Json.JsonConvert.DeserializeObject>(objectJson); objectDict.Add("dateAdded", DateTime.UtcNow); objectDict.Add("lastUpdated", DateTime.UtcNow); - // generate sql - string fieldList = ""; - string valueList = ""; - string updateFieldValueList = ""; - foreach (KeyValuePair key in objectDict) - { - if (fieldList.Length > 0) - { - fieldList = fieldList + ", "; - valueList = valueList + ", "; - } - fieldList = fieldList + key.Key; - valueList = valueList + "@" + key.Key; - if ((key.Key != "id") && (key.Key != "dateAdded")) + // generate sql + string fieldList = ""; + string valueList = ""; + string updateFieldValueList = ""; + foreach (KeyValuePair key in objectDict) + { + if (fieldList.Length > 0) + { + fieldList = fieldList + ", "; + valueList = valueList + ", "; + } + fieldList = fieldList + key.Key; + valueList = valueList + "@" + key.Key; + if ((key.Key != "id") && (key.Key != "dateAdded")) { if (updateFieldValueList.Length > 0) { updateFieldValueList = updateFieldValueList + ", "; } updateFieldValueList += key.Key + " = @" + key.Key; - } + } - // check property type - Type objectType = ObjectToCache.GetType(); - if (objectType != null) - { - PropertyInfo objectProperty = objectType.GetProperty(key.Key); - if (objectProperty != null) - { - string compareName = objectProperty.PropertyType.Name.ToLower().Split("`")[0]; - var objectValue = objectProperty.GetValue(ObjectToCache); - if (objectValue != null) - { - string newObjectValue; - Dictionary newDict; + // check property type + Type objectType = ObjectToCache.GetType(); + if (objectType != null) + { + PropertyInfo objectProperty = objectType.GetProperty(key.Key); + if (objectProperty != null) + { + string compareName = objectProperty.PropertyType.Name.ToLower().Split("`")[0]; + var objectValue = objectProperty.GetValue(ObjectToCache); + if (objectValue != null) + { + string newObjectValue; + Dictionary newDict; switch (compareName) - { - case "identityorvalue": + { + case "identityorvalue": newObjectValue = Newtonsoft.Json.JsonConvert.SerializeObject(objectValue); - newDict = Newtonsoft.Json.JsonConvert.DeserializeObject>(newObjectValue); + newDict = Newtonsoft.Json.JsonConvert.DeserializeObject>(newObjectValue); objectDict[key.Key] = newDict["Id"]; break; case "identitiesorvalues": newObjectValue = Newtonsoft.Json.JsonConvert.SerializeObject(objectValue); newDict = Newtonsoft.Json.JsonConvert.DeserializeObject>(newObjectValue); - newObjectValue = Newtonsoft.Json.JsonConvert.SerializeObject(newDict["Ids"]); + newObjectValue = Newtonsoft.Json.JsonConvert.SerializeObject(newDict["Ids"]); objectDict[key.Key] = newObjectValue; StoreRelations(ObjectTypeName, key.Key, (long)objectDict["Id"], newObjectValue); break; - case "int32[]": + case "int32[]": newObjectValue = Newtonsoft.Json.JsonConvert.SerializeObject(objectValue); objectDict[key.Key] = newObjectValue; break; } - } - } - } - } - - string sql = ""; - if (UpdateRecord == false) - { - sql = "INSERT INTO " + ObjectTypeName + " (" + fieldList + ") VALUES (" + valueList + ")"; + } + } + } + } + + string sql = ""; + if (UpdateRecord == false) + { + sql = "INSERT INTO " + ObjectTypeName + " (" + fieldList + ") VALUES (" + valueList + ")"; + } + else + { + sql = "UPDATE " + ObjectTypeName + " SET " + updateFieldValueList + " WHERE Id = @Id"; } - else - { - sql = "UPDATE " + ObjectTypeName + " SET " + updateFieldValueList + " WHERE Id = @Id"; - } // execute sql Database db = new Database(Database.databaseType.MySql, Config.DatabaseConfiguration.ConnectionString); - db.ExecuteCMD(sql, objectDict); + db.ExecuteCMD(sql, objectDict); } - public static T GetCacheValue(T EndpointType, string SearchField, object SearchValue) - { + public static T GetCacheValue(T EndpointType, string SearchField, object SearchValue) + { string Endpoint = EndpointType.GetType().Name; Database db = new Database(Database.databaseType.MySql, Config.DatabaseConfiguration.ConnectionString); @@ -175,23 +175,23 @@ namespace gaseous_server.Classes.Metadata dbDict.Add("Endpoint", Endpoint); dbDict.Add(SearchField, SearchValue); - DataTable dt = db.ExecuteCMD(sql, dbDict); + DataTable dt = db.ExecuteCMD(sql, dbDict, new Database.DatabaseMemoryCacheOptions(true, (int)TimeSpan.FromHours(8).Ticks)); if (dt.Rows.Count == 0) { - // no data stored for this item - throw new Exception("No record found that matches endpoint " + Endpoint + " with search value " + SearchValue); + // no data stored for this item + throw new Exception("No record found that matches endpoint " + Endpoint + " with search value " + SearchValue); } else { - DataRow dataRow = dt.Rows[0]; + DataRow dataRow = dt.Rows[0]; object returnObject = BuildCacheObject(EndpointType, dataRow); - + return (T)returnObject; } } - public static T BuildCacheObject(T EndpointType, DataRow dataRow) - { + public static T BuildCacheObject(T EndpointType, DataRow dataRow) + { foreach (PropertyInfo property in EndpointType.GetType().GetProperties()) { if (dataRow.Table.Columns.Contains(property.Name)) @@ -428,11 +428,35 @@ namespace gaseous_server.Classes.Metadata } } + public static void CreateRelationsTables() + { + string PrimaryTable = typeof(T).Name; + foreach (PropertyInfo property in typeof(T).GetProperties()) + { + string SecondaryTable = property.Name; + + if (property.PropertyType.Name == "IdentitiesOrValues`1") + { + + string TableName = "Relation_" + PrimaryTable + "_" + SecondaryTable; + Database db = new Database(Database.databaseType.MySql, Config.DatabaseConfiguration.ConnectionString); + string sql = "SELECT * FROM information_schema.tables WHERE table_schema = '" + Config.DatabaseConfiguration.DatabaseName + "' AND table_name = '" + TableName + "';"; + DataTable data = db.ExecuteCMD(sql); + if (data.Rows.Count == 0) + { + // table doesn't exist, create it + sql = "CREATE TABLE `" + Config.DatabaseConfiguration.DatabaseName + "`.`" + TableName + "` (`" + PrimaryTable + "Id` BIGINT NOT NULL, `" + SecondaryTable + "Id` BIGINT NOT NULL, PRIMARY KEY (`" + PrimaryTable + "Id`, `" + SecondaryTable + "Id`), INDEX `idx_PrimaryColumn` (`" + PrimaryTable + "Id` ASC) VISIBLE);"; + db.ExecuteCMD(sql); + } + } + } + } + private class MemoryCacheObject { public object Object { get; set; } public DateTime CreationTime { get; } = DateTime.UtcNow; - public DateTime ExpiryTime + public DateTime ExpiryTime { get { diff --git a/gaseous-server/Classes/RomMediaGroup.cs b/gaseous-server/Classes/RomMediaGroup.cs index 5fb0f52..be65c69 100644 --- a/gaseous-server/Classes/RomMediaGroup.cs +++ b/gaseous-server/Classes/RomMediaGroup.cs @@ -11,12 +11,12 @@ using gaseous_server.Models; namespace gaseous_server.Classes { - public class RomMediaGroup + public class RomMediaGroup { public class InvalidMediaGroupId : Exception - { + { public InvalidMediaGroupId(long Id) : base("Unable to find media group by id " + Id) - {} + { } } public static GameRomMediaGroupItem CreateMediaGroup(long GameId, long PlatformId, List RomIds) @@ -81,14 +81,42 @@ namespace gaseous_server.Classes } } - public static List GetMediaGroupsFromGameId(long GameId, string userid = "") + public static List GetMediaGroupsFromGameId(long GameId, string userid = "", long? PlatformId = null) { + string PlatformWhereClause = ""; + if (PlatformId != null) + { + PlatformWhereClause = " AND RomMediaGroup.PlatformId=@platformid"; + } + + string UserFields = ""; + string UserJoin = ""; + if (userid.Length > 0) + { + UserFields = ", User_RecentPlayedRoms.RomId AS MostRecentRomId, User_RecentPlayedRoms.IsMediaGroup AS MostRecentRomIsMediaGroup, User_GameFavouriteRoms.RomId AS FavouriteRomId, User_GameFavouriteRoms.IsMediaGroup AS FavouriteRomIsMediaGroup"; + UserJoin = @" + LEFT JOIN + User_RecentPlayedRoms ON User_RecentPlayedRoms.UserId = @userid + AND User_RecentPlayedRoms.GameId = RomMediaGroup.GameId + AND User_RecentPlayedRoms.PlatformId = RomMediaGroup.PlatformId + AND User_RecentPlayedRoms.RomId = RomMediaGroup.Id + AND User_RecentPlayedRoms.IsMediaGroup = 1 + LEFT JOIN + User_GameFavouriteRoms ON User_GameFavouriteRoms.UserId = @userid + AND User_GameFavouriteRoms.GameId = RomMediaGroup.GameId + AND User_GameFavouriteRoms.PlatformId = RomMediaGroup.PlatformId + AND User_GameFavouriteRoms.RomId = RomMediaGroup.Id + AND User_GameFavouriteRoms.IsMediaGroup = 1 + "; + } + Database db = new Database(Database.databaseType.MySql, Config.DatabaseConfiguration.ConnectionString); - string sql = "SELECT DISTINCT RomMediaGroup.*, GameState.RomId AS GameStateRomId FROM gaseous.RomMediaGroup LEFT JOIN GameState ON RomMediaGroup.Id = GameState.RomId AND GameState.IsMediaGroup = 1 AND GameState.UserId = @userid WHERE RomMediaGroup.GameId=@gameid;"; + string sql = "SELECT DISTINCT RomMediaGroup.*, GameState.RomId AS GameStateRomId" + UserFields + " FROM gaseous.RomMediaGroup LEFT JOIN GameState ON RomMediaGroup.Id = GameState.RomId AND GameState.IsMediaGroup = 1 AND GameState.UserId = @userid " + UserJoin + " WHERE RomMediaGroup.GameId=@gameid" + PlatformWhereClause + ";"; Dictionary dbDict = new Dictionary { { "gameid", GameId }, - { "userid", userid } + { "userid", userid }, + { "platformid", PlatformId } }; DataTable dataTable = db.ExecuteCMD(sql, dbDict); @@ -165,7 +193,7 @@ namespace gaseous_server.Classes public static void DeleteMediaGroup(long Id) { Database db = new Database(Database.databaseType.MySql, Config.DatabaseConfiguration.ConnectionString); - string sql = "DELETE FROM RomMediaGroup WHERE Id=@id; DELETE FROM GameState WHERE RomId=@id AND IsMediaGroup=1;"; + string sql = "DELETE FROM RomMediaGroup WHERE Id=@id; DELETE FROM GameState WHERE RomId=@id AND IsMediaGroup=1; UPDATE UserTimeTracking SET PlatformId = NULL, IsMediaGroup = NULL, RomId = NULL WHERE RomId=@id AND IsMediaGroup = 1;"; Dictionary dbDict = new Dictionary(); dbDict.Add("id", Id); db.ExecuteCMD(sql, dbDict); @@ -180,22 +208,42 @@ namespace gaseous_server.Classes internal static GameRomMediaGroupItem BuildMediaGroupFromRow(DataRow row) { bool hasSaveStates = false; - if (row.Table.Columns.Contains("GameStateRomId")) - { - if (row["GameStateRomId"] != DBNull.Value) - { - hasSaveStates = true; - } - } + if (row.Table.Columns.Contains("GameStateRomId")) + { + if (row["GameStateRomId"] != DBNull.Value) + { + hasSaveStates = true; + } + } - GameRomMediaGroupItem mediaGroupItem = new GameRomMediaGroupItem(); - mediaGroupItem.Id = (long)row["Id"]; - mediaGroupItem.Status = (GameRomMediaGroupItem.GroupBuildStatus)row["Status"]; - mediaGroupItem.PlatformId = (long)row["PlatformId"]; - mediaGroupItem.GameId = (long)row["GameId"]; - mediaGroupItem.RomIds = new List(); - mediaGroupItem.Roms = new List(); - mediaGroupItem.HasSaveStates = hasSaveStates; + GameRomMediaGroupItem mediaGroupItem = new GameRomMediaGroupItem + { + Id = (long)row["Id"], + Status = (GameRomMediaGroupItem.GroupBuildStatus)row["Status"], + PlatformId = (long)row["PlatformId"], + GameId = (long)row["GameId"], + RomIds = new List(), + Roms = new List(), + HasSaveStates = hasSaveStates + }; + + mediaGroupItem.RomUserLastUsed = false; + if (row.Table.Columns.Contains("MostRecentRomId")) + { + if (row["MostRecentRomId"] != DBNull.Value) + { + mediaGroupItem.RomUserLastUsed = true; + } + } + + mediaGroupItem.RomUserFavourite = false; + if (row.Table.Columns.Contains("FavouriteRomId")) + { + if (row["FavouriteRomId"] != DBNull.Value) + { + mediaGroupItem.RomUserFavourite = true; + } + } // get members Database db = new Database(Database.databaseType.MySql, Config.DatabaseConfiguration.ConnectionString); @@ -216,18 +264,6 @@ namespace gaseous_server.Classes } } - // check for a web emulator and update the romItem - foreach (Models.PlatformMapping.PlatformMapItem platformMapping in Models.PlatformMapping.PlatformMap) - { - if (platformMapping.IGDBId == mediaGroupItem.PlatformId) - { - if (platformMapping.WebEmulator != null) - { - mediaGroupItem.Emulator = platformMapping.WebEmulator; - } - } - } - return mediaGroupItem; } @@ -301,7 +337,7 @@ namespace gaseous_server.Classes if (File.Exists(rom.Path)) { string romExt = Path.GetExtension(rom.Path); - if (new string[]{ ".zip", ".rar", ".7z" }.Contains(romExt)) + if (new string[] { ".zip", ".rar", ".7z" }.Contains(romExt)) { Logging.Log(Logging.LogType.Information, "Media Group", "Decompressing ROM: " + rom.Name); @@ -511,11 +547,12 @@ namespace gaseous_server.Classes } public class GameRomMediaGroupItem - { - public long Id { get; set; } - public long GameId { get; set; } - public long PlatformId { get; set; } - public string Platform { + { + public long Id { get; set; } + public long GameId { get; set; } + public long PlatformId { get; set; } + public string Platform + { get { try @@ -528,37 +565,40 @@ namespace gaseous_server.Classes } } } - public Models.PlatformMapping.PlatformMapItem.WebEmulatorItem? Emulator { get; set; } - public List RomIds { get; set; } + public List RomIds { get; set; } public List Roms { get; set; } public bool HasSaveStates { get; set; } = false; - private GroupBuildStatus _Status { get; set; } - public GroupBuildStatus Status { - get - { - if (_Status == GroupBuildStatus.Completed) - { - if (File.Exists(MediaGroupZipPath)) - { - return GroupBuildStatus.Completed; - } - else - { - return GroupBuildStatus.NoStatus; - } - } - else - { - return _Status; - } - } + public bool RomUserLastUsed { get; set; } + public bool RomUserFavourite { get; set; } + private GroupBuildStatus _Status { get; set; } + public GroupBuildStatus Status + { + get + { + if (_Status == GroupBuildStatus.Completed) + { + if (File.Exists(MediaGroupZipPath)) + { + return GroupBuildStatus.Completed; + } + else + { + return GroupBuildStatus.NoStatus; + } + } + else + { + return _Status; + } + } set { _Status = value; } - } - public long? Size { - get + } + public long? Size + { + get { if (Status == GroupBuildStatus.Completed) { @@ -577,15 +617,15 @@ namespace gaseous_server.Classes return 0; } } - } - internal string MediaGroupZipPath - { - get - { - return Path.Combine(Config.LibraryConfiguration.LibraryMediaGroupDirectory, Id + ".zip"); - } - } - public enum GroupBuildStatus + } + internal string MediaGroupZipPath + { + get + { + return Path.Combine(Config.LibraryConfiguration.LibraryMediaGroupDirectory, Id + ".zip"); + } + } + public enum GroupBuildStatus { NoStatus = 0, WaitingForBuild = 1, @@ -593,6 +633,6 @@ namespace gaseous_server.Classes Completed = 3, Failed = 4 } - } + } } } \ No newline at end of file diff --git a/gaseous-server/Classes/Roms.cs b/gaseous-server/Classes/Roms.cs index 904b14c..4721977 100644 --- a/gaseous-server/Classes/Roms.cs +++ b/gaseous-server/Classes/Roms.cs @@ -18,6 +18,12 @@ namespace gaseous_server.Classes { } } + public class InvalidRomHash : Exception + { + public InvalidRomHash(String Hash) : base("Unable to find ROM by hash " + Hash) + { } + } + public static GameRomObject GetRoms(long GameId, long PlatformId = -1, string NameSearch = "", int pageNumber = 0, int pageSize = 0, string userid = "") { GameRomObject GameRoms = new GameRomObject(); @@ -37,13 +43,34 @@ namespace gaseous_server.Classes dbDict.Add("namesearch", '%' + NameSearch + '%'); } + string UserFields = ""; + string UserJoin = ""; + if (userid.Length > 0) + { + UserFields = ", User_RecentPlayedRoms.RomId AS MostRecentRomId, User_RecentPlayedRoms.IsMediaGroup AS MostRecentRomIsMediaGroup, User_GameFavouriteRoms.RomId AS FavouriteRomId, User_GameFavouriteRoms.IsMediaGroup AS FavouriteRomIsMediaGroup"; + UserJoin = @" + LEFT JOIN + User_RecentPlayedRoms ON User_RecentPlayedRoms.UserId = @userid + AND User_RecentPlayedRoms.GameId = Games_Roms.GameId + AND User_RecentPlayedRoms.PlatformId = Games_Roms.PlatformId + AND User_RecentPlayedRoms.RomId = Games_Roms.Id + AND User_RecentPlayedRoms.IsMediaGroup = 0 + LEFT JOIN + User_GameFavouriteRoms ON User_GameFavouriteRoms.UserId = @userid + AND User_GameFavouriteRoms.GameId = Games_Roms.GameId + AND User_GameFavouriteRoms.PlatformId = Games_Roms.PlatformId + AND User_GameFavouriteRoms.RomId = Games_Roms.Id + AND User_GameFavouriteRoms.IsMediaGroup = 0 + "; + } + // platform query sqlPlatform = "SELECT DISTINCT Games_Roms.PlatformId, Platform.`Name` FROM Games_Roms LEFT JOIN Platform ON Games_Roms.PlatformId = Platform.Id WHERE GameId = @id ORDER BY Platform.`Name`;"; if (PlatformId == -1) { // data query - sql = "SELECT DISTINCT Games_Roms.*, Platform.`Name` AS platformname, GameState.RomId AS SavedStateRomId FROM Games_Roms LEFT JOIN Platform ON Games_Roms.PlatformId = Platform.Id LEFT JOIN GameState ON (Games_Roms.Id = GameState.RomId AND GameState.UserId = @userid AND GameState.IsMediaGroup = 0) WHERE Games_Roms.GameId = @id" + NameSearchWhere + " ORDER BY Platform.`Name`, Games_Roms.`Name` LIMIT 1000;"; + sql = "SELECT DISTINCT Games_Roms.*, Platform.`Name` AS platformname, Game.`Name` AS gamename, GameState.RomId AS SavedStateRomId" + UserFields + " FROM Games_Roms LEFT JOIN Platform ON Games_Roms.PlatformId = Platform.Id LEFT JOIN Game ON Games_Roms.GameId = Game.Id LEFT JOIN GameState ON (Games_Roms.Id = GameState.RomId AND GameState.UserId = @userid AND GameState.IsMediaGroup = 0) " + UserJoin + " WHERE Games_Roms.GameId = @id" + NameSearchWhere + " ORDER BY Platform.`Name`, Games_Roms.`Name` LIMIT 1000;"; // count query sqlCount = "SELECT COUNT(Games_Roms.Id) AS RomCount FROM Games_Roms WHERE Games_Roms.GameId = @id" + NameSearchWhere + ";"; @@ -51,16 +78,15 @@ namespace gaseous_server.Classes else { // data query - sql = "SELECT DISTINCT Games_Roms.*, Platform.`Name` AS platformname, GameState.RomId AS SavedStateRomId FROM Games_Roms LEFT JOIN Platform ON Games_Roms.PlatformId = Platform.Id LEFT JOIN GameState ON (Games_Roms.Id = GameState.RomId AND GameState.UserId = @userid AND GameState.IsMediaGroup = 0) WHERE Games_Roms.GameId = @id AND Games_Roms.PlatformId = @platformid" + NameSearchWhere + " ORDER BY Platform.`Name`, Games_Roms.`Name` LIMIT 1000;"; + sql = "SELECT DISTINCT Games_Roms.*, Platform.`Name` AS platformname, Game.`Name` AS gamename, GameState.RomId AS SavedStateRomId" + UserFields + " FROM Games_Roms LEFT JOIN Platform ON Games_Roms.PlatformId = Platform.Id LEFT JOIN Game ON Games_Roms.GameId = Game.Id LEFT JOIN GameState ON (Games_Roms.Id = GameState.RomId AND GameState.UserId = @userid AND GameState.IsMediaGroup = 0) " + UserJoin + " WHERE Games_Roms.GameId = @id AND Games_Roms.PlatformId = @platformid" + NameSearchWhere + " ORDER BY Platform.`Name`, Games_Roms.`Name` LIMIT 1000;"; // count query sqlCount = "SELECT COUNT(Games_Roms.Id) AS RomCount FROM Games_Roms WHERE Games_Roms.GameId = @id AND Games_Roms.PlatformId = @platformid" + NameSearchWhere + ";"; dbDict.Add("platformid", PlatformId); } - DataTable romDT = db.ExecuteCMD(sql, dbDict); - Dictionary rowCount = db.ExecuteCMDDict(sqlCount, dbDict)[0]; - DataTable platformDT = db.ExecuteCMD(sqlPlatform, dbDict); + DataTable romDT = db.ExecuteCMD(sql, dbDict, new Database.DatabaseMemoryCacheOptions(true, (int)TimeSpan.FromMinutes(1).Ticks)); + Dictionary rowCount = db.ExecuteCMDDict(sqlCount, dbDict, new Database.DatabaseMemoryCacheOptions(true, (int)TimeSpan.FromMinutes(1).Ticks))[0]; if (romDT.Rows.Count > 0) { @@ -70,10 +96,9 @@ namespace gaseous_server.Classes int pageOffset = pageSize * (pageNumber - 1); for (int i = 0; i < romDT.Rows.Count; i++) { - GameRomItem gameRomItem = BuildRom(romDT.Rows[i]); - if ((i >= pageOffset && i < pageOffset + pageSize) || pageSize == 0) { + GameRomItem gameRomItem = BuildRom(romDT.Rows[i]); GameRoms.GameRomItems.Add(gameRomItem); } } @@ -89,7 +114,7 @@ namespace gaseous_server.Classes public static GameRomItem GetRom(long RomId) { Database db = new Database(Database.databaseType.MySql, Config.DatabaseConfiguration.ConnectionString); - string sql = "SELECT Games_Roms.*, Platform.`Name` AS platformname FROM Games_Roms LEFT JOIN Platform ON Games_Roms.PlatformId = Platform.Id WHERE Games_Roms.Id = @id"; + string sql = "SELECT Games_Roms.*, Platform.`Name` AS platformname, Game.`Name` AS gamename FROM Games_Roms LEFT JOIN Platform ON Games_Roms.PlatformId = Platform.Id LEFT JOIN Game ON Games_Roms.GameId = Game.Id WHERE Games_Roms.Id = @id"; Dictionary dbDict = new Dictionary(); dbDict.Add("id", RomId); DataTable romDT = db.ExecuteCMD(sql, dbDict); @@ -106,6 +131,26 @@ namespace gaseous_server.Classes } } + public static GameRomItem GetRom(string MD5) + { + Database db = new Database(Database.databaseType.MySql, Config.DatabaseConfiguration.ConnectionString); + string sql = "SELECT Games_Roms.*, Platform.`Name` AS platformname, Game.`Name` AS gamename FROM Games_Roms LEFT JOIN Platform ON Games_Roms.PlatformId = Platform.Id LEFT JOIN Game ON Games_Roms.GameId = Game.Id WHERE Games_Roms.MD5 = @id"; + Dictionary dbDict = new Dictionary(); + dbDict.Add("id", MD5); + DataTable romDT = db.ExecuteCMD(sql, dbDict); + + if (romDT.Rows.Count > 0) + { + DataRow romDR = romDT.Rows[0]; + GameRomItem romItem = BuildRom(romDR); + return romItem; + } + else + { + throw new InvalidRomHash(MD5); + } + } + public static GameRomItem UpdateRom(long RomId, long PlatformId, long GameId) { // ensure metadata for platformid is present @@ -185,7 +230,7 @@ namespace gaseous_server.Classes } Database db = new Database(Database.databaseType.MySql, Config.DatabaseConfiguration.ConnectionString); - string sql = "DELETE FROM Games_Roms WHERE Id = @id; DELETE FROM GameState WHERE RomId = @id;"; + string sql = "DELETE FROM Games_Roms WHERE Id = @id; DELETE FROM GameState WHERE RomId = @id; UPDATE UserTimeTracking SET PlatformId = NULL, IsMediaGroup = NULL, RomId = NULL WHERE RomId = @id AND IsMediaGroup = 0;"; Dictionary dbDict = new Dictionary(); dbDict.Add("id", RomId); db.ExecuteCMD(sql, dbDict); @@ -225,6 +270,7 @@ namespace gaseous_server.Classes PlatformId = (long)romDR["platformid"], Platform = (string)romDR["platformname"], GameId = (long)romDR["gameid"], + Game = (string)romDR["gamename"], Name = (string)romDR["name"], Size = (long)romDR["size"], Crc = ((string)romDR["crc"]).ToLower(), @@ -242,15 +288,21 @@ namespace gaseous_server.Classes Library = GameLibrary.GetLibrary((int)romDR["LibraryId"]) }; - // check for a web emulator and update the romItem - foreach (Models.PlatformMapping.PlatformMapItem platformMapping in Models.PlatformMapping.PlatformMap) + romItem.RomUserLastUsed = false; + if (romDR.Table.Columns.Contains("MostRecentRomId")) { - if (platformMapping.IGDBId == romItem.PlatformId) + if (romDR["MostRecentRomId"] != DBNull.Value) { - if (platformMapping.WebEmulator != null) - { - romItem.Emulator = platformMapping.WebEmulator; - } + romItem.RomUserLastUsed = true; + } + } + + romItem.RomUserFavourite = false; + if (romDR.Table.Columns.Contains("FavouriteRomId")) + { + if (romDR["FavouriteRomId"] != DBNull.Value) + { + romItem.RomUserFavourite = true; } } @@ -267,12 +319,14 @@ namespace gaseous_server.Classes { public long PlatformId { get; set; } public string Platform { get; set; } - public Models.PlatformMapping.PlatformMapItem.WebEmulatorItem? Emulator { get; set; } public long GameId { get; set; } + public string Game { get; set; } public string? Path { get; set; } public string? SignatureSourceGameTitle { get; set; } public bool HasSaveStates { get; set; } = false; public GameLibrary.LibraryItem Library { get; set; } + public bool RomUserLastUsed { get; set; } + public bool RomUserFavourite { get; set; } } } } diff --git a/gaseous-server/Classes/SignatureManagement.cs b/gaseous-server/Classes/SignatureManagement.cs index cad5ebc..3ecba94 100644 --- a/gaseous-server/Classes/SignatureManagement.cs +++ b/gaseous-server/Classes/SignatureManagement.cs @@ -1,4 +1,5 @@ using System.Data; +using gaseous_server.Models; using gaseous_signature_parser.models.RomSignatureObject; using static gaseous_server.Classes.Common; @@ -81,6 +82,47 @@ namespace gaseous_server.Classes return GamesList; } + public List GetSources() + { + Database db = new Database(Database.databaseType.MySql, Config.DatabaseConfiguration.ConnectionString); + string sql = "SELECT * FROM Signatures_Sources ORDER BY `SourceType`, `Name`;"; + DataTable sigDb = db.ExecuteCMD(sql); + + List SourcesList = new List(); + + foreach (DataRow sigDbRow in sigDb.Rows) + { + Signatures_Sources sourceItem = new Signatures_Sources + { + Id = (int)sigDbRow["Id"], + Name = (string)sigDbRow["Name"], + Description = (string)sigDbRow["Description"], + URL = (string)sigDbRow["URL"], + Category = (string)sigDbRow["Category"], + Version = (string)sigDbRow["Version"], + Author = (string)sigDbRow["Author"], + Email = (string)sigDbRow["Email"], + Homepage = (string)sigDbRow["Homepage"], + SourceType = (gaseous_signature_parser.parser.SignatureParser)Enum.Parse(typeof(gaseous_signature_parser.parser.SignatureParser), sigDbRow["SourceType"].ToString()), + MD5 = (string)sigDbRow["SourceMD5"], + SHA1 = (string)sigDbRow["SourceSHA1"] + }; + SourcesList.Add(sourceItem); + } + return SourcesList; + } + + public void DeleteSource(int sourceId) + { + Database db = new Database(Database.databaseType.MySql, Config.DatabaseConfiguration.ConnectionString); + string sql = "DELETE FROM Signatures_Sources WHERE Id = @sourceId;"; + Dictionary dbDict = new Dictionary + { + { "sourceId", sourceId } + }; + db.ExecuteCMD(sql, dbDict); + } + public Dictionary GetLookup(LookupTypes LookupType, long GameId) { string tableName = ""; diff --git a/gaseous-server/Classes/Statistics.cs b/gaseous-server/Classes/Statistics.cs index 495d1ac..9ee10d4 100644 --- a/gaseous-server/Classes/Statistics.cs +++ b/gaseous-server/Classes/Statistics.cs @@ -5,29 +5,45 @@ namespace gaseous_server.Classes { public class Statistics { - public StatisticsModel RecordSession(Guid SessionId, long GameId, string UserId) + public StatisticsModel RecordSession(Guid SessionId, long GameId, long PlatformId, long RomId, bool IsMediaGroup, string UserId) { Database db = new Database(Database.databaseType.MySql, Config.DatabaseConfiguration.ConnectionString); string sql; - Dictionary dbDict; + Dictionary dbDict = new Dictionary{ + { "userid", UserId }, + { "gameid", GameId }, + { "platformid", PlatformId }, + { "romid", RomId }, + { "ismediagroup", IsMediaGroup } + }; + + // update last played rom id + sql = "INSERT INTO User_RecentPlayedRoms (UserId, GameId, PlatformId, RomId, IsMediaGroup) VALUES (@userid, @gameid, @platformid, @romid, @ismediagroup) ON DUPLICATE KEY UPDATE RomId = @romid, IsMediaGroup = @ismediagroup;"; + db.ExecuteNonQuery(sql, dbDict); + + // update sessions if (SessionId == Guid.Empty) { // new session required SessionId = Guid.NewGuid(); - sql = "INSERT INTO UserTimeTracking (GameId, UserId, SessionId, SessionTime, SessionLength) VALUES (@gameid, @userid, @sessionid, @sessiontime, @sessionlength);"; + sql = "INSERT INTO UserTimeTracking (GameId, UserId, SessionId, SessionTime, SessionLength, PlatformId, IsMediaGroup, RomId) VALUES (@gameid, @userid, @sessionid, @sessiontime, @sessionlength, @platformid, @ismediagroup, @romid);"; dbDict = new Dictionary{ { "gameid", GameId }, { "userid", UserId }, { "sessionid", SessionId }, { "sessiontime", DateTime.UtcNow }, - { "sessionlength", 1 } + { "sessionlength", 1 }, + { "platformid", PlatformId }, + { "ismediagroup", IsMediaGroup }, + { "romid", RomId } }; db.ExecuteNonQuery(sql, dbDict); - - return new StatisticsModel{ + + return new StatisticsModel + { GameId = GameId, SessionId = SessionId, SessionStart = (DateTime)dbDict["sessiontime"], @@ -50,7 +66,8 @@ namespace gaseous_server.Classes sql = "SELECT * FROM UserTimeTracking WHERE GameId = @gameid AND UserId = @userid AND SessionId = @sessionid;"; DataTable data = db.ExecuteCMD(sql, dbDict); - return new StatisticsModel{ + return new StatisticsModel + { GameId = (long)data.Rows[0]["GameId"], SessionId = Guid.Parse(data.Rows[0]["SessionId"].ToString()), SessionStart = (DateTime)data.Rows[0]["SessionTime"], @@ -87,7 +104,8 @@ namespace gaseous_server.Classes sql = "SELECT * FROM UserTimeTracking WHERE GameId = @gameid AND UserId = @userid ORDER BY SessionTime DESC LIMIT 1;"; data = db.ExecuteCMD(sql, dbDict); - return new StatisticsModel{ + return new StatisticsModel + { GameId = GameId, SessionLength = TotalTime, SessionStart = (DateTime)data.Rows[0]["SessionTime"] diff --git a/gaseous-server/Classes/UserProfile.cs b/gaseous-server/Classes/UserProfile.cs new file mode 100644 index 0000000..c38a0a4 --- /dev/null +++ b/gaseous-server/Classes/UserProfile.cs @@ -0,0 +1,187 @@ +using System.Data; + +namespace gaseous_server.Classes +{ + public class UserProfile + { + static readonly Dictionary supportedImages = new Dictionary{ + { ".png", "image/png" }, + { ".jpg", "image/jpeg" }, + { ".jpeg", "image/jpeg" }, + { ".gif", "image/gif" }, + { ".bmp", "image/bmp" }, + { ".svg", "image/svg+xml" } + }; + + public Models.UserProfile? GetUserProfile(string UserId) + { + Database db = new Database(Database.databaseType.MySql, Config.DatabaseConfiguration.ConnectionString); + string sql = "SELECT Id, DisplayName, Quip, AvatarExtension, ProfileBackgroundExtension, UnstructuredData FROM UserProfiles WHERE Id = @userid;"; + Dictionary dbDict = new Dictionary{ + { "userid", UserId } + }; + + DataTable data = db.ExecuteCMD(sql, dbDict); + + if (data.Rows.Count == 0) + { + return null; + } + + Models.UserProfile.ProfileImageItem? Avatar = null; + if (data.Rows[0]["AvatarExtension"] != DBNull.Value) + { + Avatar = new Models.UserProfile.ProfileImageItem + { + MimeType = supportedImages[data.Rows[0]["AvatarExtension"].ToString()], + Extension = data.Rows[0]["AvatarExtension"].ToString() + }; + } + + Models.UserProfile.ProfileImageItem? ProfileBackground = null; + if (data.Rows[0]["ProfileBackgroundExtension"] != DBNull.Value) + { + ProfileBackground = new Models.UserProfile.ProfileImageItem + { + MimeType = supportedImages[data.Rows[0]["ProfileBackgroundExtension"].ToString()], + Extension = data.Rows[0]["ProfileBackgroundExtension"].ToString() + }; + } + + return new Models.UserProfile + { + UserId = Guid.Parse(data.Rows[0]["Id"].ToString()), + DisplayName = data.Rows[0]["DisplayName"].ToString(), + Quip = data.Rows[0]["Quip"].ToString(), + Avatar = Avatar, + ProfileBackground = ProfileBackground, + Data = Newtonsoft.Json.JsonConvert.DeserializeObject>(data.Rows[0]["UnstructuredData"].ToString()) + }; + } + + public void UpdateUserProfile(string InternalUserId, Models.UserProfile profile) + { + Database db = new Database(Database.databaseType.MySql, Config.DatabaseConfiguration.ConnectionString); + string sql = "UPDATE UserProfiles SET DisplayName = @displayname, Quip = @quip, UnstructuredData = @data WHERE UserId = @internalId AND Id = @userid;"; + Dictionary dbDict = new Dictionary{ + { "displayname", profile.DisplayName }, + { "quip", profile.Quip }, + { "data", Newtonsoft.Json.JsonConvert.SerializeObject(profile.Data) }, + { "userid", profile.UserId }, + { "internalId", InternalUserId } + }; + + db.ExecuteCMD(sql, dbDict); + } + + public enum ImageType + { + Avatar, + Background + } + + public void UpdateImage(ImageType imageType, string UserId, string InternalUserId, string Filename, byte[] bytes) + { + // check if it's a supported file type + if (!supportedImages.ContainsKey(Path.GetExtension(Filename).ToLower())) + { + throw new Exception("File type not supported"); + } + + string ByteFieldName; + string ExtensionFieldName; + switch (imageType) + { + case ImageType.Avatar: + ByteFieldName = "Avatar"; + ExtensionFieldName = "AvatarExtension"; + break; + case ImageType.Background: + ByteFieldName = "ProfileBackground"; + ExtensionFieldName = "ProfileBackgroundExtension"; + break; + default: + throw new Exception("Invalid image type"); + } + + Database db = new Database(Database.databaseType.MySql, Config.DatabaseConfiguration.ConnectionString); + string sql = String.Format("UPDATE UserProfiles SET {0} = @content, {1} = @extension WHERE Id = @userid AND UserId = @internaluserid;", ByteFieldName, ExtensionFieldName); + Dictionary dbDict = new Dictionary{ + { "content", bytes }, + { "extension", Path.GetExtension(Filename) }, + { "userid", UserId }, + { "internaluserid", InternalUserId } + }; + + db.ExecuteCMD(sql, dbDict); + } + + public Models.ImageItem? GetImage(ImageType imageType, string UserId) + { + string ByteFieldName; + string ExtensionFieldName; + switch (imageType) + { + case ImageType.Avatar: + ByteFieldName = "Avatar"; + ExtensionFieldName = "AvatarExtension"; + break; + case ImageType.Background: + ByteFieldName = "ProfileBackground"; + ExtensionFieldName = "ProfileBackgroundExtension"; + break; + default: + throw new Exception("Invalid image type"); + } + + Database db = new Database(Database.databaseType.MySql, Config.DatabaseConfiguration.ConnectionString); + string sql = String.Format("SELECT {0}, {1} FROM UserProfiles WHERE Id = @userid;", ByteFieldName, ExtensionFieldName); + Dictionary dbDict = new Dictionary{ + { "userid", UserId } + }; + + DataTable data = db.ExecuteCMD(sql, dbDict); + + if (data.Rows.Count == 0) + { + return null; + } + + Models.ImageItem? image = new Models.ImageItem + { + content = data.Rows[0][ByteFieldName] as byte[], + mimeType = supportedImages[data.Rows[0][ExtensionFieldName] as string], + extension = data.Rows[0][ExtensionFieldName] as string + }; + + return image; + } + + public void DeleteImage(ImageType imageType, string UserId) + { + string ByteFieldName; + string ExtensionFieldName; + switch (imageType) + { + case ImageType.Avatar: + ByteFieldName = "Avatar"; + ExtensionFieldName = "AvatarExtension"; + break; + case ImageType.Background: + ByteFieldName = "ProfileBackground"; + ExtensionFieldName = "ProfileBackgroundExtension"; + break; + default: + throw new Exception("Invalid image type"); + } + + Database db = new Database(Database.databaseType.MySql, Config.DatabaseConfiguration.ConnectionString); + string sql = String.Format("UPDATE UserProfiles SET {0} = NULL, {1} = NULL WHERE UserId = @userid;", ByteFieldName, ExtensionFieldName); + Dictionary dbDict = new Dictionary{ + { "userid", UserId } + }; + + db.ExecuteCMD(sql, dbDict); + } + } +} diff --git a/gaseous-server/Controllers/V1.0/AccountController.cs b/gaseous-server/Controllers/V1.0/AccountController.cs index 92a23f7..111ede9 100644 --- a/gaseous-server/Controllers/V1.0/AccountController.cs +++ b/gaseous-server/Controllers/V1.0/AccountController.cs @@ -99,7 +99,7 @@ namespace gaseous_server.Controllers profile.Roles = new List(await _userManager.GetRolesAsync(user)); profile.SecurityProfile = user.SecurityProfile; profile.UserPreferences = user.UserPreferences; - profile.Avatar = user.Avatar; + profile.ProfileId = user.ProfileId; profile.Roles.Sort(); return Ok(profile); @@ -122,7 +122,7 @@ namespace gaseous_server.Controllers profile.SecurityProfile = user.SecurityProfile; profile.UserPreferences = user.UserPreferences; profile.Roles.Sort(); - + string profileString = "var userProfile = " + Newtonsoft.Json.JsonConvert.SerializeObject(profile, Newtonsoft.Json.Formatting.Indented) + ";"; byte[] bytes = Encoding.UTF8.GetBytes(profileString); @@ -188,8 +188,8 @@ namespace gaseous_server.Controllers user.LockoutEnabled = rawUser.LockoutEnabled; user.LockoutEnd = rawUser.LockoutEnd; user.SecurityProfile = rawUser.SecurityProfile; - user.Avatar = rawUser.Avatar; - + user.ProfileId = rawUser.ProfileId; + // get roles ApplicationUser? aUser = await _userManager.FindByIdAsync(rawUser.Id); if (aUser != null) @@ -214,12 +214,16 @@ namespace gaseous_server.Controllers if (ModelState.IsValid) { ApplicationUser user = new ApplicationUser - { - UserName = model.UserName, + { + UserName = model.UserName, NormalizedUserName = model.UserName.ToUpper(), Email = model.Email, NormalizedEmail = model.Email.ToUpper() }; + if (await _userManager.FindByEmailAsync(model.Email) != null) + { + return NotFound("User already exists"); + } var result = await _userManager.CreateAsync(user, model.Password); if (result.Succeeded) { @@ -234,20 +238,37 @@ namespace gaseous_server.Controllers { return Ok(result); } - } + } else { return NotFound(); } } + [HttpGet] + [Route("Users/Test")] + [Authorize(Roles = "Admin")] + public async Task TestUserExists(string Email) + { + ApplicationUser? rawUser = await _userManager.FindByEmailAsync(Email); + + if (rawUser != null) + { + return Ok(true); + } + else + { + return Ok(false); + } + } + [HttpGet] [Route("Users/{UserId}")] [Authorize(Roles = "Admin")] public async Task GetUser(string UserId) { ApplicationUser? rawUser = await _userManager.FindByIdAsync(UserId); - + if (rawUser != null) { UserViewModel user = new UserViewModel(); @@ -256,7 +277,8 @@ namespace gaseous_server.Controllers user.LockoutEnabled = rawUser.LockoutEnabled; user.LockoutEnd = rawUser.LockoutEnd; user.SecurityProfile = rawUser.SecurityProfile; - + user.ProfileId = rawUser.ProfileId; + // get roles IList aUserRoles = await _userManager.GetRolesAsync(rawUser); user.Roles = aUserRoles.ToList(); @@ -297,16 +319,16 @@ namespace gaseous_server.Controllers public async Task SetUserRoles(string UserId, string RoleName) { ApplicationUser? user = await _userManager.FindByIdAsync(UserId); - + if (user != null) { // get roles List userRoles = (await _userManager.GetRolesAsync(user)).ToList(); - + // delete all roles foreach (string role in userRoles) { - if ((new string[] { "Admin", "Gamer", "Player" }).Contains(role) ) + if ((new string[] { "Admin", "Gamer", "Player" }).Contains(role)) { await _userManager.RemoveFromRoleAsync(user, role); } @@ -331,7 +353,7 @@ namespace gaseous_server.Controllers await _userManager.AddToRoleAsync(user, RoleName); break; } - + return Ok(); } else @@ -348,12 +370,12 @@ namespace gaseous_server.Controllers if (ModelState.IsValid) { ApplicationUser? user = await _userManager.FindByIdAsync(UserId); - + if (user != null) { user.SecurityProfile = securityProfile; await _userManager.UpdateAsync(user); - + return Ok(); } else @@ -413,7 +435,7 @@ namespace gaseous_server.Controllers { user.UserPreferences = model; await _userManager.UpdateAsync(user); - + return Ok(); } } diff --git a/gaseous-server/Controllers/V1.0/BiosController.cs b/gaseous-server/Controllers/V1.0/BiosController.cs index 6dc01f8..cf777d3 100644 --- a/gaseous-server/Controllers/V1.0/BiosController.cs +++ b/gaseous-server/Controllers/V1.0/BiosController.cs @@ -7,6 +7,9 @@ using gaseous_server.Classes; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Asp.Versioning; +using Authentication; +using Microsoft.AspNetCore.Identity; +using gaseous_server.Models; namespace gaseous_server.Controllers { @@ -17,6 +20,15 @@ namespace gaseous_server.Controllers [Authorize] public class BiosController : Controller { + private readonly UserManager _userManager; + private readonly SignInManager _signInManager; + + public BiosController(UserManager userManager, SignInManager signInManager) + { + _userManager = userManager; + _signInManager = signInManager; + } + [MapToApiVersion("1.0")] [MapToApiVersion("1.1")] [HttpGet] @@ -43,29 +55,59 @@ namespace gaseous_server.Controllers [MapToApiVersion("1.1")] [HttpHead] [Route("zip/{PlatformId}")] + [Route("zip/{PlatformId}/{GameId}")] [ProducesResponseType(typeof(FileStreamResult), StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] - public ActionResult GetBiosCompressed(long PlatformId) + public async Task GetBiosCompressedAsync(long PlatformId, long GameId = -1, bool filtered = false) { try { - IGDB.Models.Platform platform = Classes.Metadata.Platforms.GetPlatform(PlatformId); - - string biosPath = Path.Combine(Config.LibraryConfiguration.LibraryBIOSDirectory, platform.Slug); - - string tempFile = Path.GetTempFileName(); - - using (FileStream zipFile = System.IO.File.Create(tempFile)) - using (var zipArchive = new ZipArchive(zipFile, ZipArchiveMode.Create)) + if (GameId == -1 || filtered == false) { - foreach (string file in Directory.GetFiles(biosPath)) - { - zipArchive.CreateEntryFromFile(file, Path.GetFileName(file)); - } - } + IGDB.Models.Platform platform = Classes.Metadata.Platforms.GetPlatform(PlatformId); - var stream = new FileStream(tempFile, FileMode.Open); - return File(stream, "application/zip", platform.Slug + ".zip"); + string biosPath = Path.Combine(Config.LibraryConfiguration.LibraryBIOSDirectory, platform.Slug); + + string tempFile = Path.GetTempFileName(); + + using (FileStream zipFile = System.IO.File.Create(tempFile)) + using (var zipArchive = new ZipArchive(zipFile, ZipArchiveMode.Create)) + { + foreach (string file in Directory.GetFiles(biosPath)) + { + zipArchive.CreateEntryFromFile(file, Path.GetFileName(file)); + } + } + + var stream = new FileStream(tempFile, FileMode.Open); + return File(stream, "application/zip", platform.Slug + ".zip"); + } + else + { + // get user platform map + var user = await _userManager.GetUserAsync(User); + + PlatformMapping platformMapping = new PlatformMapping(); + PlatformMapping.PlatformMapItem userPlatformMap = platformMapping.GetUserPlatformMap(user.Id, PlatformId, GameId); + + // build zip file + string tempFile = Path.GetTempFileName(); + + using (FileStream zipFile = System.IO.File.Create(tempFile)) + using (var zipArchive = new ZipArchive(zipFile, ZipArchiveMode.Create)) + { + foreach (Bios.BiosItem bios in GetBios(PlatformId, true)) + { + if (userPlatformMap.EnabledBIOSHashes.Contains(bios.hash)) + { + zipArchive.CreateEntryFromFile(bios.biosPath, bios.filename); + } + } + } + + var stream = new FileStream(tempFile, FileMode.Open); + return File(stream, "application/zip", userPlatformMap.IGDBSlug + ".zip"); + } } catch { diff --git a/gaseous-server/Controllers/V1.0/GamesController.cs b/gaseous-server/Controllers/V1.0/GamesController.cs index 2482dcc..2b4c08f 100644 --- a/gaseous-server/Controllers/V1.0/GamesController.cs +++ b/gaseous-server/Controllers/V1.0/GamesController.cs @@ -18,6 +18,7 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.CodeAnalysis.Scripting; using static gaseous_server.Classes.Metadata.AgeRatings; using Asp.Versioning; +using static gaseous_server.Models.PlatformMapping; namespace gaseous_server.Controllers { @@ -30,7 +31,7 @@ namespace gaseous_server.Controllers { private readonly UserManager _userManager; private readonly SignInManager _signInManager; - + public GamesController( UserManager userManager, SignInManager signInManager @@ -53,7 +54,7 @@ namespace gaseous_server.Controllers int minrating = -1, int maxrating = -1, bool sortdescending = false) - { + { return Ok(GetGames(name, platform, genre, gamemode, playerperspective, theme, minrating, maxrating, "Adult", true, true, sortdescending)); } @@ -457,73 +458,6 @@ namespace gaseous_server.Controllers } } - [MapToApiVersion("1.0")] - [MapToApiVersion("1.1")] - [HttpGet] - [Route("{GameId}/artwork/{ArtworkId}/image/{size}")] - [Route("{GameId}/artwork/{ArtworkId}/image/{size}/{ImageName}")] - [ProducesResponseType(typeof(FileStreamResult), StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task GameCoverImage(long GameId, long ArtworkId, Communications.IGDBAPI_ImageSize size, string ImageName) - { - try - { - IGDB.Models.Game gameObject = Classes.Metadata.Games.GetGame(GameId, false, false, false); - - try - { - IGDB.Models.Artwork artworkObject = Artworks.GetArtwork(ArtworkId, Config.LibraryConfiguration.LibraryMetadataDirectory_Game(gameObject), true); - - if (artworkObject != null) { - //string coverFilePath = Path.Combine(Config.LibraryConfiguration.LibraryMetadataDirectory_Game(gameObject), "Artwork", size.ToString(), artworkObject.ImageId + ".jpg"); - - string basePath = Path.Combine(Config.LibraryConfiguration.LibraryMetadataDirectory_Game(gameObject), "Artwork"); - - Communications comms = new Communications(); - Task ImgFetch = comms.GetSpecificImageFromServer(basePath, artworkObject.ImageId, size, new List{ Communications.IGDBAPI_ImageSize.original }); - - string coverFilePath = ImgFetch.Result; - - - if (System.IO.File.Exists(coverFilePath)) - { - string filename = artworkObject.ImageId + ".jpg"; - string filepath = coverFilePath; - byte[] filedata = System.IO.File.ReadAllBytes(filepath); - string contentType = "image/jpg"; - - var cd = new System.Net.Mime.ContentDisposition - { - FileName = filename, - Inline = true, - }; - - Response.Headers.Add("Content-Disposition", cd.ToString()); - Response.Headers.Add("Cache-Control", "public, max-age=604800"); - - return File(filedata, contentType); - } - else - { - return NotFound(); - } - } - else - { - return NotFound(); - } - } - catch - { - return NotFound(); - } - } - catch - { - return NotFound(); - } - } - [MapToApiVersion("1.0")] [MapToApiVersion("1.1")] [HttpGet] @@ -562,47 +496,126 @@ namespace gaseous_server.Controllers [MapToApiVersion("1.0")] [MapToApiVersion("1.1")] [HttpGet] - [Route("{GameId}/cover/image/{size}")] - [Route("{GameId}/cover/image/{size}/{imagename}")] + [Route("{GameId}/{ImageType}/{ImageId}/image/{size}")] + [Route("{GameId}/{ImageType}/{ImageId}/image/{size}/{imagename}")] [ProducesResponseType(typeof(FileStreamResult), StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task GameCoverImage(long GameId, Communications.IGDBAPI_ImageSize size, string imagename = "") + public async Task GameImage(long GameId, MetadataImageType imageType, long ImageId, Communications.IGDBAPI_ImageSize size, string imagename = "") { try { IGDB.Models.Game gameObject = Classes.Metadata.Games.GetGame(GameId, false, false, false); - if (gameObject.Cover != null) + string? imageId = null; + string? imageTypePath = null; + + switch (imageType) { - if (gameObject.Cover.Id != null) - { - IGDB.Models.Cover cover = Classes.Metadata.Covers.GetCover(gameObject.Cover.Id, Config.LibraryConfiguration.LibraryMetadataDirectory_Game(gameObject), false); - string basePath = Path.Combine(Config.LibraryConfiguration.LibraryMetadataDirectory_Game(gameObject), "Covers"); - - Communications comms = new Communications(); - Task ImgFetch = comms.GetSpecificImageFromServer(basePath, cover.ImageId, size, new List{ Communications.IGDBAPI_ImageSize.cover_big, Communications.IGDBAPI_ImageSize.original }); - - string coverFilePath = ImgFetch.Result; - - if (System.IO.File.Exists(coverFilePath)) { - string filename = cover.ImageId + ".jpg"; - string filepath = coverFilePath; - byte[] filedata = System.IO.File.ReadAllBytes(filepath); - string contentType = "image/jpg"; - - var cd = new System.Net.Mime.ContentDisposition + case MetadataImageType.cover: + if (gameObject.Cover != null) + { + if (gameObject.Cover.Id != null) { - FileName = filename, - Inline = true, - }; + IGDB.Models.Cover cover = Classes.Metadata.Covers.GetCover(gameObject.Cover.Id, Config.LibraryConfiguration.LibraryMetadataDirectory_Game(gameObject), false); + imageId = cover.ImageId; + imageTypePath = "Covers"; + } + } + else + { + return NotFound(); + } + break; - Response.Headers.Add("Content-Disposition", cd.ToString()); - Response.Headers.Add("Cache-Control", "public, max-age=604800"); + case MetadataImageType.screenshots: + if (gameObject.Screenshots != null) + { + if (gameObject.Screenshots.Ids.Contains(ImageId)) + { + IGDB.Models.Screenshot imageObject = Screenshots.GetScreenshot(ImageId, Config.LibraryConfiguration.LibraryMetadataDirectory_Game(gameObject), true); - return File(filedata, contentType); + imageId = imageObject.ImageId; + imageTypePath = "Screenshots"; + } + } + else + { + return NotFound(); + } + break; + + case MetadataImageType.artwork: + if (gameObject.Artworks != null) + { + if (gameObject.Artworks.Ids.Contains(ImageId)) + { + IGDB.Models.Artwork imageObject = Artworks.GetArtwork(ImageId, Config.LibraryConfiguration.LibraryMetadataDirectory_Game(gameObject), true); + + imageId = imageObject.ImageId; + imageTypePath = "Artwork"; + } + } + else + { + return NotFound(); + } + break; + + default: + return NotFound(); + } + + if (imageId == null) + { + return NotFound(); + } + + string basePath = Path.Combine(Config.LibraryConfiguration.LibraryMetadataDirectory_Game(gameObject), imageTypePath); + string imagePath = Path.Combine(Config.LibraryConfiguration.LibraryMetadataDirectory_Game(gameObject), imageTypePath, size.ToString(), imageId + ".jpg"); + + if (!System.IO.File.Exists(imagePath)) + { + Communications comms = new Communications(); + Task ImgFetch = comms.GetSpecificImageFromServer(Path.Combine(Config.LibraryConfiguration.LibraryMetadataDirectory_Game(gameObject), imageTypePath), imageId, size, new List { Communications.IGDBAPI_ImageSize.cover_big, Communications.IGDBAPI_ImageSize.original }); + + imagePath = ImgFetch.Result; + } + + if (!System.IO.File.Exists(imagePath)) + { + Communications comms = new Communications(); + Task ImgFetch = comms.GetSpecificImageFromServer(basePath, imageId, size, new List { Communications.IGDBAPI_ImageSize.cover_big, Communications.IGDBAPI_ImageSize.original }); + + imagePath = ImgFetch.Result; + } + + if (System.IO.File.Exists(imagePath)) + { + string filename = imageId + ".jpg"; + string filepath = imagePath; + string contentType = "image/jpg"; + + var cd = new System.Net.Mime.ContentDisposition + { + FileName = filename, + Inline = true, + }; + + Response.Headers.Add("Content-Disposition", cd.ToString()); + Response.Headers.Add("Cache-Control", "public, max-age=604800"); + + byte[] filedata = null; + using (FileStream fs = System.IO.File.OpenRead(filepath)) + { + using (BinaryReader binaryReader = new BinaryReader(fs)) + { + filedata = binaryReader.ReadBytes((int)fs.Length); } } + + return File(filedata, contentType); } + return NotFound(); } catch @@ -611,6 +624,13 @@ namespace gaseous_server.Controllers } } + public enum MetadataImageType + { + cover, + screenshots, + artwork + } + [MapToApiVersion("1.0")] [MapToApiVersion("1.1")] @@ -627,7 +647,7 @@ namespace gaseous_server.Controllers if (gameObject != null) { var user = await _userManager.GetUserAsync(User); - + if (user != null) { Favourites favourites = new Favourites(); @@ -664,7 +684,7 @@ namespace gaseous_server.Controllers if (gameObject != null) { var user = await _userManager.GetUserAsync(User); - + if (user != null) { Favourites favourites = new Favourites(); @@ -796,7 +816,8 @@ namespace gaseous_server.Controllers companyData.Add("company", company); return Ok(companyData); - } else + } + else { return NotFound(); } @@ -857,17 +878,146 @@ namespace gaseous_server.Controllers } } + [MapToApiVersion("1.0")] + [MapToApiVersion("1.1")] + [HttpGet] + [Route("{GameId}/emulatorconfiguration/{PlatformId}")] + [Authorize] + [ProducesResponseType(typeof(UserEmulatorConfiguration), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task GetGameEmulator(long GameId, long PlatformId) + { + try + { + IGDB.Models.Game gameObject = Classes.Metadata.Games.GetGame(GameId, false, false, false); + + if (gameObject != null) + { + IGDB.Models.Platform platformObject = Classes.Metadata.Platforms.GetPlatform(PlatformId); + + if (platformObject != null) + { + var user = await _userManager.GetUserAsync(User); + + if (user != null) + { + PlatformMapping platformMapping = new PlatformMapping(); + UserEmulatorConfiguration platformMappingObject = platformMapping.GetUserEmulator(user.Id, GameId, PlatformId); + + if (platformMappingObject != null) + { + return Ok(platformMappingObject); + } + } + } + } + + return NotFound(); + } + catch + { + return NotFound(); + } + } + + [MapToApiVersion("1.0")] + [MapToApiVersion("1.1")] + [HttpPost] + [Route("{GameId}/emulatorconfiguration/{PlatformId}")] + [Authorize] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task SetGameEmulator(long GameId, long PlatformId, UserEmulatorConfiguration configuration) + { + try + { + IGDB.Models.Game gameObject = Classes.Metadata.Games.GetGame(GameId, false, false, false); + + if (gameObject != null) + { + IGDB.Models.Platform platformObject = Classes.Metadata.Platforms.GetPlatform(PlatformId); + + if (platformObject != null) + { + var user = await _userManager.GetUserAsync(User); + + if (user != null) + { + PlatformMapping platformMapping = new PlatformMapping(); + platformMapping.SetUserEmulator(user.Id, GameId, PlatformId, configuration); + + return Ok(); + } + } + } + + return NotFound(); + } + catch + { + return NotFound(); + } + } + + [MapToApiVersion("1.0")] + [MapToApiVersion("1.1")] + [HttpDelete] + [Route("{GameId}/emulatorconfiguration/{PlatformId}")] + [Authorize] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task DeleteGameEmulator(long GameId, long PlatformId) + { + try + { + IGDB.Models.Game gameObject = Classes.Metadata.Games.GetGame(GameId, false, false, false); + + if (gameObject != null) + { + IGDB.Models.Platform platformObject = Classes.Metadata.Platforms.GetPlatform(PlatformId); + + if (platformObject != null) + { + var user = await _userManager.GetUserAsync(User); + + if (user != null) + { + PlatformMapping platformMapping = new PlatformMapping(); + platformMapping.DeleteUserEmulator(user.Id, GameId, PlatformId); + + return Ok(); + } + } + } + + return NotFound(); + } + catch + { + return NotFound(); + } + } + [MapToApiVersion("1.0")] [MapToApiVersion("1.1")] [HttpGet] [Route("{GameId}/platforms")] - [ProducesResponseType(typeof(List>), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(List), StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task GamePlatforms(long GameId) { try { - return Ok(Games.GetAvailablePlatforms(GameId)); + var user = await _userManager.GetUserAsync(User); + + if (user != null) + { + return Ok(Games.GetAvailablePlatforms(user.Id, GameId)); + } + else + { + return NotFound(); + } } catch { @@ -895,7 +1045,7 @@ namespace gaseous_server.Controllers foreach (long icId in gameObject.ReleaseDates.Ids) { ReleaseDate releaseDate = Classes.Metadata.ReleaseDates.GetReleaseDates(icId); - + rdObjects.Add(releaseDate); } } @@ -923,7 +1073,7 @@ namespace gaseous_server.Controllers public async Task GameRomAsync(long GameId, int pageNumber = 0, int pageSize = 0, long PlatformId = -1, string NameSearch = "") { var user = await _userManager.GetUserAsync(User); - + try { Game gameObject = Classes.Metadata.Games.GetGame(GameId, false, false, false); @@ -1025,6 +1175,67 @@ namespace gaseous_server.Controllers } } + [MapToApiVersion("1.0")] + [MapToApiVersion("1.1")] + [HttpPost] + [Route("{GameId}/roms/{RomId}/{PlatformId}/favourite")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task GameRomFavourite(long GameId, long RomId, long PlatformId, bool IsMediaGroup, bool favourite) + { + try + { + ApplicationUser? user = await _userManager.GetUserAsync(User); + + Game gameObject = Classes.Metadata.Games.GetGame(GameId, false, false, false); + + if (IsMediaGroup == false) + { + Classes.Roms.GameRomItem rom = Classes.Roms.GetRom(RomId); + if (rom.GameId == GameId) + { + if (favourite == true) + { + Classes.Metadata.Games.GameSetFavouriteRom(user.Id, GameId, PlatformId, RomId, IsMediaGroup); + } + else + { + Classes.Metadata.Games.GameClearFavouriteRom(user.Id, GameId, PlatformId); + } + return Ok(); + } + else + { + return NotFound(); + } + } + else + { + Classes.RomMediaGroup.GameRomMediaGroupItem rom = Classes.RomMediaGroup.GetMediaGroup(RomId, user.Id); + if (rom.GameId == GameId) + { + if (favourite == true) + { + Classes.Metadata.Games.GameSetFavouriteRom(user.Id, GameId, PlatformId, RomId, IsMediaGroup); + } + else + { + Classes.Metadata.Games.GameClearFavouriteRom(user.Id, GameId, PlatformId); + } + return Ok(); + } + else + { + return NotFound(); + } + } + } + catch + { + return NotFound(); + } + } + [MapToApiVersion("1.0")] [MapToApiVersion("1.1")] [HttpGet] @@ -1113,7 +1324,7 @@ namespace gaseous_server.Controllers public async Task GameRomGroupAsync(long GameId, long RomGroupId) { var user = await _userManager.GetUserAsync(User); - + try { Game gameObject = Classes.Metadata.Games.GetGame(GameId, false, false, false); @@ -1137,21 +1348,20 @@ namespace gaseous_server.Controllers [MapToApiVersion("1.0")] [MapToApiVersion("1.1")] [HttpGet] - [Authorize(Roles = "Admin,Gamer")] [Route("{GameId}/romgroup")] [ProducesResponseType(typeof(List), StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task GetGameRomGroupAsync(long GameId) + public async Task GetGameRomGroupAsync(long GameId, long? PlatformId = null) { var user = await _userManager.GetUserAsync(User); - + try { Game gameObject = Classes.Metadata.Games.GetGame(GameId, false, false, false); try { - return Ok(RomMediaGroup.GetMediaGroupsFromGameId(GameId, user.Id)); + return Ok(RomMediaGroup.GetMediaGroupsFromGameId(GameId, user.Id, PlatformId)); } catch (Exception ex) { @@ -1204,7 +1414,7 @@ namespace gaseous_server.Controllers public async Task GameRomGroupMembersAsync(long GameId, long RomGroupId, [FromBody] List RomIds) { var user = await _userManager.GetUserAsync(User); - + try { Game gameObject = Classes.Metadata.Games.GetGame(GameId, false, false, false); @@ -1386,9 +1596,10 @@ namespace gaseous_server.Controllers public async Task GameScreenshot(long GameId, long ScreenshotId) { try - { + { IGDB.Models.Game gameObject = Classes.Metadata.Games.GetGame(GameId, false, false, false); - if (gameObject != null) { + if (gameObject != null) + { IGDB.Models.Screenshot screenshotObject = Screenshots.GetScreenshot(ScreenshotId, Config.LibraryConfiguration.LibraryMetadataDirectory_Game(gameObject), false); if (screenshotObject != null) { @@ -1410,56 +1621,56 @@ namespace gaseous_server.Controllers } } - [MapToApiVersion("1.0")] - [MapToApiVersion("1.1")] - [HttpGet] - [Route("{GameId}/screenshots/{ScreenshotId}/image/{size}")] - [Route("{GameId}/screenshots/{ScreenshotId}/image/{size}/{ImageName}")] - [ProducesResponseType(typeof(FileStreamResult), StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task GameScreenshotImage(long GameId, long ScreenshotId, Communications.IGDBAPI_ImageSize Size, string ImageName) - { - try - { - IGDB.Models.Game gameObject = Classes.Metadata.Games.GetGame(GameId, false, false, false); + // [MapToApiVersion("1.0")] + // [MapToApiVersion("1.1")] + // [HttpGet] + // [Route("{GameId}/screenshots/{ScreenshotId}/image/{size}")] + // [Route("{GameId}/screenshots/{ScreenshotId}/image/{size}/{ImageName}")] + // [ProducesResponseType(typeof(FileStreamResult), StatusCodes.Status200OK)] + // [ProducesResponseType(StatusCodes.Status404NotFound)] + // public async Task GameScreenshotImage(long GameId, long ScreenshotId, Communications.IGDBAPI_ImageSize Size, string ImageName) + // { + // try + // { + // IGDB.Models.Game gameObject = Classes.Metadata.Games.GetGame(GameId, false, false, false); - IGDB.Models.Screenshot screenshotObject = Screenshots.GetScreenshot(ScreenshotId, Config.LibraryConfiguration.LibraryMetadataDirectory_Game(gameObject), true); + // IGDB.Models.Screenshot screenshotObject = Screenshots.GetScreenshot(ScreenshotId, Config.LibraryConfiguration.LibraryMetadataDirectory_Game(gameObject), true); - string basePath = Path.Combine(Config.LibraryConfiguration.LibraryMetadataDirectory_Game(gameObject), "Screenshots"); + // string basePath = Path.Combine(Config.LibraryConfiguration.LibraryMetadataDirectory_Game(gameObject), "Screenshots"); - Communications comms = new Communications(); - Task ImgFetch = comms.GetSpecificImageFromServer(basePath, screenshotObject.ImageId, Size, new List{ Communications.IGDBAPI_ImageSize.original }); + // Communications comms = new Communications(); + // Task ImgFetch = comms.GetSpecificImageFromServer(basePath, screenshotObject.ImageId, Size, new List { Communications.IGDBAPI_ImageSize.original }); - string coverFilePath = ImgFetch.Result; + // string coverFilePath = ImgFetch.Result; - if (System.IO.File.Exists(coverFilePath)) - { - string filename = screenshotObject.ImageId + ".jpg"; - string filepath = coverFilePath; - byte[] filedata = System.IO.File.ReadAllBytes(filepath); - string contentType = "image/jpg"; + // if (System.IO.File.Exists(coverFilePath)) + // { + // string filename = screenshotObject.ImageId + ".jpg"; + // string filepath = coverFilePath; + // byte[] filedata = System.IO.File.ReadAllBytes(filepath); + // string contentType = "image/jpg"; - var cd = new System.Net.Mime.ContentDisposition - { - FileName = filename, - Inline = true, - }; + // var cd = new System.Net.Mime.ContentDisposition + // { + // FileName = filename, + // Inline = true, + // }; - Response.Headers.Add("Content-Disposition", cd.ToString()); - Response.Headers.Add("Cache-Control", "public, max-age=604800"); + // Response.Headers.Add("Content-Disposition", cd.ToString()); + // Response.Headers.Add("Cache-Control", "public, max-age=604800"); - return File(filedata, contentType); - } - else - { - return NotFound(); - } - } - catch - { - return NotFound(); - } - } + // return File(filedata, contentType); + // } + // else + // { + // return NotFound(); + // } + // } + // catch + // { + // return NotFound(); + // } + // } [MapToApiVersion("1.0")] [MapToApiVersion("1.1")] diff --git a/gaseous-server/Controllers/V1.0/PlatformMapsController.cs b/gaseous-server/Controllers/V1.0/PlatformMapsController.cs index 2e48460..afd3c3f 100644 --- a/gaseous-server/Controllers/V1.0/PlatformMapsController.cs +++ b/gaseous-server/Controllers/V1.0/PlatformMapsController.cs @@ -149,35 +149,6 @@ namespace gaseous_server.Controllers return Ok(new { count = files.Count, size }); } - // [MapToApiVersion("1.0")] - [MapToApiVersion("1.1")] - [HttpPost] - // [Route("{PlatformId}")] - // [ProducesResponseType(typeof(PlatformMapping.PlatformMapItem), StatusCodes.Status200OK)] - // [ProducesResponseType(StatusCodes.Status404NotFound)] - // [ProducesResponseType(StatusCodes.Status409Conflict)] - // public ActionResult NewPlatformMap(long PlatformId, PlatformMapping.PlatformMapItem Map) - // { - // try - // { - // PlatformMapping.PlatformMapItem platformMapItem = PlatformMapping.GetPlatformMap(PlatformId); - - // if (platformMapItem != null) - // { - // return Conflict(); - // } - // else - // { - // PlatformMapping.WritePlatformMap(Map, false, false); - // return Ok(PlatformMapping.GetPlatformMap(PlatformId)); - // } - // } - // catch - // { - // return NotFound(); - // } - // } - [MapToApiVersion("1.0")] [MapToApiVersion("1.1")] [HttpPatch] diff --git a/gaseous-server/Controllers/V1.0/PlatformsController.cs b/gaseous-server/Controllers/V1.0/PlatformsController.cs index d91236c..bebb965 100644 --- a/gaseous-server/Controllers/V1.0/PlatformsController.cs +++ b/gaseous-server/Controllers/V1.0/PlatformsController.cs @@ -114,22 +114,64 @@ namespace gaseous_server.Controllers [MapToApiVersion("1.0")] [MapToApiVersion("1.1")] [HttpGet] - [Route("{PlatformId}/platformlogo/image")] + [Route("{PlatformId}/platformlogo/{size}/logo.png")] [ProducesResponseType(typeof(FileStreamResult), StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] - public ActionResult PlatformLogoImage(long PlatformId) + public async Task GameImage(long PlatformId, Communications.IGDBAPI_ImageSize size) { try { IGDB.Models.Platform platformObject = Classes.Metadata.Platforms.GetPlatform(PlatformId); - - string logoFilePath = Path.Combine(Config.LibraryConfiguration.LibraryMetadataDirectory_Platform(platformObject), "Logo_Medium.png"); - if (System.IO.File.Exists(logoFilePath)) + IGDB.Models.PlatformLogo? logoObject = null; + try { - string filename = "Logo.png"; - string filepath = logoFilePath; - byte[] filedata = System.IO.File.ReadAllBytes(filepath); - string contentType = "image/png"; + logoObject = PlatformLogos.GetPlatformLogo(platformObject.PlatformLogo.Id, Config.LibraryConfiguration.LibraryMetadataDirectory_Platform(platformObject)); + } + catch + { + // getting the logo failed, so we'll try a platform variant if available + if (platformObject.Versions != null) + { + if (platformObject.Versions.Ids.Length > 0) + { + IGDB.Models.PlatformVersion platformVersion = Classes.Metadata.PlatformVersions.GetPlatformVersion(platformObject.Versions.Ids[0], platformObject); + logoObject = PlatformLogos.GetPlatformLogo(platformVersion.PlatformLogo.Id, Config.LibraryConfiguration.LibraryMetadataDirectory_Platform(platformObject)); + } + else + { + return NotFound(); + } + } + else + { + return NotFound(); + } + } + + string basePath = Path.Combine(Config.LibraryConfiguration.LibraryMetadataDirectory_Platform(platformObject)); + string imagePath = Path.Combine(basePath, size.ToString(), logoObject.ImageId + ".jpg"); + + if (!System.IO.File.Exists(imagePath)) + { + Communications comms = new Communications(); + Task ImgFetch = comms.GetSpecificImageFromServer(Path.Combine(Config.LibraryConfiguration.LibraryMetadataDirectory_Platform(platformObject)), logoObject.ImageId, size, new List { Communications.IGDBAPI_ImageSize.cover_big, Communications.IGDBAPI_ImageSize.original }); + + imagePath = ImgFetch.Result; + } + + if (!System.IO.File.Exists(imagePath)) + { + Communications comms = new Communications(); + Task ImgFetch = comms.GetSpecificImageFromServer(basePath, logoObject.ImageId, size, new List { Communications.IGDBAPI_ImageSize.cover_big, Communications.IGDBAPI_ImageSize.original }); + + imagePath = ImgFetch.Result; + } + + if (System.IO.File.Exists(imagePath)) + { + string filename = logoObject.ImageId + ".jpg"; + string filepath = imagePath; + string contentType = "image/jpg"; var cd = new System.Net.Mime.ContentDisposition { @@ -138,13 +180,21 @@ namespace gaseous_server.Controllers }; Response.Headers.Add("Content-Disposition", cd.ToString()); + Response.Headers.Add("Cache-Control", "public, max-age=604800"); + + byte[] filedata = null; + using (FileStream fs = System.IO.File.OpenRead(filepath)) + { + using (BinaryReader binaryReader = new BinaryReader(fs)) + { + filedata = binaryReader.ReadBytes((int)fs.Length); + } + } return File(filedata, contentType); } - else - { - return NotFound(); - } + + return NotFound(); } catch { diff --git a/gaseous-server/Controllers/V1.0/RomsController.cs b/gaseous-server/Controllers/V1.0/RomsController.cs index a6fe557..7b5d9be 100644 --- a/gaseous-server/Controllers/V1.0/RomsController.cs +++ b/gaseous-server/Controllers/V1.0/RomsController.cs @@ -31,63 +31,70 @@ namespace gaseous_server.Controllers [Authorize(Roles = "Admin,Gamer")] [ProducesResponseType(typeof(List), StatusCodes.Status200OK)] [RequestSizeLimit(long.MaxValue)] + [Consumes("multipart/form-data")] [DisableRequestSizeLimit, RequestFormLimits(MultipartBodyLengthLimit = long.MaxValue, ValueLengthLimit = int.MaxValue)] - public async Task UploadRom(List files, long? OverridePlatformId = null) + public async Task UploadRom(IFormFile file, long? OverridePlatformId = null) { Guid sessionid = Guid.NewGuid(); string workPath = Path.Combine(Config.LibraryConfiguration.LibraryUploadDirectory, sessionid.ToString()); - long size = files.Sum(f => f.Length); - - List> UploadedFiles = new List>(); - - foreach (IFormFile formFile in files) + if (file.Length > 0) { - if (formFile.Length > 0) + Guid FileId = Guid.NewGuid(); + + string filePath = Path.Combine(workPath, Path.GetFileName(file.FileName)); + + if (!Directory.Exists(workPath)) { - Guid FileId = Guid.NewGuid(); - - string filePath = Path.Combine(workPath, Path.GetFileName(formFile.FileName)); - - if (!Directory.Exists(workPath)) - { - Directory.CreateDirectory(workPath); - } - - using (var stream = System.IO.File.Create(filePath)) - { - await formFile.CopyToAsync(stream); - - Dictionary UploadedFile = new Dictionary(); - UploadedFile.Add("id", FileId.ToString()); - UploadedFile.Add("originalname", Path.GetFileName(formFile.FileName)); - UploadedFile.Add("fullpath", filePath); - UploadedFiles.Add(UploadedFile); - } + Directory.CreateDirectory(workPath); } - } - // get override platform if specified - IGDB.Models.Platform? OverridePlatform = null; - if (OverridePlatformId != null) - { - OverridePlatform = Platforms.GetPlatform((long)OverridePlatformId); - } + Dictionary UploadedFile = new Dictionary(); - // Process uploaded files - foreach (Dictionary UploadedFile in UploadedFiles) - { + using (var stream = System.IO.File.Create(filePath)) + { + await file.CopyToAsync(stream); + + UploadedFile.Add("id", FileId.ToString()); + UploadedFile.Add("originalname", Path.GetFileName(file.FileName)); + UploadedFile.Add("fullpath", filePath); + } + + // get override platform if specified + IGDB.Models.Platform? OverridePlatform = null; + if (OverridePlatformId != null) + { + OverridePlatform = Platforms.GetPlatform((long)OverridePlatformId); + } + + // Process uploaded file Classes.ImportGame uploadImport = new ImportGame(); - uploadImport.ImportGameFile((string)UploadedFile["fullpath"], OverridePlatform); + Dictionary RetVal = uploadImport.ImportGameFile((string)UploadedFile["fullpath"], OverridePlatform); + switch (RetVal["type"]) + { + case "rom": + if (RetVal["status"] == "imported") + { + IGDB.Models.Game? game = (IGDB.Models.Game)RetVal["game"]; + if (game.Id == null) + { + RetVal["game"] = Games.GetGame(0, false, false, false); + } + } + break; + + } + + if (Directory.Exists(workPath)) + { + Directory.Delete(workPath, true); + } + + return Ok(RetVal); } - if (Directory.Exists(workPath)) - { - Directory.Delete(workPath, true); - } - - return Ok(new { count = files.Count, size }); + return Ok(); } } } \ No newline at end of file diff --git a/gaseous-server/Controllers/V1.0/SearchController.cs b/gaseous-server/Controllers/V1.0/SearchController.cs index 3589410..455867f 100644 --- a/gaseous-server/Controllers/V1.0/SearchController.cs +++ b/gaseous-server/Controllers/V1.0/SearchController.cs @@ -73,7 +73,7 @@ namespace gaseous_server.Controllers private static async Task> _SearchForGame(long PlatformId, string SearchString) { string searchBody = ""; - string searchFields = "fields cover,first_release_date,name,platforms,slug; "; + string searchFields = "fields *; "; searchBody += "search \"" + SearchString + "\";"; searchBody += "where platforms = (" + PlatformId + ");"; searchBody += "limit 100;"; @@ -86,12 +86,12 @@ namespace gaseous_server.Controllers // get Game metadata from data source Communications comms = new Communications(); var results = await comms.APIComm(IGDBClient.Endpoints.Games, searchFields, searchBody); - + List games = new List(); foreach (Game game in results.ToList()) { Storage.CacheStatus cacheStatus = Storage.GetCacheStatus("Game", (long)game.Id); - switch(cacheStatus) + switch (cacheStatus) { case Storage.CacheStatus.NotPresent: Storage.NewCacheValue(game, false); diff --git a/gaseous-server/Controllers/V1.0/SignaturesController.cs b/gaseous-server/Controllers/V1.0/SignaturesController.cs index 2d85377..3a87437 100644 --- a/gaseous-server/Controllers/V1.0/SignaturesController.cs +++ b/gaseous-server/Controllers/V1.0/SignaturesController.cs @@ -9,6 +9,7 @@ using gaseous_signature_parser.models.RomSignatureObject; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Asp.Versioning; +using gaseous_server.Models; // For more information on enabling MVC for empty projects, visit https://go.microsoft.com/fwlink/?LinkID=397860 @@ -54,11 +55,34 @@ namespace gaseous_server.Controllers { SignatureManagement signatureManagement = new SignatureManagement(); return signatureManagement.GetByTosecName(TosecName); - } else + } + else { return null; } } + + [MapToApiVersion("1.0")] + [MapToApiVersion("1.1")] + [HttpGet] + [ProducesResponseType(StatusCodes.Status200OK)] + public List GetSignatureSources() + { + SignatureManagement signatureManagement = new SignatureManagement(); + return signatureManagement.GetSources(); + } + + [MapToApiVersion("1.0")] + [MapToApiVersion("1.1")] + [HttpDelete] + [ProducesResponseType(StatusCodes.Status200OK)] + public IActionResult DeleteSignatureSource(int Id) + { + SignatureManagement signatureManagement = new SignatureManagement(); + signatureManagement.DeleteSource(Id); + + return Ok(); + } } } diff --git a/gaseous-server/Controllers/V1.0/SystemController.cs b/gaseous-server/Controllers/V1.0/SystemController.cs index 2aced21..ea7995d 100644 --- a/gaseous-server/Controllers/V1.0/SystemController.cs +++ b/gaseous-server/Controllers/V1.0/SystemController.cs @@ -38,7 +38,10 @@ namespace gaseous_server.Controllers List Disks = new List(); foreach (GameLibrary.LibraryItem libraryItem in GameLibrary.GetLibraries) { - Disks.Add(GetDisk(libraryItem.Path)); + SystemInfo.PathItem pathItem = GetDisk(libraryItem.Path); + pathItem.Name = libraryItem.Name; + + Disks.Add(pathItem); } ReturnValue.Paths = Disks; @@ -100,7 +103,7 @@ namespace gaseous_server.Controllers string ver = "var AppVersion = \"" + Assembly.GetExecutingAssembly().GetName().Version.ToString() + "\";" + Environment.NewLine + "var DBSchemaVersion = \"" + db.GetDatabaseSchemaVersion() + "\";" + Environment.NewLine + - "var FirstRunStatus = " + Config.ReadSetting("FirstRunStatus", "0") + ";" + Environment.NewLine + + "var FirstRunStatus = \"" + Config.ReadSetting("FirstRunStatus", "0") + "\";" + Environment.NewLine + "var AgeRatingBoardsStrings = " + JsonSerializer.Serialize(ClassificationBoardsStrings, new JsonSerializerOptions { WriteIndented = true @@ -324,6 +327,7 @@ namespace gaseous_server.Controllers public class PathItem { + public string Name { get; set; } public string LibraryPath { get; set; } public long SpaceUsed { get; set; } public long SpaceAvailable { get; set; } diff --git a/gaseous-server/Controllers/V1.1/FileSystemController.cs b/gaseous-server/Controllers/V1.1/FileSystemController.cs new file mode 100644 index 0000000..307e0f5 --- /dev/null +++ b/gaseous-server/Controllers/V1.1/FileSystemController.cs @@ -0,0 +1,80 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Data; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Threading.Tasks; +using gaseous_server.Classes; +using gaseous_server.Classes.Metadata; +using IGDB.Models; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.CodeAnalysis.Scripting; +using static gaseous_server.Classes.Metadata.AgeRatings; +using Asp.Versioning; + +namespace gaseous_server.Controllers.v1_1 +{ + [Route("api/v{version:apiVersion}/[controller]")] + [ApiVersion("1.1")] + [ApiController] + public class FileSystemController : ControllerBase + { + [MapToApiVersion("1.1")] + [HttpGet] + [Authorize(Roles = "Admin")] + [ProducesResponseType(typeof(FileContentResult), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public ActionResult GetFileSystem(string path, bool showFiles = false) + { + try + { + if (path.Contains("..")) + { + return NotFound(); + } + + if (Directory.Exists(path)) + { + Dictionary>> allFiles = new Dictionary>>(); + List> directories = new List>(); + string[] dirs = Directory.GetDirectories(path); + Array.Sort(dirs); + foreach (string dir in dirs) + { + DirectoryInfo directoryInfo = new DirectoryInfo(dir); + directories.Add(new Dictionary { { "name", directoryInfo.Name }, { "path", directoryInfo.FullName } }); + } + allFiles.Add("directories", directories); + + if (showFiles == true) + { + List> files = new List>(); + string[] filePaths = Directory.GetFiles(path); + Array.Sort(filePaths); + foreach (string file in filePaths) + { + FileInfo fileInfo = new FileInfo(file); + files.Add(new Dictionary { { "name", fileInfo.Name }, { "path", fileInfo.FullName } }); + } + allFiles.Add("files", files); + } + + return Ok(allFiles); + } + else + { + return NotFound(); + + } + } + catch (Exception ex) + { + return NotFound(); + } + } + } +} \ No newline at end of file diff --git a/gaseous-server/Controllers/V1.1/GamesController.cs b/gaseous-server/Controllers/V1.1/GamesController.cs index 50e2e19..5e675ad 100644 --- a/gaseous-server/Controllers/V1.1/GamesController.cs +++ b/gaseous-server/Controllers/V1.1/GamesController.cs @@ -26,11 +26,11 @@ namespace gaseous_server.Controllers.v1_1 [ApiVersion("1.1")] [ApiController] [Authorize] - public class GamesController: ControllerBase + public class GamesController : ControllerBase { private readonly UserManager _userManager; private readonly SignInManager _signInManager; - + public GamesController( UserManager userManager, SignInManager signInManager) @@ -106,7 +106,8 @@ namespace gaseous_server.Controllers.v1_1 if (user != null) { string IncludeUnrated = ""; - if (user.SecurityProfile.AgeRestrictionPolicy.IncludeUnrated == true) { + if (user.SecurityProfile.AgeRestrictionPolicy.IncludeUnrated == true) + { IncludeUnrated = " OR view_Games.AgeGroupId IS NULL"; } @@ -150,7 +151,7 @@ namespace gaseous_server.Controllers.v1_1 public GameSortingItem Sorting { get; set; } = new GameSortingItem(); public bool HasSavedGame { get; set; } public bool IsFavourite { get; set; } - + public class GameRatingItem { @@ -163,7 +164,7 @@ namespace gaseous_server.Controllers.v1_1 public class GameAgeRatingItem { - public List AgeGroupings { get; set; } = new List{ + public List AgeGroupings { get; set; } = new List{ AgeGroups.AgeRestrictionGroupings.Child, AgeGroups.AgeRestrictionGroupings.Teen, AgeGroups.AgeRestrictionGroupings.Mature, @@ -186,7 +187,7 @@ namespace gaseous_server.Controllers.v1_1 } } } - + public static GameReturnPackage GetGames(GameSearchModel model, string userid, int pageNumber = 0, int pageSize = 0) { string whereClause = ""; @@ -446,7 +447,7 @@ namespace gaseous_server.Controllers.v1_1 string orderByOrder = "ASC"; if (model.Sorting != null) { - switch(model.Sorting.SortBy) + switch (model.Sorting.SortBy) { case GameSearchModel.GameSortingItem.SortField.NameThe: orderByField = "NameThe"; @@ -477,7 +478,7 @@ namespace gaseous_server.Controllers.v1_1 string orderByClause = "ORDER BY `" + orderByField + "` " + orderByOrder; Database db = new Database(Database.databaseType.MySql, Config.DatabaseConfiguration.ConnectionString); - + string sql = @" SET SESSION sql_mode=(SELECT REPLACE(@@sql_mode,'ONLY_FULL_GROUP_BY','')); SELECT DISTINCT @@ -485,6 +486,7 @@ SELECT DISTINCT Game.`Name`, Game.NameThe, Game.Slug, + Game.Summary, Game.PlatformId, Game.TotalRating, Game.TotalRatingCount, @@ -551,7 +553,7 @@ FROM Favourites ON Game.Id = Favourites.GameId AND Favourites.UserId = @userid " + whereClause + " " + havingClause + " " + orderByClause; List RetVal = new List(); - DataTable dbResponse = db.ExecuteCMD(sql, whereParams); + DataTable dbResponse = db.ExecuteCMD(sql, whereParams, new Database.DatabaseMemoryCacheOptions(CacheEnabled: true, ExpirationSeconds: 60)); // get count int RecordCount = dbResponse.Rows.Count; @@ -568,7 +570,7 @@ FROM } } - Game retGame = Storage.BuildCacheObject(new Game() , dbResponse.Rows[i]); + Game retGame = Storage.BuildCacheObject(new Game(), dbResponse.Rows[i]); Games.MinimalGameItem retMinGame = new Games.MinimalGameItem(retGame); retMinGame.Index = i; if (dbResponse.Rows[i]["RomSaveCount"] != DBNull.Value || dbResponse.Rows[i]["MediaGroupSaveCount"] != DBNull.Value) @@ -593,29 +595,44 @@ FROM // build alpha list Dictionary AlphaList = new Dictionary(); - int CurrentPage = 1; - int NextPageIndex = pageSize; - for (int i = 0; i < dbResponse.Rows.Count; i++) + if (orderByField == "NameThe" || orderByField == "Name") { - string firstChar = dbResponse.Rows[i]["NameThe"].ToString().Substring(0, 1).ToUpperInvariant(); - if (!"ABCDEFGHIJKLMNOPQRSTUVWXYZ".Contains(firstChar)) + int CurrentPage = 1; + int NextPageIndex = pageSize; + + string alphaSearchField; + if (orderByField == "NameThe") { - if (!AlphaList.ContainsKey("#")) - { - AlphaList.Add("#", 1); - } + alphaSearchField = "NameThe"; } else { - if (!AlphaList.ContainsKey(firstChar)) - { - AlphaList.Add(firstChar, CurrentPage); - } + alphaSearchField = "Name"; + } + + for (int i = 0; i < dbResponse.Rows.Count; i++) + { if (NextPageIndex == i + 1) { NextPageIndex += pageSize; CurrentPage += 1; } + + string firstChar = dbResponse.Rows[i][alphaSearchField].ToString().Substring(0, 1).ToUpperInvariant(); + if ("ABCDEFGHIJKLMNOPQRSTUVWXYZ".Contains(firstChar)) + { + if (!AlphaList.ContainsKey(firstChar)) + { + AlphaList.Add(firstChar, CurrentPage); + } + } + else + { + if (!AlphaList.ContainsKey("#")) + { + AlphaList.Add("#", 1); + } + } } } diff --git a/gaseous-server/Controllers/V1.1/StateManagerController.cs b/gaseous-server/Controllers/V1.1/StateManagerController.cs index db47d7e..6c214df 100644 --- a/gaseous-server/Controllers/V1.1/StateManagerController.cs +++ b/gaseous-server/Controllers/V1.1/StateManagerController.cs @@ -6,6 +6,7 @@ using Authentication; using Microsoft.AspNetCore.Identity; using System.Data; using Asp.Versioning; +using System.IO.Compression; namespace gaseous_server.Controllers.v1_1 { @@ -13,7 +14,7 @@ namespace gaseous_server.Controllers.v1_1 [ApiVersion("1.0")] [ApiVersion("1.1")] [ApiController] - public class StateManagerController: ControllerBase + public class StateManagerController : ControllerBase { private readonly UserManager _userManager; private readonly SignInManager _signInManager; @@ -39,7 +40,7 @@ namespace gaseous_server.Controllers.v1_1 var user = await _userManager.GetUserAsync(User); byte[] CompressedState = Common.Compress(uploadState.StateByteArray); - + Database db = new Database(Database.databaseType.MySql, Config.DatabaseConfiguration.ConnectionString); string sql = "INSERT INTO GameState (UserId, RomId, IsMediaGroup, StateDateTime, Name, Screenshot, State, Zipped) VALUES (@userid, @romid, @ismediagroup, @statedatetime, @name, @screenshot, @state, @zipped); SELECT LAST_INSERT_ID();"; Dictionary dbDict = new Dictionary @@ -86,7 +87,7 @@ namespace gaseous_server.Controllers.v1_1 { "ismediagroup", IsMediaGroup } }; DataTable data = db.ExecuteCMD(sql, dbDict); - + List gameStates = new List(); foreach (DataRow row in data.Rows) { @@ -116,7 +117,7 @@ namespace gaseous_server.Controllers.v1_1 { "ismediagroup", IsMediaGroup } }; DataTable data = db.ExecuteCMD(sql, dbDict); - + if (data.Rows.Count == 0) { // invalid match - return not found @@ -150,7 +151,7 @@ namespace gaseous_server.Controllers.v1_1 { "ismediagroup", IsMediaGroup } }; db.ExecuteNonQuery(sql, dbDict); - + return Ok(); } @@ -175,7 +176,7 @@ namespace gaseous_server.Controllers.v1_1 { "name", model.Name } }; db.ExecuteNonQuery(sql, dbDict); - + return Ok(); } @@ -200,7 +201,7 @@ namespace gaseous_server.Controllers.v1_1 { "ismediagroup", IsMediaGroup } }; DataTable data = db.ExecuteCMD(sql, dbDict); - + if (data.Rows.Count == 0) { // invalid match - return not found @@ -233,11 +234,11 @@ namespace gaseous_server.Controllers.v1_1 [ProducesResponseType(StatusCodes.Status404NotFound)] [Route("{RomId}/{StateId}/State/")] [Route("{RomId}/{StateId}/State/savestate.state")] - public async Task GetStateDataAsync(long RomId, long StateId, bool IsMediaGroup = false) + public async Task GetStateDataAsync(long RomId, long StateId, bool IsMediaGroup = false, bool StateOnly = false) { var user = await _userManager.GetUserAsync(User); Database db = new Database(Database.databaseType.MySql, Config.DatabaseConfiguration.ConnectionString); - string sql = "SELECT Zipped, State FROM GameState WHERE Id = @id AND RomId = @romid AND IsMediaGroup = @ismediagroup AND UserId = @userid;"; + string sql = "SELECT * FROM GameState WHERE Id = @id AND RomId = @romid AND IsMediaGroup = @ismediagroup AND UserId = @userid;"; Dictionary dbDict = new Dictionary { { "id", StateId }, @@ -246,7 +247,7 @@ namespace gaseous_server.Controllers.v1_1 { "ismediagroup", IsMediaGroup } }; DataTable data = db.ExecuteCMD(sql, dbDict); - + if (data.Rows.Count == 0) { // invalid match - return not found @@ -254,7 +255,9 @@ namespace gaseous_server.Controllers.v1_1 } else { - string filename = "savestate.state"; + // get rom data + Roms.GameRomItem romItem = Roms.GetRom(RomId); + byte[] bytes; if ((bool)data.Rows[0]["Zipped"] == false) { @@ -264,7 +267,85 @@ namespace gaseous_server.Controllers.v1_1 { bytes = Common.Decompress((byte[])data.Rows[0]["State"]); } - string contentType = "application/octet-stream"; + string contentType = ""; + string filename = ((DateTime)data.Rows[0]["StateDateTime"]).ToString("yyyy-MM-ddTHH-mm-ss") + "-" + Path.GetFileNameWithoutExtension(romItem.Name); + + + if (StateOnly == true) + { + contentType = "application/octet-stream"; + filename = filename + ".state"; + } + else + { + contentType = "application/zip"; + filename = filename + ".zip"; + + Dictionary RomInfo = new Dictionary + { + { "Name", romItem.Name }, + { "StateDateTime", data.Rows[0]["StateDateTime"] }, + { "StateName", data.Rows[0]["Name"] } + }; + if ((int)data.Rows[0]["IsMediaGroup"] == 0) + { + RomInfo.Add("MD5", romItem.Md5); + RomInfo.Add("SHA1", romItem.Sha1); + RomInfo.Add("Type", "ROM"); + } + else + { + RomInfo.Add("Type", "Media Group"); + RomInfo.Add("MediaGroupId", (long)data.Rows[0]["RomId"]); + } + string RomInfoString = Newtonsoft.Json.JsonConvert.SerializeObject(RomInfo, Newtonsoft.Json.Formatting.Indented, new Newtonsoft.Json.JsonSerializerSettings { NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore }); + + // compile zip file + using (var compressedFileStream = new MemoryStream()) + { + List> Attachments = new List>(); + Attachments.Add(new Dictionary + { + { "Name", "savestate.state" }, + { "Body", bytes } + }); + // check if value is dbnull + if (data.Rows[0]["Screenshot"] != DBNull.Value) + { + Attachments.Add(new Dictionary + { + { "Name", "screenshot.jpg" }, + { "Body", (byte[])data.Rows[0]["Screenshot"] } + }); + } + Attachments.Add(new Dictionary + { + { "Name", "rominfo.json" }, + { "Body", System.Text.Encoding.UTF8.GetBytes(RomInfoString) } + }); + + //Create an archive and store the stream in memory. + using (var zipArchive = new ZipArchive(compressedFileStream, ZipArchiveMode.Create, false)) + { + foreach (var Attachment in Attachments) + { + //Create a zip entry for each attachment + var zipEntry = zipArchive.CreateEntry(Attachment["Name"].ToString()); + + //Get the stream of the attachment + using (var originalFileStream = new MemoryStream((byte[])Attachment["Body"])) + using (var zipEntryStream = zipEntry.Open()) + { + //Copy the attachment stream to the zip entry stream + originalFileStream.CopyTo(zipEntryStream); + } + } + } + + //return new FileContentResult(compressedFileStream.ToArray(), "application/zip") { FileDownloadName = filename }; + bytes = compressedFileStream.ToArray(); + } + } var cd = new System.Net.Mime.ContentDisposition { @@ -279,6 +360,156 @@ namespace gaseous_server.Controllers.v1_1 } } + [MapToApiVersion("1.0")] + [MapToApiVersion("1.1")] + [HttpPost] + [Authorize] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [RequestSizeLimit(long.MaxValue)] + [Consumes("multipart/form-data")] + [DisableRequestSizeLimit, RequestFormLimits(MultipartBodyLengthLimit = long.MaxValue, ValueLengthLimit = int.MaxValue)] + [Route("Upload")] + public async Task UploadStateDataAsync(IFormFile file, long RomId = 0, bool IsMediaGroup = false) + { + // get user + var user = await _userManager.GetUserAsync(User); + + if (file.Length > 0) + { + MemoryStream fileContent = new MemoryStream(); + file.CopyTo(fileContent); + + // test if file is a zip file + try + { + using (var zipArchive = new ZipArchive(fileContent, ZipArchiveMode.Read, false)) + { + foreach (var entry in zipArchive.Entries) + { + if (entry.FullName == "rominfo.json") + { + using (var stream = entry.Open()) + using (var reader = new StreamReader(stream)) + { + string RomInfoString = reader.ReadToEnd(); + Dictionary RomInfo = Newtonsoft.Json.JsonConvert.DeserializeObject>(RomInfoString); + + // get rom data + Roms.GameRomItem romItem; + + try + { + romItem = Roms.GetRom((string)RomInfo["MD5"]); + } + catch (Roms.InvalidRomHash) + { + return NotFound(); + } + + // get state data + byte[] StateData = null; + byte[] ScreenshotData = null; + string StateName = RomInfo["StateName"].ToString(); + DateTime StateDateTime = DateTime.Parse(RomInfo["StateDateTime"].ToString()); + IsMediaGroup = RomInfo["Type"].ToString() == "Media Group" ? true : false; + + if (zipArchive.GetEntry("savestate.state") != null) + { + using (var stateStream = zipArchive.GetEntry("savestate.state").Open()) + using (var stateReader = new MemoryStream()) + { + stateStream.CopyTo(stateReader); + StateData = stateReader.ToArray(); + } + } + if (zipArchive.GetEntry("screenshot.jpg") != null) + { + using (var screenshotStream = zipArchive.GetEntry("screenshot.jpg").Open()) + using (var screenshotReader = new MemoryStream()) + { + screenshotStream.CopyTo(screenshotReader); + ScreenshotData = screenshotReader.ToArray(); + } + } + + // save state + Database db = new Database(Database.databaseType.MySql, Config.DatabaseConfiguration.ConnectionString); + string sql = "INSERT INTO GameState (UserId, RomId, IsMediaGroup, StateDateTime, Name, Screenshot, State, Zipped) VALUES (@userid, @romid, @ismediagroup, @statedatetime, @name, @screenshot, @state, @zipped); SELECT LAST_INSERT_ID();"; + Dictionary dbDict = new Dictionary + { + { "userid", user.Id }, + { "romid", romItem.Id }, + { "ismediagroup", IsMediaGroup }, + { "statedatetime", StateDateTime }, + { "name", StateName }, + { "screenshot", ScreenshotData }, + { "state", Common.Compress(StateData) }, + { "zipped", true } + }; + DataTable data = db.ExecuteCMD(sql, dbDict); + + RomInfo.Add("RomId", romItem.Id); + RomInfo.Add("Management", "Managed"); + return Ok(RomInfo); + } + } + } + } + + return BadRequest("File is not a valid Gaseous state file."); + } + catch + { + // not a zip file + if (RomId != 0) + { + // get rom data + Roms.GameRomItem romItem; + + try + { + romItem = Roms.GetRom(RomId); + } + catch (Roms.InvalidRomHash) + { + return NotFound(); + } + + // save state + Database db = new Database(Database.databaseType.MySql, Config.DatabaseConfiguration.ConnectionString); + string sql = "INSERT INTO GameState (UserId, RomId, IsMediaGroup, StateDateTime, Name, Screenshot, State, Zipped) VALUES (@userid, @romid, @ismediagroup, @statedatetime, @name, @screenshot, @state, @zipped); SELECT LAST_INSERT_ID();"; + Dictionary dbDict = new Dictionary + { + { "userid", user.Id }, + { "romid", RomId }, + { "ismediagroup", IsMediaGroup }, + { "statedatetime", DateTime.UtcNow }, + { "name", "" }, + { "screenshot", null }, + { "state", Common.Compress(fileContent.ToArray()) }, + { "zipped", true } + }; + DataTable data = db.ExecuteCMD(sql, dbDict); + + return Ok(new Dictionary + { + { "RomId", RomId }, + { "Management", "Unmanaged" } + }); + } + else + { + return BadRequest("No rom id provided."); + } + } + } + else + { + return BadRequest("File is empty."); + } + } + private Models.GameStateItem BuildGameStateItem(DataRow dr) { bool HasScreenshot = true; diff --git a/gaseous-server/Controllers/V1.1/StatisticsController.cs b/gaseous-server/Controllers/V1.1/StatisticsController.cs index 22ac5a2..517d12e 100644 --- a/gaseous-server/Controllers/V1.1/StatisticsController.cs +++ b/gaseous-server/Controllers/V1.1/StatisticsController.cs @@ -12,7 +12,7 @@ namespace gaseous_server.Controllers.v1_1 [ApiVersion("1.0")] [ApiVersion("1.1")] [ApiController] - public class StatisticsController: ControllerBase + public class StatisticsController : ControllerBase { private readonly UserManager _userManager; private readonly SignInManager _signInManager; @@ -32,15 +32,15 @@ namespace gaseous_server.Controllers.v1_1 [Authorize] [ProducesResponseType(typeof(Models.StatisticsModel), StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] - [Route("Games/{GameId}/")] - public async Task NewRecordStatistics(long GameId) + [Route("Games/{GameId}/{PlatformId}/{RomId}")] + public async Task NewRecordStatistics(long GameId, long PlatformId, long RomId, bool IsMediaGroup) { var user = await _userManager.GetUserAsync(User); if (user != null) { Statistics statistics = new Statistics(); - return Ok(statistics.RecordSession(Guid.Empty, GameId, user.Id)); + return Ok(statistics.RecordSession(Guid.Empty, GameId, PlatformId, RomId, IsMediaGroup, user.Id)); } else { @@ -54,15 +54,15 @@ namespace gaseous_server.Controllers.v1_1 [Authorize] [ProducesResponseType(typeof(Models.StatisticsModel), StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] - [Route("Games/{GameId}/{SessionId}")] - public async Task SubsequentRecordStatistics(long GameId, Guid SessionId) + [Route("Games/{GameId}/{PlatformId}/{RomId}/{SessionId}")] + public async Task SubsequentRecordStatistics(long GameId, long PlatformId, long RomId, Guid SessionId, bool IsMediaGroup) { var user = await _userManager.GetUserAsync(User); if (user != null) { Statistics statistics = new Statistics(); - return Ok(statistics.RecordSession(SessionId, GameId, user.Id)); + return Ok(statistics.RecordSession(SessionId, GameId, PlatformId, RomId, IsMediaGroup, user.Id)); } else { diff --git a/gaseous-server/Controllers/V1.1/UserProfileController.cs b/gaseous-server/Controllers/V1.1/UserProfileController.cs new file mode 100644 index 0000000..da31a83 --- /dev/null +++ b/gaseous-server/Controllers/V1.1/UserProfileController.cs @@ -0,0 +1,137 @@ +using Asp.Versioning; +using Authentication; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; + +namespace gaseous_server.Controllers +{ + [ApiController] + [Route("api/v{version:apiVersion}/[controller]")] + [ApiVersion("1.0")] + [ApiVersion("1.1")] + [Authorize] + public class UserProfileController : ControllerBase + { + private readonly UserManager _userManager; + private readonly SignInManager _signInManager; + + public UserProfileController( + UserManager userManager, + SignInManager signInManager + ) + { + _userManager = userManager; + _signInManager = signInManager; + } + + [MapToApiVersion("1.0")] + [MapToApiVersion("1.1")] + [HttpGet] + [Route("{UserId}")] + [ProducesResponseType(typeof(Models.UserProfile), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ResponseCache(CacheProfileName = "5Minute")] + public ActionResult GetUserProfile(string UserId) + { + Classes.UserProfile profile = new Classes.UserProfile(); + Models.UserProfile RetVal = profile.GetUserProfile(UserId); + return Ok(RetVal); + } + + [MapToApiVersion("1.0")] + [MapToApiVersion("1.1")] + [HttpPut] + [Route("{UserId}")] + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task UpdateUserProfileAsync(string UserId, Models.UserProfile profile) + { + var user = await _userManager.GetUserAsync(User); + + if (user.ProfileId.ToString() != UserId) + { + return Unauthorized(); + } + + Classes.UserProfile userProfile = new Classes.UserProfile(); + userProfile.UpdateUserProfile(user.Id, profile); + return Ok(); + } + + [MapToApiVersion("1.0")] + [MapToApiVersion("1.1")] + [HttpPut] + [ProducesResponseType(typeof(List), StatusCodes.Status200OK)] + [RequestSizeLimit(long.MaxValue)] + [Consumes("multipart/form-data")] + [DisableRequestSizeLimit, RequestFormLimits(MultipartBodyLengthLimit = long.MaxValue, ValueLengthLimit = int.MaxValue)] + [Route("{UserId}/{ProfileImageType}")] + public async Task UpdateAvatarAsync(string UserId, Classes.UserProfile.ImageType ProfileImageType, IFormFile file) + { + var user = await _userManager.GetUserAsync(User); + + if (user.ProfileId.ToString() != UserId) + { + return Unauthorized(); + } + + if (file.Length > 0) + { + using (var ms = new MemoryStream()) + { + file.CopyTo(ms); + byte[] fileBytes = ms.ToArray(); + + Classes.UserProfile userProfile = new Classes.UserProfile(); + userProfile.UpdateImage(ProfileImageType, UserId, user.Id, file.FileName, fileBytes); + } + } + + return Ok(); + } + + [MapToApiVersion("1.0")] + [MapToApiVersion("1.1")] + [HttpGet] + [Route("{UserId}/{ProfileImageType}")] + [ProducesResponseType(typeof(FileStreamResult), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ResponseCache(CacheProfileName = "5Minute")] + public async Task GetAvatarAsync(string UserId, Classes.UserProfile.ImageType ProfileImageType) + { + Classes.UserProfile userProfile = new Classes.UserProfile(); + + Models.ImageItem image = userProfile.GetImage(ProfileImageType, UserId); + + if (image == null) + { + return NotFound(); + } + else + { + return File(image.content, image.mimeType, UserId + image.extension); + } + } + + [MapToApiVersion("1.0")] + [MapToApiVersion("1.1")] + [HttpDelete] + [Route("{UserId}/{ProfileImageType}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + public async Task DeleteAvatarAsync(string UserId, Classes.UserProfile.ImageType ProfileImageType) + { + var user = await _userManager.GetUserAsync(User); + + if (user.ProfileId.ToString() != UserId) + { + return Unauthorized(); + } + + Classes.UserProfile userProfile = new Classes.UserProfile(); + userProfile.DeleteImage(ProfileImageType, user.Id); + + return Ok(); + } + } +} diff --git a/gaseous-server/Models/GaseousGame.cs b/gaseous-server/Models/GaseousGame.cs index 150f108..284430b 100644 --- a/gaseous-server/Models/GaseousGame.cs +++ b/gaseous-server/Models/GaseousGame.cs @@ -16,7 +16,7 @@ namespace gaseous_server.Models { var targetType = this.GetType(); var sourceType = game.GetType(); - foreach(var prop in targetType.GetProperties(BindingFlags.Instance | BindingFlags.Public| BindingFlags.SetProperty)) + foreach (var prop in targetType.GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.SetProperty)) { // check whether source object has the the property var sp = sourceType.GetProperty(prop.Name); @@ -39,9 +39,11 @@ namespace gaseous_server.Models { if (this.Cover.Id != null) { - IGDB.Models.Cover cover = Covers.GetCover(Cover.Id, Config.LibraryConfiguration.LibraryMetadataDirectory_Game(this), false); - - return cover; + // IGDB.Models.Cover cover = Covers.GetCover(Cover.Id, Config.LibraryConfiguration.LibraryMetadataDirectory_Game(this), false); + IGDB.Models.Cover cover = new IGDB.Models.Cover() + { + Id = this.Cover.Id + }; } } diff --git a/gaseous-server/Models/ImageItem.cs b/gaseous-server/Models/ImageItem.cs new file mode 100644 index 0000000..7474d48 --- /dev/null +++ b/gaseous-server/Models/ImageItem.cs @@ -0,0 +1,9 @@ +namespace gaseous_server.Models +{ + public class ImageItem + { + public byte[] content { get; set; } + public string mimeType { get; set; } + public string extension { get; set; } + } +} \ No newline at end of file diff --git a/gaseous-server/Models/PlatformMapping.cs b/gaseous-server/Models/PlatformMapping.cs index 88eea1f..e8c7b52 100644 --- a/gaseous-server/Models/PlatformMapping.cs +++ b/gaseous-server/Models/PlatformMapping.cs @@ -13,10 +13,8 @@ using Newtonsoft.Json; namespace gaseous_server.Models { - public class PlatformMapping - { - private static Dictionary PlatformMapCache = new Dictionary(); - + public class PlatformMapping + { /// /// Updates the platform map from the embedded platform map resource /// @@ -27,7 +25,8 @@ namespace gaseous_server.Models { string rawJson = reader.ReadToEnd(); List platforms = new List(); - Newtonsoft.Json.JsonSerializerSettings jsonSerializerSettings = new JsonSerializerSettings{ + Newtonsoft.Json.JsonSerializerSettings jsonSerializerSettings = new JsonSerializerSettings + { MaxDepth = 64 }; platforms = Newtonsoft.Json.JsonConvert.DeserializeObject>(rawJson, jsonSerializerSettings); @@ -73,8 +72,8 @@ namespace gaseous_server.Models foreach (PlatformMapItem mapItem in platforms) { - // get the IGDB platform data - Platform platform = Platforms.GetPlatform(mapItem.IGDBId); + // insert dummy platform data - it'll be cleaned up on the first metadata refresh + Platform platform = CreateDummyPlatform(mapItem); try { @@ -92,74 +91,83 @@ namespace gaseous_server.Models } } } - + + private static IGDB.Models.Platform CreateDummyPlatform(PlatformMapItem mapItem) + { + IGDB.Models.Platform platform = new IGDB.Models.Platform + { + Id = mapItem.IGDBId, + Name = mapItem.IGDBName, + Slug = mapItem.IGDBSlug, + AlternativeName = mapItem.AlternateNames.FirstOrDefault() + }; + + if (Storage.GetCacheStatus("Platform", mapItem.IGDBId) == Storage.CacheStatus.NotPresent) + { + Storage.NewCacheValue(platform); + } + + return platform; + } + public static List PlatformMap { get { + // if (Database.DatabaseMemoryCache.GetCacheObject("PlatformMap") != null) + // { + // return (List)Database.DatabaseMemoryCache.GetCacheObject("PlatformMap"); + // } Database db = new Database(Database.databaseType.MySql, Config.DatabaseConfiguration.ConnectionString); string sql = "SELECT * FROM PlatformMap"; - DataTable data = db.ExecuteCMD(sql); + DataTable data = db.ExecuteCMD(sql); //, new Database.DatabaseMemoryCacheOptions(true, (int)TimeSpan.FromSeconds(5).Ticks)); List platformMaps = new List(); foreach (DataRow row in data.Rows) { long mapId = (long)row["Id"]; - if (PlatformMapCache.ContainsKey(mapId.ToString())) + + PlatformMapItem mapItem = BuildPlatformMapItem(row); + if (mapItem != null) { - PlatformMapItem mapItem = PlatformMapCache[mapId.ToString()]; - if (mapItem != null) - { - platformMaps.Add(mapItem); - } - } - else - { - PlatformMapItem mapItem = BuildPlatformMapItem(row); - if (mapItem != null) - { - platformMaps.Add(mapItem); - } + platformMaps.Add(mapItem); } } platformMaps.Sort((x, y) => x.IGDBName.CompareTo(y.IGDBName)); + //Database.DatabaseMemoryCache.SetCacheObject("PlatformMap", platformMaps, 600); + return platformMaps; } } public static PlatformMapItem GetPlatformMap(long Id) { - if (PlatformMapCache.ContainsKey(Id.ToString())) + Database db = new Database(Database.databaseType.MySql, Config.DatabaseConfiguration.ConnectionString); + string sql = "SELECT * FROM PlatformMap WHERE Id = @Id"; + Dictionary dbDict = new Dictionary(); + dbDict.Add("Id", Id); + DataTable data = db.ExecuteCMD(sql, dbDict); + + if (data.Rows.Count > 0) { - return PlatformMapCache[Id.ToString()]; + PlatformMapItem platformMap = BuildPlatformMapItem(data.Rows[0]); + + return platformMap; } else { - Database db = new Database(Database.databaseType.MySql, Config.DatabaseConfiguration.ConnectionString); - string sql = "SELECT * FROM PlatformMap WHERE Id = @Id"; - Dictionary dbDict = new Dictionary(); - dbDict.Add("Id", Id); - DataTable data = db.ExecuteCMD(sql, dbDict); - - if (data.Rows.Count > 0) - { - PlatformMapItem platformMap = BuildPlatformMapItem(data.Rows[0]); - - return platformMap; - } - else - { - Exception exception = new Exception("Platform Map Id " + Id + " does not exist."); - Logging.Log(Logging.LogType.Critical, "Platform Map", "Platform Map Id " + Id + " does not exist.", exception); - throw exception; - } + Exception exception = new Exception("Platform Map Id " + Id + " does not exist."); + Logging.Log(Logging.LogType.Critical, "Platform Map", "Platform Map Id " + Id + " does not exist.", exception); + throw exception; } } public static void WritePlatformMap(PlatformMapItem item, bool Update, bool AllowAvailableEmulatorOverwrite) { + CreateDummyPlatform(item); + Database db = new Database(Database.databaseType.MySql, Config.DatabaseConfiguration.ConnectionString); string sql = ""; Dictionary dbDict = new Dictionary(); @@ -238,23 +246,32 @@ namespace gaseous_server.Models { foreach (PlatformMapItem.EmulatorBiosItem biosItem in item.Bios) { - sql = "INSERT INTO PlatformMap_Bios (Id, Filename, Description, Hash) VALUES (@Id, @Filename, @Description, @Hash);"; + bool isEnabled = false; + if (item.EnabledBIOSHashes == null) + { + item.EnabledBIOSHashes = new List(); + } + if (item.EnabledBIOSHashes.Contains(biosItem.hash)) + { + isEnabled = true; + } + + sql = "INSERT INTO PlatformMap_Bios (Id, Filename, Description, Hash, Enabled) VALUES (@Id, @Filename, @Description, @Hash, @Enabled);"; dbDict.Clear(); dbDict.Add("Id", item.IGDBId); dbDict.Add("Filename", biosItem.filename); dbDict.Add("Description", biosItem.description); dbDict.Add("Hash", biosItem.hash); + dbDict.Add("Enabled", isEnabled); db.ExecuteCMD(sql, dbDict); } } - if (PlatformMapCache.ContainsKey(item.IGDBId.ToString())) - { - PlatformMapCache.Remove(item.IGDBId.ToString()); - } + // clear cache + Database.DatabaseMemoryCache.RemoveCacheObject("PlatformMap"); } - public static void WriteAvailableEmulators (PlatformMapItem item) + public static void WriteAvailableEmulators(PlatformMapItem item) { Database db = new Database(Database.databaseType.MySql, Config.DatabaseConfiguration.ConnectionString); string sql = ""; @@ -286,7 +303,16 @@ namespace gaseous_server.Models string sql = ""; // get platform data - IGDB.Models.Platform? platform = Platforms.GetPlatform(IGDBId); + // IGDB.Models.Platform? platform = Platforms.GetPlatform(IGDBId, false); + IGDB.Models.Platform? platform = null; + if (Storage.GetCacheStatus("Platform", IGDBId) == Storage.CacheStatus.NotPresent) + { + //platform = Platforms.GetPlatform(IGDBId, false); + } + else + { + platform = (IGDB.Models.Platform)Storage.GetCacheValue(new Platform(), "id", IGDBId); + } if (platform != null) { @@ -352,6 +378,7 @@ namespace gaseous_server.Models DataTable biosTable = db.ExecuteCMD(sql, dbDict); List bioss = new List(); + List enabledBios = new List(); foreach (DataRow biosRow in biosTable.Rows) { PlatformMapItem.EmulatorBiosItem bios = new PlatformMapItem.EmulatorBiosItem @@ -361,6 +388,11 @@ namespace gaseous_server.Models hash = ((string)Common.ReturnValueIfNull(biosRow["Hash"], "")).ToLower() }; bioss.Add(bios); + + if ((bool)Common.ReturnValueIfNull(biosRow["Enabled"], true) == true) + { + enabledBios.Add(bios.hash); + } } // build item @@ -369,26 +401,20 @@ namespace gaseous_server.Models mapItem.IGDBName = platform.Name; mapItem.IGDBSlug = platform.Slug; mapItem.AlternateNames = alternateNames; - mapItem.Extensions = new PlatformMapItem.FileExtensions{ + mapItem.Extensions = new PlatformMapItem.FileExtensions + { SupportedFileExtensions = knownExtensions, UniqueFileExtensions = uniqueExtensions }; mapItem.RetroPieDirectoryName = (string)Common.ReturnValueIfNull(row["RetroPieDirectoryName"], ""); - mapItem.WebEmulator = new PlatformMapItem.WebEmulatorItem{ + mapItem.WebEmulator = new PlatformMapItem.WebEmulatorItem + { Type = (string)Common.ReturnValueIfNull(row["WebEmulator_Type"], ""), Core = (string)Common.ReturnValueIfNull(row["WebEmulator_Core"], ""), AvailableWebEmulators = Newtonsoft.Json.JsonConvert.DeserializeObject>((string)Common.ReturnValueIfNull(row["AvailableWebEmulators"], "[]")) }; mapItem.Bios = bioss; - - if (PlatformMapCache.ContainsKey(IGDBId.ToString())) - { - PlatformMapCache[IGDBId.ToString()] = mapItem; - } - else - { - PlatformMapCache.Add(IGDBId.ToString(), mapItem); - } + mapItem.EnabledBIOSHashes = enabledBios; return mapItem; } @@ -455,13 +481,81 @@ namespace gaseous_server.Models } } + public PlatformMapItem GetUserPlatformMap(string UserId, long PlatformId, long GameId) + { + // get the system enabled bios hashes + Models.PlatformMapping.PlatformMapItem platformMapItem = PlatformMapping.GetPlatformMap(PlatformId); + + // get the user enabled bios hashes + PlatformMapping.UserEmulatorConfiguration userEmulatorConfiguration = GetUserEmulator(UserId, GameId, PlatformId); + if (userEmulatorConfiguration != null) + { + platformMapItem.WebEmulator.Type = userEmulatorConfiguration.EmulatorType; + platformMapItem.WebEmulator.Core = userEmulatorConfiguration.Core; + platformMapItem.EnabledBIOSHashes = userEmulatorConfiguration.EnableBIOSFiles; + } + + return platformMapItem; + } + + public UserEmulatorConfiguration GetUserEmulator(string UserId, long GameId, long PlatformId) + { + Database db = new Database(Database.databaseType.MySql, Config.DatabaseConfiguration.ConnectionString); + string sql = "SELECT Mapping FROM User_PlatformMap WHERE id = @UserId AND GameId = @GameId AND PlatformId = @PlatformId;"; + Dictionary dbDict = new Dictionary + { + { "UserId", UserId }, + { "GameId", GameId }, + { "PlatformId", PlatformId } + }; + DataTable data = db.ExecuteCMD(sql, dbDict); + + if (data.Rows.Count > 0) + { + UserEmulatorConfiguration emulator = Newtonsoft.Json.JsonConvert.DeserializeObject((string)data.Rows[0]["Mapping"]); + + return emulator; + } + else + { + return null; + } + } + + public void SetUserEmulator(string UserId, long GameId, long PlatformId, UserEmulatorConfiguration Mapping) + { + Database db = new Database(Database.databaseType.MySql, Config.DatabaseConfiguration.ConnectionString); + string sql = "INSERT INTO User_PlatformMap (id, GameId, PlatformId, Mapping) VALUES (@UserId, @GameId, @PlatformId, @Mapping) ON DUPLICATE KEY UPDATE Mapping = @Mapping;"; + Dictionary dbDict = new Dictionary + { + { "UserId", UserId }, + { "GameId", GameId }, + { "PlatformId", PlatformId }, + { "Mapping", Newtonsoft.Json.JsonConvert.SerializeObject(Mapping) } + }; + db.ExecuteCMD(sql, dbDict); + } + + public void DeleteUserEmulator(string UserId, long GameId, long PlatformId) + { + Database db = new Database(Database.databaseType.MySql, Config.DatabaseConfiguration.ConnectionString); + string sql = "DELETE FROM User_PlatformMap WHERE id = @UserId AND GameId = @GameId AND PlatformId = @PlatformId;"; + Dictionary dbDict = new Dictionary + { + { "UserId", UserId }, + { "GameId", GameId }, + { "PlatformId", PlatformId } + }; + db.ExecuteCMD(sql, dbDict); + } + public class PlatformMapItem { public long IGDBId { get; set; } public string IGDBName { get; set; } public string IGDBSlug { get; set; } public List AlternateNames { get; set; } = new List(); - + public FileExtensions Extensions { get; set; } public class FileExtensions { @@ -502,7 +596,16 @@ namespace gaseous_server.Models public string description { get; set; } public string filename { get; set; } } + + public List EnabledBIOSHashes { get; set; } } - } + + public class UserEmulatorConfiguration + { + public string EmulatorType { get; set; } + public string Core { get; set; } + public List EnableBIOSFiles { get; set; } = new List(); + } + } } diff --git a/gaseous-server/Models/Signatures_Sources.cs b/gaseous-server/Models/Signatures_Sources.cs new file mode 100644 index 0000000..d1d9192 --- /dev/null +++ b/gaseous-server/Models/Signatures_Sources.cs @@ -0,0 +1,20 @@ +using NuGet.Protocol.Core.Types; + +namespace gaseous_server.Models +{ + public class Signatures_Sources + { + public int Id { get; set; } + public string Name { get; set; } + public string Description { get; set; } + public string URL { get; set; } + public string Category { get; set; } + public string Version { get; set; } + public string Author { get; set; } + public string Email { get; set; } + public string Homepage { get; set; } + public gaseous_signature_parser.parser.SignatureParser SourceType { get; set; } + public string MD5 { get; set; } + public string SHA1 { get; set; } + } +} \ No newline at end of file diff --git a/gaseous-server/Models/UserProfile.cs b/gaseous-server/Models/UserProfile.cs new file mode 100644 index 0000000..a9e4f74 --- /dev/null +++ b/gaseous-server/Models/UserProfile.cs @@ -0,0 +1,17 @@ +namespace gaseous_server.Models +{ + public class UserProfile + { + public Guid UserId { get; set; } + public string DisplayName { get; set; } + public string Quip { get; set; } + public ProfileImageItem? Avatar { get; set; } + public ProfileImageItem? ProfileBackground { get; set; } + public Dictionary Data { get; set; } + public class ProfileImageItem + { + public string MimeType { get; set; } + public string Extension { get; set; } + } + } +} \ No newline at end of file diff --git a/gaseous-server/Program.cs b/gaseous-server/Program.cs index 15a759e..dcd556c 100644 --- a/gaseous-server/Program.cs +++ b/gaseous-server/Program.cs @@ -36,6 +36,9 @@ db = new Database(Database.databaseType.MySql, Config.DatabaseConfiguration.Conn // set up db db.InitDB(); +// create relation tables if they don't exist +Storage.CreateRelationsTables(); +Storage.CreateRelationsTables(); // populate db with static data for lookups AgeRatings.PopulateAgeMap(); @@ -71,7 +74,7 @@ ProcessQueue.QueueItem queueItem = new ProcessQueue.QueueItem( 1, new List { - ProcessQueue.QueueItemType.All + ProcessQueue.QueueItemType.SignatureIngestor }, false, true @@ -398,9 +401,6 @@ gaseous_server.Classes.Metadata.Platforms.AssignAllPlatformsToGameIdZero(); // extract platform map if not present PlatformMapping.ExtractPlatformMap(); -// force load platform map into cache -var platformMap = PlatformMapping.PlatformMap; - // add background tasks ProcessQueue.QueueItems.Add(new ProcessQueue.QueueItem( ProcessQueue.QueueItemType.SignatureIngestor) diff --git a/gaseous-server/Support/Database/MySQL/gaseous-1022.sql b/gaseous-server/Support/Database/MySQL/gaseous-1022.sql index b089e9c..c88f8ba 100644 --- a/gaseous-server/Support/Database/MySQL/gaseous-1022.sql +++ b/gaseous-server/Support/Database/MySQL/gaseous-1022.sql @@ -1,40 +1 @@ -CREATE TABLE `Signatures_RomToSource` ( - `SourceId` int NOT NULL, - `RomId` int NOT NULL, - PRIMARY KEY (`SourceId`, `RomId`) -); - -CREATE TABLE `Signatures_Games_Countries` ( - `GameId` INT NOT NULL, - `CountryId` INT NOT NULL, - PRIMARY KEY (`GameId`, `CountryId`), - CONSTRAINT `GameCountry` FOREIGN KEY (`GameId`) REFERENCES `Signatures_Games` (`Id`) ON DELETE CASCADE ON UPDATE NO ACTION -); - -CREATE TABLE `Signatures_Games_Languages` ( - `GameId` INT NOT NULL, - `LanguageId` INT NOT NULL, - PRIMARY KEY (`GameId`, `LanguageId`), - CONSTRAINT `GameLanguage` FOREIGN KEY (`GameId`) REFERENCES `Signatures_Games` (`Id`) ON DELETE CASCADE ON UPDATE NO ACTION -); - -CREATE TABLE `Country` ( - `Id` INT NOT NULL AUTO_INCREMENT, - `Code` VARCHAR(20) NULL, - `Value` VARCHAR(255) NULL, - PRIMARY KEY (`Id`), - INDEX `id_Code` (`Code` ASC) VISIBLE, - INDEX `id_Value` (`Value` ASC) VISIBLE -); - -CREATE TABLE `Language` ( - `Id` INT NOT NULL AUTO_INCREMENT, - `Code` VARCHAR(20) NULL, - `Value` VARCHAR(255) NULL, - PRIMARY KEY (`Id`), - INDEX `id_Code` (`Code` ASC) VISIBLE, - INDEX `id_Value` (`Value` ASC) VISIBLE -); - -ALTER TABLE `Games_Roms` -ADD COLUMN `RomDataVersion` INT DEFAULT 1; \ No newline at end of file +ALTER TABLE `Platform` CHANGE `Name` `Name` varchar(255); \ No newline at end of file diff --git a/gaseous-server/Support/Database/MySQL/gaseous-1023.sql b/gaseous-server/Support/Database/MySQL/gaseous-1023.sql new file mode 100644 index 0000000..9307882 --- /dev/null +++ b/gaseous-server/Support/Database/MySQL/gaseous-1023.sql @@ -0,0 +1,97 @@ +CREATE TABLE `Signatures_RomToSource` ( + `SourceId` int NOT NULL, + `RomId` int NOT NULL, + PRIMARY KEY (`SourceId`, `RomId`) +); + +CREATE TABLE `Signatures_Games_Countries` ( + `GameId` INT NOT NULL, + `CountryId` INT NOT NULL, + PRIMARY KEY (`GameId`, `CountryId`), + CONSTRAINT `GameCountry` FOREIGN KEY (`GameId`) REFERENCES `Signatures_Games` (`Id`) ON DELETE CASCADE ON UPDATE NO ACTION +); + +CREATE TABLE `Signatures_Games_Languages` ( + `GameId` INT NOT NULL, + `LanguageId` INT NOT NULL, + PRIMARY KEY (`GameId`, `LanguageId`), + CONSTRAINT `GameLanguage` FOREIGN KEY (`GameId`) REFERENCES `Signatures_Games` (`Id`) ON DELETE CASCADE ON UPDATE NO ACTION +); + +CREATE TABLE `Country` ( + `Id` INT NOT NULL AUTO_INCREMENT, + `Code` VARCHAR(20) NULL, + `Value` VARCHAR(255) NULL, + PRIMARY KEY (`Id`), + INDEX `id_Code` (`Code` ASC) VISIBLE, + INDEX `id_Value` (`Value` ASC) VISIBLE +); + +CREATE TABLE `Language` ( + `Id` INT NOT NULL AUTO_INCREMENT, + `Code` VARCHAR(20) NULL, + `Value` VARCHAR(255) NULL, + PRIMARY KEY (`Id`), + INDEX `id_Code` (`Code` ASC) VISIBLE, + INDEX `id_Value` (`Value` ASC) VISIBLE +); + +ALTER TABLE `Games_Roms` ADD COLUMN `RomDataVersion` INT DEFAULT 1; + +CREATE TABLE UserProfiles ( + `Id` VARCHAR(45) NOT NULL, + `UserId` VARCHAR(45) NOT NULL, + `DisplayName` VARCHAR(255) NOT NULL, + `Quip` VARCHAR(255) NOT NULL, + `Avatar` LONGBLOB, + `AvatarExtension` CHAR(6), + `ProfileBackground` LONGBLOB, + `ProfileBackgroundExtension` CHAR(6), + `UnstructuredData` LONGTEXT NOT NULL, + PRIMARY KEY (`Id`, `UserId`) +); + +ALTER TABLE `PlatformMap_Bios` +ADD COLUMN `Enabled` BOOLEAN DEFAULT TRUE; + +CREATE TABLE `User_PlatformMap` ( + `id` VARCHAR(128) NOT NULL, + `GameId` BIGINT NOT NULL, + `PlatformId` BIGINT NOT NULL, + `Mapping` LONGTEXT, + PRIMARY KEY (`id`, `GameId`, `PlatformId`), + CONSTRAINT `User_PlatformMap_UserId` FOREIGN KEY (`id`) REFERENCES `Users` (`Id`) ON DELETE CASCADE +); + +ALTER TABLE `UserTimeTracking` +ADD COLUMN `PlatformId` BIGINT, +ADD COLUMN `IsMediaGroup` BOOLEAN DEFAULT FALSE, +ADD COLUMN `RomId` BIGINT; + +CREATE TABLE `User_RecentPlayedRoms` ( + `UserId` varchar(128) NOT NULL, + `GameId` bigint(20) NOT NULL, + `PlatformId` bigint(20) NOT NULL, + `RomId` bigint(20) NOT NULL, + `IsMediaGroup` tinyint(1) DEFAULT NULL, + PRIMARY KEY ( + `UserId`, + `GameId`, + `PlatformId` + ), + CONSTRAINT `RecentPlayedRoms_Users` FOREIGN KEY (`UserId`) REFERENCES `Users` (`Id`) ON DELETE CASCADE +); + +CREATE TABLE `User_GameFavouriteRoms` ( + `UserId` varchar(128) NOT NULL, + `GameId` bigint(20) NOT NULL, + `PlatformId` bigint(20) NOT NULL, + `RomId` bigint(20) NOT NULL, + `IsMediaGroup` tinyint(1) DEFAULT NULL, + PRIMARY KEY ( + `UserId`, + `GameId`, + `PlatformId` + ), + CONSTRAINT `GameFavouriteRoms_Users` FOREIGN KEY (`UserId`) REFERENCES `Users` (`Id`) ON DELETE CASCADE +); \ No newline at end of file diff --git a/gaseous-server/Support/PlatformMap.json b/gaseous-server/Support/PlatformMap.json index 155c0d6..b4c9210 100644 --- a/gaseous-server/Support/PlatformMap.json +++ b/gaseous-server/Support/PlatformMap.json @@ -1,1870 +1,4145 @@ [ - { - "igdbId": 50, - "igdbName": "3DO Interactive Multiplayer", - "igdbSlug": "3do", - "alternateNames": [ - "3DO", - "3DO Interactive Multiplayer" - ], - "extensions": { - "supportedFileExtensions": [ - ".CHD", - ".CUE", - ".ISO", - ".ZIP" - ], - "uniqueFileExtensions": [] - }, - "retroPieDirectoryName": "3do", - "webEmulator": { - "type": "EmulatorJS", - "core": "3do", - "availableWebEmulators": [ - { - "emulatorType": "EmulatorJS", - "availableWebEmulatorCores": [ - { - "core": "3do", - "alternateCoreName": "opera", - "default": true - } - ] - } - ] - }, - "bios": [ + { + "IGDBId": 139, + "IGDBName": "1292 Advanced Programmable Video System", + "IGDBSlug": "1292-advanced-programmable-video-system", + "AlternateNames": [], + "Extensions": { + "SupportedFileExtensions": [], + "UniqueFileExtensions": [] + }, + "RetroPieDirectoryName": "", + "WebEmulator": { + "Type": "", + "Core": "", + "AvailableWebEmulators": null + }, + "Bios": [] + }, + { + "IGDBId": 50, + "IGDBName": "3DO Interactive Multiplayer", + "IGDBSlug": "3do", + "AlternateNames": [ + "3DO", + "3DO Interactive Multiplayer" + ], + "Extensions": { + "SupportedFileExtensions": [ + ".CHD", + ".CUE", + ".ISO", + ".ZIP" + ], + "UniqueFileExtensions": [] + }, + "RetroPieDirectoryName": "3do", + "WebEmulator": { + "Type": "EmulatorJS", + "Core": "3do", + "AvailableWebEmulators": [ + { + "EmulatorType": "EmulatorJS", + "AvailableWebEmulatorCores": [ { - "hash": "8970fc987ab89a7f64da9f8a8c4333ff", - "description": "Shootout At Old Tucson", - "filename": "3do_arcade_saot.bin" - }, - { - "hash": "8639fd5e549bd6238cfee79e3e749114", - "description": "Goldstar GDO-101M", - "filename": "goldstar.bin" - }, - { - "hash": "f47264dd47fe30f73ab3c010015c155b", - "description": "Panasonic FZ-1", - "filename": "panafz1.bin" - }, - { - "hash": "1477bda80dc33731a65468c1f5bcbee9", - "description": "Panasonic FZ-10 [RSA Patch]", - "filename": "panafz10-norsa.bin" - }, - { - "hash": "51f2f43ae2f3508a14d9f56597e2d3ce", - "description": "Panasonic FZ-10", - "filename": "panafz10.bin" - }, - { - "hash": "cf11bbb5a16d7af9875cca9de9a15e09", - "description": "Panasonic FZ-10-E [Anvil RSA Patch]", - "filename": "panafz10e-anvil-norsa.bin" - }, - { - "hash": "a48e6746bd7edec0f40cff078f0bb19f", - "description": "Panasonic FZ-10-E [Anvil]", - "filename": "panafz10e-anvil.bin" - }, - { - "hash": "f6c71de7470d16abe4f71b1444883dc8", - "description": "Panasonic FZ-1J [RSA Patch]", - "filename": "panafz1j-norsa.bin" - }, - { - "hash": "a496cfdded3da562759be3561317b605", - "description": "Panasonic FZ-1J", - "filename": "panafz1j.bin" - }, - { - "hash": "35fa1a1ebaaeea286dc5cd15487c13ea", - "description": "Sanyo IMP-21J TRY", - "filename": "sanyotry.bin" + "Core": "3do", + "AlternateCoreName": "opera", + "Default": true } - ] + ] + } + ] }, - { - "igdbId": 16, - "igdbName": "Amiga", - "igdbSlug": "amiga", - "alternateNames": [ - "Amiga", - "Commodore Amiga" - ], - "extensions": { - "supportedFileExtensions": [ - ".ADF", - ".ADZ", - ".DMS", - ".FDI", - ".HDF", - ".HDZ", - ".IPF", - ".LHA", - ".UAE", - ".ZIP" - ], - "uniqueFileExtensions": [ - ".ADF", - ".ADZ", - ".DMS", - ".FDI", - ".HDF", - ".HDZ", - ".LHA", - ".UAE" - ] - }, - "retroPieDirectoryName": "amiga", - "webEmulator": { - "type": "EmulatorJS", - "core": "amiga", - "availableWebEmulators": [ - { - "emulatorType": "EmulatorJS", - "availableWebEmulatorCores": [ - { - "core": "amiga", - "alternateCoreName": "puae", - "default": true - } - ] - } - ] - }, - "bios": [ + "Bios": [ + { + "hash": "8970fc987ab89a7f64da9f8a8c4333ff", + "description": "Shootout At Old Tucson", + "filename": "3do_arcade_saot.bin" + }, + { + "hash": "8639fd5e549bd6238cfee79e3e749114", + "description": "Goldstar GDO-101M", + "filename": "goldstar.bin" + }, + { + "hash": "f47264dd47fe30f73ab3c010015c155b", + "description": "Panasonic FZ-1", + "filename": "panafz1.bin" + }, + { + "hash": "1477bda80dc33731a65468c1f5bcbee9", + "description": "Panasonic FZ-10 [RSA Patch]", + "filename": "panafz10-norsa.bin" + }, + { + "hash": "51f2f43ae2f3508a14d9f56597e2d3ce", + "description": "Panasonic FZ-10", + "filename": "panafz10.bin" + }, + { + "hash": "cf11bbb5a16d7af9875cca9de9a15e09", + "description": "Panasonic FZ-10-E [Anvil RSA Patch]", + "filename": "panafz10e-anvil-norsa.bin" + }, + { + "hash": "a48e6746bd7edec0f40cff078f0bb19f", + "description": "Panasonic FZ-10-E [Anvil]", + "filename": "panafz10e-anvil.bin" + }, + { + "hash": "f6c71de7470d16abe4f71b1444883dc8", + "description": "Panasonic FZ-1J [RSA Patch]", + "filename": "panafz1j-norsa.bin" + }, + { + "hash": "a496cfdded3da562759be3561317b605", + "description": "Panasonic FZ-1J", + "filename": "panafz1j.bin" + }, + { + "hash": "35fa1a1ebaaeea286dc5cd15487c13ea", + "description": "Sanyo IMP-21J TRY", + "filename": "sanyotry.bin" + } + ] + }, + { + "IGDBId": 116, + "IGDBName": "Acorn Archimedes", + "IGDBSlug": "acorn-archimedes", + "AlternateNames": [], + "Extensions": { + "SupportedFileExtensions": [], + "UniqueFileExtensions": [] + }, + "RetroPieDirectoryName": "", + "WebEmulator": { + "Type": "", + "Core": "", + "AvailableWebEmulators": null + }, + "Bios": [] + }, + { + "IGDBId": 134, + "IGDBName": "Acorn Electron", + "IGDBSlug": "acorn-electron", + "AlternateNames": [], + "Extensions": { + "SupportedFileExtensions": [], + "UniqueFileExtensions": [] + }, + "RetroPieDirectoryName": "", + "WebEmulator": { + "Type": "", + "Core": "", + "AvailableWebEmulators": null + }, + "Bios": [] + }, + { + "IGDBId": 16, + "IGDBName": "Amiga", + "IGDBSlug": "amiga", + "AlternateNames": [ + "Amiga", + "Commodore Amiga" + ], + "Extensions": { + "SupportedFileExtensions": [ + ".ADF", + ".ADZ", + ".DMS", + ".FDI", + ".HDF", + ".HDZ", + ".IPF", + ".LHA", + ".UAE", + ".ZIP" + ], + "UniqueFileExtensions": [ + ".ADF", + ".ADZ", + ".DMS", + ".FDI", + ".HDF", + ".HDZ", + ".LHA", + ".UAE" + ] + }, + "RetroPieDirectoryName": "amiga", + "WebEmulator": { + "Type": "EmulatorJS", + "Core": "amiga", + "AvailableWebEmulators": [ + { + "EmulatorType": "EmulatorJS", + "AvailableWebEmulatorCores": [ { - "hash": "85ad74194e87c08904327de1a9443b7a", - "description": "Kickstart v1.2 rev 33.180", - "filename": "kick33180.A500" - }, - { - "hash": "82a21c1890cae844b3df741f2762d48d", - "description": "Kickstart v1.3 rev 34.005", - "filename": "kick34005.A500" - }, - { - "hash": "89da1838a24460e4b93f4f0c5d92d48d", - "description": "CDTV extended ROM v1.00", - "filename": "kick34005.CDTV" - }, - { - "hash": "dc10d7bdd1b6f450773dfb558477c230", - "description": "Kickstart v2.04 rev 37.175", - "filename": "kick37175.A500" - }, - { - "hash": "5f8924d013dd57a89cf349f4cdedc6b1", - "description": "CD32 Kickstart v3.1 rev 40.060", - "filename": "kick40060.CD32" - }, - { - "hash": "f2f241bf094168cfb9e7805dc2856433", - "description": "CD32 KS + extended v3.1 rev 40.060", - "filename": "kick40060.CD32" - }, - { - "hash": "bb72565701b1b6faece07d68ea5da639", - "description": "CD32 extended ROM rev 40.060", - "filename": "kick40060.CD32.ext" - }, - { - "hash": "e40a5dfb3d017ba8779faba30cbd1c8e", - "description": "Kickstart v3.1 rev 40.063", - "filename": "kick40063.A600" - }, - { - "hash": "646773759326fbac3b2311fd8c8793ee", - "description": "Kickstart v3.1 rev 40.068", - "filename": "kick40068.A1200" - }, - { - "hash": "9bdedde6a4f33555b4a270c8ca53297d", - "description": "Kickstart v3.1 rev 40.068", - "filename": "kick40068.A4000" + "Core": "amiga", + "AlternateCoreName": "puae", + "Default": true } - ] + ] + } + ] }, - { - "igdbId": 114, - "igdbName": "Amiga CD32", - "igdbSlug": "amiga-cd32", - "alternateNames": [ - "Amiga CD32", - "Commodore Amiga CD32" - ], - "extensions": { - "supportedFileExtensions": [ - ".ZIP" - ], - "uniqueFileExtensions": [ - ] - }, - "retroPieDirectoryName": "amiga", - "webEmulator": { - "type": "EmulatorJS", - "core": "amiga", - "availableWebEmulators": [ - { - "emulatorType": "EmulatorJS", - "availableWebEmulatorCores": [ - { - "core": "amiga", - "alternateCoreName": "puae", - "default": true - } - ] - } - ] - }, - "bios": [ + "Bios": [ + { + "hash": "85ad74194e87c08904327de1a9443b7a", + "description": "Kickstart v1.2 rev 33.180", + "filename": "kick33180.A500" + }, + { + "hash": "82a21c1890cae844b3df741f2762d48d", + "description": "Kickstart v1.3 rev 34.005", + "filename": "kick34005.A500" + }, + { + "hash": "89da1838a24460e4b93f4f0c5d92d48d", + "description": "CDTV extended ROM v1.00", + "filename": "kick34005.CDTV" + }, + { + "hash": "dc10d7bdd1b6f450773dfb558477c230", + "description": "Kickstart v2.04 rev 37.175", + "filename": "kick37175.A500" + }, + { + "hash": "5f8924d013dd57a89cf349f4cdedc6b1", + "description": "CD32 Kickstart v3.1 rev 40.060", + "filename": "kick40060.CD32" + }, + { + "hash": "f2f241bf094168cfb9e7805dc2856433", + "description": "CD32 KS + extended v3.1 rev 40.060", + "filename": "kick40060.CD32" + }, + { + "hash": "bb72565701b1b6faece07d68ea5da639", + "description": "CD32 extended ROM rev 40.060", + "filename": "kick40060.CD32.ext" + }, + { + "hash": "e40a5dfb3d017ba8779faba30cbd1c8e", + "description": "Kickstart v3.1 rev 40.063", + "filename": "kick40063.A600" + }, + { + "hash": "646773759326fbac3b2311fd8c8793ee", + "description": "Kickstart v3.1 rev 40.068", + "filename": "kick40068.A1200" + }, + { + "hash": "9bdedde6a4f33555b4a270c8ca53297d", + "description": "Kickstart v3.1 rev 40.068", + "filename": "kick40068.A4000" + } + ] + }, + { + "IGDBId": 114, + "IGDBName": "Amiga CD32", + "IGDBSlug": "amiga-cd32", + "AlternateNames": [ + "Amiga CD32", + "Commodore Amiga CD32" + ], + "Extensions": { + "SupportedFileExtensions": [ + ".ZIP" + ], + "UniqueFileExtensions": [] + }, + "RetroPieDirectoryName": "amiga", + "WebEmulator": { + "Type": "EmulatorJS", + "Core": "amiga", + "AvailableWebEmulators": [ + { + "EmulatorType": "EmulatorJS", + "AvailableWebEmulatorCores": [ { - "hash": "85ad74194e87c08904327de1a9443b7a", - "description": "Kickstart v1.2 rev 33.180", - "filename": "kick33180.A500" - }, - { - "hash": "82a21c1890cae844b3df741f2762d48d", - "description": "Kickstart v1.3 rev 34.005", - "filename": "kick34005.A500" - }, - { - "hash": "89da1838a24460e4b93f4f0c5d92d48d", - "description": "CDTV extended ROM v1.00", - "filename": "kick34005.CDTV" - }, - { - "hash": "dc10d7bdd1b6f450773dfb558477c230", - "description": "Kickstart v2.04 rev 37.175", - "filename": "kick37175.A500" - }, - { - "hash": "5f8924d013dd57a89cf349f4cdedc6b1", - "description": "CD32 Kickstart v3.1 rev 40.060", - "filename": "kick40060.CD32" - }, - { - "hash": "f2f241bf094168cfb9e7805dc2856433", - "description": "CD32 KS + extended v3.1 rev 40.060", - "filename": "kick40060.CD32" - }, - { - "hash": "bb72565701b1b6faece07d68ea5da639", - "description": "CD32 extended ROM rev 40.060", - "filename": "kick40060.CD32.ext" - }, - { - "hash": "e40a5dfb3d017ba8779faba30cbd1c8e", - "description": "Kickstart v3.1 rev 40.063", - "filename": "kick40063.A600" - }, - { - "hash": "646773759326fbac3b2311fd8c8793ee", - "description": "Kickstart v3.1 rev 40.068", - "filename": "kick40068.A1200" - }, - { - "hash": "9bdedde6a4f33555b4a270c8ca53297d", - "description": "Kickstart v3.1 rev 40.068", - "filename": "kick40068.A4000" + "Core": "amiga", + "AlternateCoreName": "puae", + "Default": true } - ] + ] + } + ] }, - { - "igdbId": 25, - "igdbName": "Amstrad CPC", - "igdbSlug": "acpc", - "alternateNames": [ - "acpc", - "Amstrad CPC", - "Colour Personal Computer" - ], - "extensions": { - "supportedFileExtensions": [ - ".CPC", - ".DSK" - ], - "uniqueFileExtensions": [ - ".CPC" - ] - }, - "retroPieDirectoryName": "amstradcpc", - "webEmulator": { - "type": "", - "core": "" - }, - "bios": [] + "Bios": [ + { + "hash": "85ad74194e87c08904327de1a9443b7a", + "description": "Kickstart v1.2 rev 33.180", + "filename": "kick33180.A500" + }, + { + "hash": "82a21c1890cae844b3df741f2762d48d", + "description": "Kickstart v1.3 rev 34.005", + "filename": "kick34005.A500" + }, + { + "hash": "89da1838a24460e4b93f4f0c5d92d48d", + "description": "CDTV extended ROM v1.00", + "filename": "kick34005.CDTV" + }, + { + "hash": "dc10d7bdd1b6f450773dfb558477c230", + "description": "Kickstart v2.04 rev 37.175", + "filename": "kick37175.A500" + }, + { + "hash": "5f8924d013dd57a89cf349f4cdedc6b1", + "description": "CD32 Kickstart v3.1 rev 40.060", + "filename": "kick40060.CD32" + }, + { + "hash": "f2f241bf094168cfb9e7805dc2856433", + "description": "CD32 KS + extended v3.1 rev 40.060", + "filename": "kick40060.CD32" + }, + { + "hash": "bb72565701b1b6faece07d68ea5da639", + "description": "CD32 extended ROM rev 40.060", + "filename": "kick40060.CD32.ext" + }, + { + "hash": "e40a5dfb3d017ba8779faba30cbd1c8e", + "description": "Kickstart v3.1 rev 40.063", + "filename": "kick40063.A600" + }, + { + "hash": "646773759326fbac3b2311fd8c8793ee", + "description": "Kickstart v3.1 rev 40.068", + "filename": "kick40068.A1200" + }, + { + "hash": "9bdedde6a4f33555b4a270c8ca53297d", + "description": "Kickstart v3.1 rev 40.068", + "filename": "kick40068.A4000" + } + ] + }, + { + "IGDBId": 25, + "IGDBName": "Amstrad CPC", + "IGDBSlug": "acpc", + "AlternateNames": [ + "acpc", + "Amstrad CPC", + "Colour Personal Computer" + ], + "Extensions": { + "SupportedFileExtensions": [ + ".CPC", + ".DSK" + ], + "UniqueFileExtensions": [ + ".CPC" + ] }, - { - "igdbId": 75, - "igdbName": "Apple II", - "igdbSlug": "appleii", - "alternateNames": [ - "Apple ][", - "Apple 2", - "Apple II", - "appleii" - ], - "extensions": { - "supportedFileExtensions": [ - ".DSK" - ], - "uniqueFileExtensions": [] - }, - "retroPieDirectoryName": "apple2", - "webEmulator": { - "type": "", - "core": "" - }, - "bios": [] + "RetroPieDirectoryName": "amstradcpc", + "WebEmulator": { + "Type": "", + "Core": "", + "AvailableWebEmulators": [] }, - { - "igdbId": 52, - "igdbName": "Arcade", - "igdbSlug": "arcade", - "alternateNames": [ - "Arcade" - ], - "extensions": { - "supportedFileExtensions": [ - ".ZIP" - ], - "uniqueFileExtensions": [] - }, - "retroPieDirectoryName": "", - "webEmulator": { - "type": "EmulatorJS", - "core": "arcade", - "availableWebEmulators": [ - { - "emulatorType": "EmulatorJS", - "availableWebEmulatorCores": [ - { - "core": "arcade", - "alternateCoreName": "fbneo", - "default": true - }, - { - "core": "fbalpha2012_cps1" - }, - { - "core": "fbalpha2012_cps2" - }, - { - "core": "mame" - } - ] - } - ] - }, - "bios": [] + "Bios": [] + }, + { + "IGDBId": 154, + "IGDBName": "Amstrad PCW", + "IGDBSlug": "amstrad-pcw", + "AlternateNames": [], + "Extensions": { + "SupportedFileExtensions": [], + "UniqueFileExtensions": [] }, - { - "igdbId": 59, - "igdbName": "Atari 2600", - "igdbSlug": "atari2600", - "alternateNames": [ - "Atari 2600", - "Atari VCS", - "atari2600" - ], - "extensions": { - "supportedFileExtensions": [ - ".7Z", - ".A26", - ".BIN", - ".GZ", - ".ROM", - ".ZIP" - ], - "uniqueFileExtensions": [ - ".A26" - ] - }, - "retroPieDirectoryName": "atari2600", - "webEmulator": { - "type": "EmulatorJS", - "core": "atari2600", - "availableWebEmulators": [ - { - "emulatorType": "EmulatorJS", - "availableWebEmulatorCores": [ - { - "core": "atari2600", - "alternateCoreName": "stella2014", - "default": true - } - ] - } - ] - }, - "bios": [] + "RetroPieDirectoryName": "", + "WebEmulator": { + "Type": "", + "Core": "", + "AvailableWebEmulators": null }, - { - "igdbId": 66, - "igdbName": "Atari 5200", - "igdbSlug": "atari5200", - "alternateNames": [ - "Atari 5200", - "Atari 5200 SuperSystem", - "atari5200" - ], - "extensions": { - "supportedFileExtensions": [ - ".A52", - ".ATR", - ".ATR.GZ", - ".BAS", - ".BIN", - ".CAR", - ".DCM", - ".XEX", - ".XFD", - ".XFD.GZ" - ], - "uniqueFileExtensions": [ - ".A52", - ".ATR", - ".ATR.GZ", - ".BAS", - ".CAR", - ".DCM", - ".XEX", - ".XFD", - ".XFD.GZ" - ] - }, - "retroPieDirectoryName": "atari5200", - "webEmulator": { - "type": "EmulatorJS", - "core": "atari5200", - "availableWebEmulators": [ - { - "emulatorType": "EmulatorJS", - "availableWebEmulatorCores": [ - { - "core": "atari5200", - "alternateCoreName": "a5200", - "default": true - } - ] - } - ] - }, - "bios": [ + "Bios": [] + }, + { + "IGDBId": 34, + "IGDBName": "Android", + "IGDBSlug": "android", + "AlternateNames": [ + "Infocusa3" + ], + "Extensions": { + "SupportedFileExtensions": [], + "UniqueFileExtensions": [] + }, + "RetroPieDirectoryName": "", + "WebEmulator": { + "Type": "", + "Core": "", + "AvailableWebEmulators": null + }, + "Bios": [] + }, + { + "IGDBId": 75, + "IGDBName": "Apple II", + "IGDBSlug": "appleii", + "AlternateNames": [ + "Apple 2", + "Apple II", + "Apple ][", + "appleii" + ], + "Extensions": { + "SupportedFileExtensions": [ + ".DSK" + ], + "UniqueFileExtensions": [] + }, + "RetroPieDirectoryName": "apple2", + "WebEmulator": { + "Type": "", + "Core": "", + "AvailableWebEmulators": [] + }, + "Bios": [] + }, + { + "IGDBId": 115, + "IGDBName": "Apple IIGS", + "IGDBSlug": "apple-iigs", + "AlternateNames": [], + "Extensions": { + "SupportedFileExtensions": [], + "UniqueFileExtensions": [] + }, + "RetroPieDirectoryName": "", + "WebEmulator": { + "Type": "", + "Core": "", + "AvailableWebEmulators": null + }, + "Bios": [] + }, + { + "IGDBId": 52, + "IGDBName": "Arcade", + "IGDBSlug": "arcade", + "AlternateNames": [ + "Arcade" + ], + "Extensions": { + "SupportedFileExtensions": [ + ".ZIP" + ], + "UniqueFileExtensions": [] + }, + "RetroPieDirectoryName": "", + "WebEmulator": { + "Type": "EmulatorJS", + "Core": "arcade", + "AvailableWebEmulators": [ + { + "EmulatorType": "EmulatorJS", + "AvailableWebEmulatorCores": [ { - "hash": "281f20ea4320404ec820fb7ec0693b38", - "description": "BIOS for the Atari 5200", - "filename": "5200.rom" + "Core": "arcade", + "AlternateCoreName": "fbneo", + "Default": true + }, + { + "Core": "fbalpha2012_cps1", + "AlternateCoreName": "", + "Default": false + }, + { + "Core": "fbalpha2012_cps2", + "AlternateCoreName": "", + "Default": false + }, + { + "Core": "mame", + "AlternateCoreName": "", + "Default": false } - ] + ] + } + ] }, - { - "igdbId": 60, - "igdbName": "Atari 7800", - "igdbSlug": "atari7800", - "alternateNames": [ - "Atari 7800", - "Atari 7800 ProSystem", - "Atari VCS", - "atari7800" - ], - "extensions": { - "supportedFileExtensions": [ - ".7Z", - ".A78", - ".BIN", - ".ZIP" - ], - "uniqueFileExtensions": [ - ".A78" - ] - }, - "retroPieDirectoryName": "atari7800", - "webEmulator": { - "type": "EmulatorJS", - "core": "atari7800", - "availableWebEmulators": [ - { - "emulatorType": "EmulatorJS", - "availableWebEmulatorCores": [ - { - "core": "atari7800", - "alternateCoreName": "prosystem", - "default": true - } - ] - } - ] - }, - "bios": [ + "Bios": [ + { + "hash": "7a11bfe0cc72886d032e386db68f890c", + "description": "", + "filename": "airlbios.zip" + }, + { + "hash": "85254fbe320ca82a768ec2c26bb08def", + "description": "", + "filename": "awbios.zip" + }, + { + "hash": "f81298afd68a1a24a49a1a2d9f087964", + "description": "", + "filename": "bubsys.zip" + }, + { + "hash": "df6f8a3d83c028a5cb9f2f2be60773f3", + "description": "", + "filename": "cchip.zip" + }, + { + "hash": "b7e1189b341bf6a8e270017c096d21b0", + "description": "", + "filename": "decocass.zip" + }, + { + "hash": "547f3d12aed389058ca06148f1cca0ed", + "description": "", + "filename": "f355bios.zip" + }, + { + "hash": "1028615bcac4c31634a3364ce5c04044", + "description": "", + "filename": "f355dlx.zip" + }, + { + "hash": "f4011d3116500354edf7302a90402711", + "description": "", + "filename": "hod2bios.zip" + }, + { + "hash": "4a56d56e2219c5e2b006b66a4263c01c", + "description": "", + "filename": "isgsm.zip" + }, + { + "hash": "5904b0de768d1d506e766aa7e18994c1", + "description": "", + "filename": "midssio.zip" + }, + { + "hash": "526eda1e2a7920c92c88178789d71d84", + "description": "", + "filename": "naomi.zip" + }, + { + "hash": "00dad01abdbf8ea9e79ad2fe11bdb182", + "description": "", + "filename": "neogeo.zip" + }, + { + "hash": "bfacf1a68792d5348f93cf724d2f1dda", + "description": "", + "filename": "nmk004.zip" + }, + { + "hash": "87cc944eef4c671aa2629a8ba48a08e0", + "description": "", + "filename": "pgm.zip" + }, + { + "hash": "3f956c4e7008804cb47cbde49bd5b908", + "description": "", + "filename": "skns.zip" + }, + { + "hash": "79ae0d2bb1901b7e606b6dc339b79a97", + "description": "", + "filename": "ym2608.zip" + } + ] + }, + { + "IGDBId": 473, + "IGDBName": "Arcadia 2001", + "IGDBSlug": "arcadia-2001", + "AlternateNames": [], + "Extensions": { + "SupportedFileExtensions": [], + "UniqueFileExtensions": [] + }, + "RetroPieDirectoryName": "", + "WebEmulator": { + "Type": "", + "Core": "", + "AvailableWebEmulators": null + }, + "Bios": [] + }, + { + "IGDBId": 59, + "IGDBName": "Atari 2600", + "IGDBSlug": "atari2600", + "AlternateNames": [ + "Atari 2600", + "Atari VCS", + "atari2600" + ], + "Extensions": { + "SupportedFileExtensions": [ + ".7Z", + ".A26", + ".BIN", + ".GZ", + ".ROM", + ".ZIP" + ], + "UniqueFileExtensions": [ + ".A26" + ] + }, + "RetroPieDirectoryName": "atari2600", + "WebEmulator": { + "Type": "EmulatorJS", + "Core": "atari2600", + "AvailableWebEmulators": [ + { + "EmulatorType": "EmulatorJS", + "AvailableWebEmulatorCores": [ { - "hash": "0763f1ffb006ddbe32e52d497ee848ae", - "description": "7800 BIOS - Optional", - "filename": "7800 BIOS (U).rom" + "Core": "atari2600", + "AlternateCoreName": "stella2014", + "Default": true } - ] + ] + } + ] }, - { - "igdbId": 62, - "igdbName": "Atari Jaguar", - "igdbSlug": "jaguar", - "alternateNames": [ - "Atari Jaguar", - "Jaguar" - ], - "extensions": { - "supportedFileExtensions": [ - ".J64", - ".JAG" - ], - "uniqueFileExtensions": [ - ".J64", - ".JAG" - ] - }, - "retroPieDirectoryName": "atarijaguar", - "webEmulator": { - "type": "EmulatorJS", - "core": "jaguar", - "availableWebEmulators": [ - { - "emulatorType": "EmulatorJS", - "availableWebEmulatorCores": [ - { - "core": "jaguar", - "alternateCoreName": "virtualjaguar", - "default": true - } - ] - } - ] - }, - "bios": [] + "Bios": [] + }, + { + "IGDBId": 66, + "IGDBName": "Atari 5200", + "IGDBSlug": "atari5200", + "AlternateNames": [ + "Atari 5200", + "Atari 5200 SuperSystem", + "atari5200" + ], + "Extensions": { + "SupportedFileExtensions": [ + ".A52", + ".ATR", + ".ATR.GZ", + ".BAS", + ".BIN", + ".CAR", + ".DCM", + ".XEX", + ".XFD", + ".XFD.GZ" + ], + "UniqueFileExtensions": [ + ".A52", + ".ATR", + ".ATR.GZ", + ".BAS", + ".CAR", + ".DCM", + ".XEX", + ".XFD", + ".XFD.GZ" + ] }, - { - "igdbId": 61, - "igdbName": "Atari Lynx", - "igdbSlug": "lynx", - "alternateNames": [ - "Atari Lynx", - "Lynx" - ], - "extensions": { - "supportedFileExtensions": [ - ".7Z", - ".LNX", - ".ZIP" - ], - "uniqueFileExtensions": [ - ".LNX" - ] - }, - "retroPieDirectoryName": "atarilynx", - "webEmulator": { - "type": "EmulatorJS", - "core": "lynx", - "availableWebEmulators": [ - { - "emulatorType": "EmulatorJS", - "availableWebEmulatorCores": [ - { - "core": "lynx", - "alternateCoreName": "handy", - "default": true - } - ] - } - ] - }, - "bios": [ + "RetroPieDirectoryName": "atari5200", + "WebEmulator": { + "Type": "EmulatorJS", + "Core": "atari5200", + "AvailableWebEmulators": [ + { + "EmulatorType": "EmulatorJS", + "AvailableWebEmulatorCores": [ { - "hash": "fcd403db69f54290b51035d82f835e7b", - "description": "BIOS for the Atari Lynx", - "filename": "lynxboot.img" + "Core": "atari5200", + "AlternateCoreName": "a5200", + "Default": true } - ] + ] + } + ] }, - { - "igdbId": 63, - "igdbName": "Atari ST/STE", - "igdbSlug": "atari-st", - "alternateNames": [ - "Atari ST", - "Atari ST/STE", - "Atari STE", - "atari-st" - ], - "extensions": { - "supportedFileExtensions": [ - ".CTR", - ".IMG", - ".IPF", - ".MSA", - ".RAW", - ".ROM", - ".ST", - ".STX", - ".ZIP" - ], - "uniqueFileExtensions": [ - ".CTR", - ".MSA", - ".RAW", - ".ST", - ".STX" - ] - }, - "retroPieDirectoryName": "atarist", - "webEmulator": { - "type": "", - "core": "" - }, - "bios": [ + "Bios": [ + { + "hash": "281f20ea4320404ec820fb7ec0693b38", + "description": "BIOS for the Atari 5200", + "filename": "5200.rom" + } + ] + }, + { + "IGDBId": 60, + "IGDBName": "Atari 7800", + "IGDBSlug": "atari7800", + "AlternateNames": [ + "Atari 7800", + "Atari 7800 ProSystem", + "Atari VCS", + "atari7800" + ], + "Extensions": { + "SupportedFileExtensions": [ + ".7Z", + ".A78", + ".BIN", + ".ZIP" + ], + "UniqueFileExtensions": [ + ".A78" + ] + }, + "RetroPieDirectoryName": "atari7800", + "WebEmulator": { + "Type": "EmulatorJS", + "Core": "atari7800", + "AvailableWebEmulators": [ + { + "EmulatorType": "EmulatorJS", + "AvailableWebEmulatorCores": [ { - "hash": "C1C57CE48E8EE4135885CEE9E63A68A2", - "description": "TOS", - "filename": "tos.img" + "Core": "atari7800", + "AlternateCoreName": "prosystem", + "Default": true } - ] + ] + } + ] }, - { - "igdbId": 68, - "igdbName": "ColecoVision", - "igdbSlug": "colecovision", - "alternateNames": [ - "ColecoVision" - ], - "extensions": { - "supportedFileExtensions": [ - ".BIN", - ".COL", - ".ROM", - ".ZIP" - ], - "uniqueFileExtensions": [ - ] - }, - "retroPieDirectoryName": "coleco", - "webEmulator": { - "type": "EmulatorJS", - "core": "coleco", - "availableWebEmulators": [ - { - "emulatorType": "EmulatorJS", - "availableWebEmulatorCores": [ - { - "core": "coleco", - "alternateCoreName": "coleco", - "default": true - } - ] - } - ] - }, - "bios": [ + "Bios": [ + { + "hash": "0763f1ffb006ddbe32e52d497ee848ae", + "description": "7800 BIOS - Optional", + "filename": "7800 BIOS (U).rom" + } + ] + }, + { + "IGDBId": 65, + "IGDBName": "Atari 8-bit", + "IGDBSlug": "atari8bit", + "AlternateNames": [], + "Extensions": { + "SupportedFileExtensions": [], + "UniqueFileExtensions": [] + }, + "RetroPieDirectoryName": "", + "WebEmulator": { + "Type": "", + "Core": "", + "AvailableWebEmulators": null + }, + "Bios": [] + }, + { + "IGDBId": 62, + "IGDBName": "Atari Jaguar", + "IGDBSlug": "jaguar", + "AlternateNames": [ + "Atari Jaguar", + "Jaguar" + ], + "Extensions": { + "SupportedFileExtensions": [ + ".J64", + ".JAG" + ], + "UniqueFileExtensions": [ + ".J64", + ".JAG" + ] + }, + "RetroPieDirectoryName": "atarijaguar", + "WebEmulator": { + "Type": "EmulatorJS", + "Core": "jaguar", + "AvailableWebEmulators": [ + { + "EmulatorType": "EmulatorJS", + "AvailableWebEmulatorCores": [ { - "hash": "2c66f5911e5b42b8ebe113403548eee7", - "description": "ColecoVision BIOS - Mandatory", - "filename": "colecovision.rom" + "Core": "jaguar", + "AlternateCoreName": "virtualjaguar", + "Default": true } - ] + ] + } + ] }, - { - "igdbId": 15, - "igdbName": "Commodore C64/128/MAX", - "igdbSlug": "c64", - "alternateNames": [ - "C64", - "C64/C128/MAX", - "Commodore C128", - "Commodore C64", - "Commodore C64/128/MAX", - "Commodore C64DTV" - ], - "extensions": { - "supportedFileExtensions": [ - ".CRT", - ".D64", - ".D80", - ".D81", - ".G64", - ".M3U", - ".PRG", - ".T64", - ".TAP", - ".X64", - ".ZIP" - ], - "uniqueFileExtensions": [ - ".CRT", - ".D64", - ".D80", - ".D81", - ".G64", - ".PRG", - ".T64", - ".X64" - ] - }, - "retroPieDirectoryName": "c64", - "webEmulator": { - "type": "EmulatorJS", - "core": "vice_x64", - "availableWebEmulators": [ - { - "emulatorType": "EmulatorJS", - "availableWebEmulatorCores": [ - { - "core": "c64", - "alternateCoreName": "vice_x64sc", - "default": true - }, - { - "core": "vice_x64" - } - ] - } - ] - }, - "bios": [ + "Bios": [] + }, + { + "IGDBId": 410, + "IGDBName": "Atari Jaguar CD", + "IGDBSlug": "atari-jaguar-cd", + "AlternateNames": [ + "Jag CD" + ], + "Extensions": { + "SupportedFileExtensions": [], + "UniqueFileExtensions": [] + }, + "RetroPieDirectoryName": "", + "WebEmulator": { + "Type": "", + "Core": "", + "AvailableWebEmulators": null + }, + "Bios": [] + }, + { + "IGDBId": 61, + "IGDBName": "Atari Lynx", + "IGDBSlug": "lynx", + "AlternateNames": [ + "Atari Lynx", + "Lynx" + ], + "Extensions": { + "SupportedFileExtensions": [ + ".7Z", + ".LNX", + ".ZIP" + ], + "UniqueFileExtensions": [ + ".LNX" + ] + }, + "RetroPieDirectoryName": "atarilynx", + "WebEmulator": { + "Type": "EmulatorJS", + "Core": "lynx", + "AvailableWebEmulators": [ + { + "EmulatorType": "EmulatorJS", + "AvailableWebEmulatorCores": [ { - "hash": "1b1e985ea5325a1f46eb7fd9681707bf", - "description": "JiffyDOS 1541 drive BIOS", - "filename": "JiffyDOS_1541-II.bin" - }, - { - "hash": "41c6cc528e9515ffd0ed9b180f8467c0", - "description": "JiffyDOS 1571 drive BIOS", - "filename": "JiffyDOS_1571_repl310654.bin" - }, - { - "hash": "20b6885c6dc2d42c38754a365b043d71", - "description": "JiffyDOS 1581 drive BIOS", - "filename": "JiffyDOS_1581.bin" - }, - { - "hash": "be09394f0576cf81fa8bacf634daf9a2", - "description": "JiffyDOS C64 Kernal", - "filename": "JiffyDOS_C64.bin" + "Core": "lynx", + "AlternateCoreName": "handy", + "Default": true } - ] + ] + } + ] }, - { - "igdbId": 90, - "igdbName": "Commodore PET", - "igdbSlug": "cpet", - "alternateNames": [ - "cpet", - "PET", - "Commodore PET" - ], - "extensions": { - "supportedFileExtensions": [ - ".CRT", - ".D64", - ".D80", - ".D81", - ".G64", - ".M3U", - ".PRG", - ".T64", - ".TAP", - ".X64", - ".ZIP" - ], - "uniqueFileExtensions": [ - ".CRT", - ".D64", - ".D80", - ".D81", - ".G64", - ".PRG", - ".T64", - ".X64" - ] - }, - "retroPieDirectoryName": "c64", - "webEmulator": { - "type": "EmulatorJS", - "core": "vice_xpet", - "availableWebEmulators": [ - { - "emulatorType": "EmulatorJS", - "availableWebEmulatorCores": [ - { - "core": "pet", - "alternateCoreName": "vice_xpet", - "default": true - } - ] - } - ] - }, - "bios": [ - ] + "Bios": [ + { + "hash": "fcd403db69f54290b51035d82f835e7b", + "description": "BIOS for the Atari Lynx", + "filename": "lynxboot.img" + } + ] + }, + { + "IGDBId": 63, + "IGDBName": "Atari ST/STE", + "IGDBSlug": "atari-st", + "AlternateNames": [ + "Atari ST", + "Atari ST/STE", + "Atari STE", + "atari-st" + ], + "Extensions": { + "SupportedFileExtensions": [ + ".CTR", + ".IMG", + ".IPF", + ".MSA", + ".RAW", + ".ROM", + ".ST", + ".STX", + ".ZIP" + ], + "UniqueFileExtensions": [ + ".CTR", + ".MSA", + ".RAW", + ".ST", + ".STX" + ] }, - { - "igdbId": 94, - "igdbName": "Commodore Plus/4", - "igdbSlug": "c-plus-4", - "alternateNames": [ - "C+4", - "c-plus-4", - "Plus/4", - "Commodore Plus/4" - ], - "extensions": { - "supportedFileExtensions": [ - ".CRT", - ".D64", - ".D80", - ".D81", - ".G64", - ".M3U", - ".PRG", - ".T64", - ".TAP", - ".X64", - ".ZIP" - ], - "uniqueFileExtensions": [ - ".CRT", - ".D64", - ".D80", - ".D81", - ".G64", - ".PRG", - ".T64", - ".X64" - ] - }, - "retroPieDirectoryName": "c64", - "webEmulator": { - "type": "EmulatorJS", - "core": "vice_xplus4", - "availableWebEmulators": [ - { - "emulatorType": "EmulatorJS", - "availableWebEmulatorCores": [ - { - "core": "plus4", - "alternateCoreName": "vice_xplus4", - "default": true - } - ] - } - ] - }, - "bios": [ - ] + "RetroPieDirectoryName": "atarist", + "WebEmulator": { + "Type": "", + "Core": "", + "AvailableWebEmulators": [] }, - { - "igdbId": 71, - "igdbName": "Commodore VIC-20", - "igdbSlug": "vic-20", - "alternateNames": [ - "VIC20", - "VIC-20", - "Commodore VIC-20" - ], - "extensions": { - "supportedFileExtensions": [ - ".CRT", - ".D64", - ".D80", - ".D81", - ".G64", - ".M3U", - ".PRG", - ".T64", - ".TAP", - ".X64", - ".ZIP" - ], - "uniqueFileExtensions": [ - ".CRT", - ".D64", - ".D80", - ".D81", - ".G64", - ".PRG", - ".T64", - ".X64" - ] - }, - "retroPieDirectoryName": "c64", - "webEmulator": { - "type": "EmulatorJS", - "core": "vice_xvic", - "availableWebEmulators": [ - { - "emulatorType": "EmulatorJS", - "availableWebEmulatorCores": [ - { - "core": "vic20", - "alternateCoreName": "vice_xvic", - "default": true - } - ] - } - ] - }, - "bios": [ - ] + "Bios": [ + { + "hash": "c1c57ce48e8ee4135885cee9e63a68a2", + "description": "TOS", + "filename": "tos.img" + } + ] + }, + { + "IGDBId": 140, + "IGDBName": "AY-3-8500", + "IGDBSlug": "ay-3-8500", + "AlternateNames": [], + "Extensions": { + "SupportedFileExtensions": [], + "UniqueFileExtensions": [] }, - { - "igdbId": 158, - "igdbName": "Commodore CDTV", - "igdbSlug": "commodore-cdtv", - "alternateNames": [ - "Commodore CDTV", - "Amiga CDTV", - "Commodore Amiga CDTV" - ], - "extensions": { - "supportedFileExtensions": [ - ".ZIP" - ], - "uniqueFileExtensions": [ - ] - }, - "retroPieDirectoryName": "amiga", - "webEmulator": { - "type": "EmulatorJS", - "core": "amiga", - "availableWebEmulators": [ - { - "emulatorType": "EmulatorJS", - "availableWebEmulatorCores": [ - { - "core": "amiga", - "alternateCoreName": "puae", - "default": true - } - ] - } - ] - }, - "bios": [ + "RetroPieDirectoryName": "", + "WebEmulator": { + "Type": "", + "Core": "", + "AvailableWebEmulators": null + }, + "Bios": [] + }, + { + "IGDBId": 141, + "IGDBName": "AY-3-8610", + "IGDBSlug": "ay-3-8610", + "AlternateNames": [], + "Extensions": { + "SupportedFileExtensions": [], + "UniqueFileExtensions": [] + }, + "RetroPieDirectoryName": "", + "WebEmulator": { + "Type": "", + "Core": "", + "AvailableWebEmulators": null + }, + "Bios": [] + }, + { + "IGDBId": 91, + "IGDBName": "Bally Astrocade", + "IGDBSlug": "astrocade", + "AlternateNames": [ + "Bally Arcade" + ], + "Extensions": { + "SupportedFileExtensions": [], + "UniqueFileExtensions": [] + }, + "RetroPieDirectoryName": "", + "WebEmulator": { + "Type": "", + "Core": "", + "AvailableWebEmulators": null + }, + "Bios": [] + }, + { + "IGDBId": 69, + "IGDBName": "BBC Microcomputer System", + "IGDBSlug": "bbcmicro", + "AlternateNames": [ + "BBC Micro" + ], + "Extensions": { + "SupportedFileExtensions": [], + "UniqueFileExtensions": [] + }, + "RetroPieDirectoryName": "", + "WebEmulator": { + "Type": "", + "Core": "", + "AvailableWebEmulators": null + }, + "Bios": [] + }, + { + "IGDBId": 73, + "IGDBName": "BlackBerry OS", + "IGDBSlug": "blackberry", + "AlternateNames": [], + "Extensions": { + "SupportedFileExtensions": [], + "UniqueFileExtensions": [] + }, + "RetroPieDirectoryName": "", + "WebEmulator": { + "Type": "", + "Core": "", + "AvailableWebEmulators": null + }, + "Bios": [] + }, + { + "IGDBId": 68, + "IGDBName": "ColecoVision", + "IGDBSlug": "colecovision", + "AlternateNames": [ + "Coleco", + "ColecoVision" + ], + "Extensions": { + "SupportedFileExtensions": [ + ".BIN", + ".COL", + ".ROM", + ".ZIP" + ], + "UniqueFileExtensions": [ + ".COL" + ] + }, + "RetroPieDirectoryName": "coleco", + "WebEmulator": { + "Type": "EmulatorJS", + "Core": "coleco", + "AvailableWebEmulators": [ + { + "EmulatorType": "EmulatorJS", + "AvailableWebEmulatorCores": [ { - "hash": "85ad74194e87c08904327de1a9443b7a", - "description": "Kickstart v1.2 rev 33.180", - "filename": "kick33180.A500" - }, - { - "hash": "82a21c1890cae844b3df741f2762d48d", - "description": "Kickstart v1.3 rev 34.005", - "filename": "kick34005.A500" - }, - { - "hash": "89da1838a24460e4b93f4f0c5d92d48d", - "description": "CDTV extended ROM v1.00", - "filename": "kick34005.CDTV" - }, - { - "hash": "dc10d7bdd1b6f450773dfb558477c230", - "description": "Kickstart v2.04 rev 37.175", - "filename": "kick37175.A500" - }, - { - "hash": "5f8924d013dd57a89cf349f4cdedc6b1", - "description": "CD32 Kickstart v3.1 rev 40.060", - "filename": "kick40060.CD32" - }, - { - "hash": "f2f241bf094168cfb9e7805dc2856433", - "description": "CD32 KS + extended v3.1 rev 40.060", - "filename": "kick40060.CD32" - }, - { - "hash": "bb72565701b1b6faece07d68ea5da639", - "description": "CD32 extended ROM rev 40.060", - "filename": "kick40060.CD32.ext" - }, - { - "hash": "e40a5dfb3d017ba8779faba30cbd1c8e", - "description": "Kickstart v3.1 rev 40.063", - "filename": "kick40063.A600" - }, - { - "hash": "646773759326fbac3b2311fd8c8793ee", - "description": "Kickstart v3.1 rev 40.068", - "filename": "kick40068.A1200" - }, - { - "hash": "9bdedde6a4f33555b4a270c8ca53297d", - "description": "Kickstart v3.1 rev 40.068", - "filename": "kick40068.A4000" + "Core": "coleco", + "AlternateCoreName": "coleco", + "Default": true } - ] + ] + } + ] }, - { - "igdbId": 33, - "igdbName": "Game Boy", - "igdbSlug": "gb", - "alternateNames": [ - "Game Boy", - "GB" - ], - "extensions": { - "supportedFileExtensions": [ - ".7Z", - ".GB", - ".ZIP" - ], - "uniqueFileExtensions": [ - ".GB" - ] - }, - "retroPieDirectoryName": "gb", - "webEmulator": { - "type": "EmulatorJS", - "core": "gb", - "availableWebEmulators": [ - { - "emulatorType": "EmulatorJS", - "availableWebEmulatorCores": [ - { - "core": "gb", - "alternateCoreName": "gambatte", - "default": true - }, - { - "core": "mgba" - } - ] - } - ] - }, - "bios": [ + "Bios": [ + { + "hash": "2c66f5911e5b42b8ebe113403548eee7", + "description": "ColecoVision BIOS - Mandatory", + "filename": "colecovision.rom" + } + ] + }, + { + "IGDBId": 93, + "IGDBName": "Commodore 16", + "IGDBSlug": "c16", + "AlternateNames": [ + "C16" + ], + "Extensions": { + "SupportedFileExtensions": [], + "UniqueFileExtensions": [] + }, + "RetroPieDirectoryName": "", + "WebEmulator": { + "Type": "", + "Core": "", + "AvailableWebEmulators": null + }, + "Bios": [] + }, + { + "IGDBId": 15, + "IGDBName": "Commodore C64/128/MAX", + "IGDBSlug": "c64", + "AlternateNames": [ + "C64", + "C64/C128/MAX", + "Commodore C128", + "Commodore C64", + "Commodore C64/128/MAX", + "Commodore C64DTV" + ], + "Extensions": { + "SupportedFileExtensions": [ + ".CRT", + ".D64", + ".D80", + ".D81", + ".G64", + ".M3U", + ".PRG", + ".T64", + ".TAP", + ".X64", + ".ZIP" + ], + "UniqueFileExtensions": [] + }, + "RetroPieDirectoryName": "c64", + "WebEmulator": { + "Type": "EmulatorJS", + "Core": "vice_x64sc", + "AvailableWebEmulators": [ + { + "EmulatorType": "EmulatorJS", + "AvailableWebEmulatorCores": [ { - "hash": "32fbbd84168d3482956eb3c5051637f5", - "description": "BIOS for Game Boy", - "filename": "gb_bios.bin" + "Core": "c64", + "AlternateCoreName": "vice_x64sc", + "Default": true }, { - "hash": "d574d4f9c12f305074798f54c091a8b4", - "description": "Super Game Boy", - "filename": "sgb_bios.bin" + "Core": "vice_x64", + "AlternateCoreName": "", + "Default": false } - ] + ] + } + ] }, - { - "igdbId": 24, - "igdbName": "Game Boy Advance", - "igdbSlug": "gba", - "alternateNames": [ - "Game Boy Advance", - "GBA" - ], - "extensions": { - "supportedFileExtensions": [ - ".7Z", - ".GBA", - ".ZIP" - ], - "uniqueFileExtensions": [ - ".GBA" - ] - }, - "retroPieDirectoryName": "gba", - "webEmulator": { - "type": "EmulatorJS", - "core": "gba", - "availableWebEmulators": [ - { - "emulatorType": "EmulatorJS", - "availableWebEmulatorCores": [ - { - "core": "gb", - "alternateCoreName": "mgba", - "default": true - } - ] - } - ] - }, - "bios": [ + "Bios": [ + { + "hash": "1b1e985ea5325a1f46eb7fd9681707bf", + "description": "JiffyDOS 1541 drive BIOS", + "filename": "JiffyDOS_1541-II.bin" + }, + { + "hash": "41c6cc528e9515ffd0ed9b180f8467c0", + "description": "JiffyDOS 1571 drive BIOS", + "filename": "JiffyDOS_1571_repl310654.bin" + }, + { + "hash": "20b6885c6dc2d42c38754a365b043d71", + "description": "JiffyDOS 1581 drive BIOS", + "filename": "JiffyDOS_1581.bin" + }, + { + "hash": "be09394f0576cf81fa8bacf634daf9a2", + "description": "JiffyDOS C64 Kernal", + "filename": "JiffyDOS_C64.bin" + } + ] + }, + { + "IGDBId": 158, + "IGDBName": "Commodore CDTV", + "IGDBSlug": "commodore-cdtv", + "AlternateNames": [ + "Amiga CDTV", + "Commodore Amiga CDTV", + "Commodore CDTV", + "Commodore Dynamic Total Vision" + ], + "Extensions": { + "SupportedFileExtensions": [ + ".ZIP" + ], + "UniqueFileExtensions": [] + }, + "RetroPieDirectoryName": "amiga", + "WebEmulator": { + "Type": "EmulatorJS", + "Core": "amiga", + "AvailableWebEmulators": [ + { + "EmulatorType": "EmulatorJS", + "AvailableWebEmulatorCores": [ { - "hash": "32FBBD84168D3482956EB3C5051637F5", - "description": "[BIOS] Nintendo Game Boy Boot ROM (World) (Rev 1).gb", - "filename": "gb_bios.bin" - }, - { - "hash": "a860e8c0b6d573d191e4ec7db1b1e4f6", - "description": "[BIOS] Game Boy Advance (World).gba", - "filename": "gba_bios.bin" - }, - { - "hash": "DBFCE9DB9DEAA2567F6A84FDE55F9680", - "description": "[BIOS] Nintendo Game Boy Color Boot ROM (World).gbc", - "filename": "gbc_bios.bin" - }, - { - "hash": "D574D4F9C12F305074798F54C091A8B4", - "description": "SGB-CPU (World) (Enhancement Chip).bin", - "filename": "sgb_bios.bin" + "Core": "amiga", + "AlternateCoreName": "puae", + "Default": true } - ] + ] + } + ] }, - { - "igdbId": 22, - "igdbName": "Game Boy Color", - "igdbSlug": "gbc", - "alternateNames": [ - "Game Boy Color", - "GBC" - ], - "extensions": { - "supportedFileExtensions": [ - ".7Z", - ".GBC", - ".ZIP" - ], - "uniqueFileExtensions": [ - ".GBC" - ] - }, - "retroPieDirectoryName": "gbc", - "webEmulator": { - "type": "EmulatorJS", - "core": "gb", - "availableWebEmulators": [ - { - "emulatorType": "EmulatorJS", - "availableWebEmulatorCores": [ - { - "core": "gb", - "alternateCoreName": "gambatte", - "default": true - }, - { - "core": "mgba" - } - ] - } - ] - }, - "bios": [ + "Bios": [ + { + "hash": "85ad74194e87c08904327de1a9443b7a", + "description": "Kickstart v1.2 rev 33.180", + "filename": "kick33180.A500" + }, + { + "hash": "82a21c1890cae844b3df741f2762d48d", + "description": "Kickstart v1.3 rev 34.005", + "filename": "kick34005.A500" + }, + { + "hash": "89da1838a24460e4b93f4f0c5d92d48d", + "description": "CDTV extended ROM v1.00", + "filename": "kick34005.CDTV" + }, + { + "hash": "dc10d7bdd1b6f450773dfb558477c230", + "description": "Kickstart v2.04 rev 37.175", + "filename": "kick37175.A500" + }, + { + "hash": "5f8924d013dd57a89cf349f4cdedc6b1", + "description": "CD32 Kickstart v3.1 rev 40.060", + "filename": "kick40060.CD32" + }, + { + "hash": "f2f241bf094168cfb9e7805dc2856433", + "description": "CD32 KS + extended v3.1 rev 40.060", + "filename": "kick40060.CD32" + }, + { + "hash": "bb72565701b1b6faece07d68ea5da639", + "description": "CD32 extended ROM rev 40.060", + "filename": "kick40060.CD32.ext" + }, + { + "hash": "e40a5dfb3d017ba8779faba30cbd1c8e", + "description": "Kickstart v3.1 rev 40.063", + "filename": "kick40063.A600" + }, + { + "hash": "646773759326fbac3b2311fd8c8793ee", + "description": "Kickstart v3.1 rev 40.068", + "filename": "kick40068.A1200" + }, + { + "hash": "9bdedde6a4f33555b4a270c8ca53297d", + "description": "Kickstart v3.1 rev 40.068", + "filename": "kick40068.A4000" + } + ] + }, + { + "IGDBId": 90, + "IGDBName": "Commodore PET", + "IGDBSlug": "cpet", + "AlternateNames": [ + "Commodore PET", + "cpet", + "PET" + ], + "Extensions": { + "SupportedFileExtensions": [ + ".CRT", + ".D64", + ".D80", + ".D81", + ".G64", + ".M3U", + ".PRG", + ".T64", + ".TAP", + ".X64", + ".ZIP" + ], + "UniqueFileExtensions": [] + }, + "RetroPieDirectoryName": "c64", + "WebEmulator": { + "Type": "EmulatorJS", + "Core": "vice_xpet", + "AvailableWebEmulators": [ + { + "EmulatorType": "EmulatorJS", + "AvailableWebEmulatorCores": [ { - "hash": "dbfce9db9deaa2567f6a84fde55f9680", - "description": "BIOS for Game Boy Color", - "filename": "gbc_bios.bin" - }, - { - "hash": "d574d4f9c12f305074798f54c091a8b4", - "description": "Super Game Boy", - "filename": "sgb_bios.bin" + "Core": "pet", + "AlternateCoreName": "vice_xpet", + "Default": true } - ] + ] + } + ] }, - { - "igdbId": 4, - "igdbName": "Nintendo 64", - "igdbSlug": "n64", - "alternateNames": [ - "N64", - "Nintendo 64" - ], - "extensions": { - "supportedFileExtensions": [ - ".N64", - ".V64", - ".Z64", - ".ZIP" - ], - "uniqueFileExtensions": [ - ".N64", - ".V64", - ".Z64" - ] - }, - "retroPieDirectoryName": "n64", - "webEmulator": { - "type": "EmulatorJS", - "core": "n64", - "availableWebEmulators": [ - { - "emulatorType": "EmulatorJS", - "availableWebEmulatorCores": [ - { - "core": "n64", - "alternateCoreName": "mupen64plus_next", - "default": true - } - ] - } - ] - }, - "bios": [] + "Bios": [] + }, + { + "IGDBId": 94, + "IGDBName": "Commodore Plus/4", + "IGDBSlug": "c-plus-4", + "AlternateNames": [ + "C+4", + "c-plus-4", + "Commodore Plus/4", + "Plus/4" + ], + "Extensions": { + "SupportedFileExtensions": [ + ".CRT", + ".D64", + ".D80", + ".D81", + ".G64", + ".M3U", + ".PRG", + ".T64", + ".TAP", + ".X64", + ".ZIP" + ], + "UniqueFileExtensions": [] }, - { - "igdbId": 20, - "igdbName": "Nintendo DS", - "igdbSlug": "nds", - "alternateNames": [ - "NDS" - ], - "extensions": { - "supportedFileExtensions": [ - ".BIN", - ".NDS", - ".ZIP" - ], - "uniqueFileExtensions": [ - ".NDS" - ] - }, - "retroPieDirectoryName": "nds", - "webEmulator": { - "type": "EmulatorJS", - "core": "nds", - "availableWebEmulators": [ - { - "emulatorType": "EmulatorJS", - "availableWebEmulatorCores": [ - { - "core": "nds", - "alternateCoreName": "melonds", - "default": true - }, - { - "core": "desmume" - }, - { - "core": "desmume2015" - } - ] - } - ] - }, - "bios": [ + "RetroPieDirectoryName": "c64", + "WebEmulator": { + "Type": "EmulatorJS", + "Core": "vice_xplus4", + "AvailableWebEmulators": [ + { + "EmulatorType": "EmulatorJS", + "AvailableWebEmulatorCores": [ { - "hash": "df692a80a5b1bc90728bc3dfc76cd948", - "description": "ARM7 BIOS", - "filename": "bios7.bin" - }, - { - "hash": "a392174eb3e572fed6447e956bde4b25", - "description": "ARM9 BIOS", - "filename": "bios9.bin" - }, - { - "hash": "145eaef5bd3037cbc247c213bb3da1b3", - "description": "NDS Firmware", - "filename": "firmware.bin" + "Core": "plus4", + "AlternateCoreName": "vice_xplus4", + "Default": true } - ] + ] + } + ] }, - { - "igdbId": 18, - "igdbName": "Nintendo Entertainment System", - "igdbSlug": "nes", - "alternateNames": [ - "NES", - "Nintendo Entertainment System", - "Nintendo Famicom & Entertainment System" - ], - "extensions": { - "supportedFileExtensions": [ - ".7Z", - ".FDS", - ".NES", - ".NEZ", - ".UNF", - ".UNIF", - ".ZIP" - ], - "uniqueFileExtensions": [ - ".FDS", - ".NES", - ".NEZ", - ".UNF", - ".UNIF" - ] - }, - "retroPieDirectoryName": "nes", - "webEmulator": { - "type": "EmulatorJS", - "core": "nes", - "availableWebEmulators": [ - { - "emulatorType": "EmulatorJS", - "availableWebEmulatorCores": [ - { - "core": "nes", - "alternateCoreName": "fceumm", - "default": true - }, - { - "core": "nestopia" - } - ] - } - ] - }, - "bios": [ + "Bios": [] + }, + { + "IGDBId": 71, + "IGDBName": "Commodore VIC-20", + "IGDBSlug": "vic-20", + "AlternateNames": [ + "Commodore VIC-20", + "VIC-20", + "VIC20" + ], + "Extensions": { + "SupportedFileExtensions": [ + ".CRT", + ".D64", + ".D80", + ".D81", + ".G64", + ".M3U", + ".PRG", + ".T64", + ".TAP", + ".X64", + ".ZIP" + ], + "UniqueFileExtensions": [] + }, + "RetroPieDirectoryName": "c64", + "WebEmulator": { + "Type": "EmulatorJS", + "Core": "vice_xvic", + "AvailableWebEmulators": [ + { + "EmulatorType": "EmulatorJS", + "AvailableWebEmulatorCores": [ { - "hash": "ca30b50f880eb660a320674ed365ef7a", - "description": "Family Computer Disk System BIOS - Required for Famicom Disk System emulation", - "filename": "disksys.rom" + "Core": "vic20", + "AlternateCoreName": "vice_xvic", + "Default": true + } + ] + } + ] + }, + "Bios": [] + }, + { + "IGDBId": 13, + "IGDBName": "DOS", + "IGDBSlug": "dos", + "AlternateNames": [ + "PC DOS" + ], + "Extensions": { + "SupportedFileExtensions": [], + "UniqueFileExtensions": [] + }, + "RetroPieDirectoryName": "", + "WebEmulator": { + "Type": "", + "Core": "", + "AvailableWebEmulators": null + }, + "Bios": [] + }, + { + "IGDBId": 153, + "IGDBName": "Dragon 32/64", + "IGDBSlug": "dragon-32-slash-64", + "AlternateNames": [], + "Extensions": { + "SupportedFileExtensions": [], + "UniqueFileExtensions": [] + }, + "RetroPieDirectoryName": "", + "WebEmulator": { + "Type": "", + "Core": "", + "AvailableWebEmulators": null + }, + "Bios": [] + }, + { + "IGDBId": 23, + "IGDBName": "Dreamcast", + "IGDBSlug": "dc", + "AlternateNames": [ + "DC" + ], + "Extensions": { + "SupportedFileExtensions": [], + "UniqueFileExtensions": [] + }, + "RetroPieDirectoryName": "", + "WebEmulator": { + "Type": "", + "Core": "", + "AvailableWebEmulators": null + }, + "Bios": [] + }, + { + "IGDBId": 203, + "IGDBName": "DUPLICATE Stadia", + "IGDBSlug": "duplicate-stadia", + "AlternateNames": [], + "Extensions": { + "SupportedFileExtensions": [], + "UniqueFileExtensions": [] + }, + "RetroPieDirectoryName": "", + "WebEmulator": { + "Type": "", + "Core": "", + "AvailableWebEmulators": null + }, + "Bios": [] + }, + { + "IGDBId": 375, + "IGDBName": "Epoch Cassette Vision", + "IGDBSlug": "epoch-cassette-vision", + "AlternateNames": [], + "Extensions": { + "SupportedFileExtensions": [], + "UniqueFileExtensions": [] + }, + "RetroPieDirectoryName": "", + "WebEmulator": { + "Type": "", + "Core": "", + "AvailableWebEmulators": null + }, + "Bios": [] + }, + { + "IGDBId": 376, + "IGDBName": "Epoch Super Cassette Vision", + "IGDBSlug": "epoch-super-cassette-vision", + "AlternateNames": [ + "YENO Super Cassette Vision" + ], + "Extensions": { + "SupportedFileExtensions": [], + "UniqueFileExtensions": [] + }, + "RetroPieDirectoryName": "", + "WebEmulator": { + "Type": "", + "Core": "", + "AvailableWebEmulators": null + }, + "Bios": [] + }, + { + "IGDBId": 309, + "IGDBName": "Evercade", + "IGDBSlug": "evercade", + "AlternateNames": [], + "Extensions": { + "SupportedFileExtensions": [], + "UniqueFileExtensions": [] + }, + "RetroPieDirectoryName": "", + "WebEmulator": { + "Type": "", + "Core": "", + "AvailableWebEmulators": null + }, + "Bios": [] + }, + { + "IGDBId": 99, + "IGDBName": "Family Computer", + "IGDBSlug": "famicom", + "AlternateNames": [ + "Famicom" + ], + "Extensions": { + "SupportedFileExtensions": [ + ".7Z", + ".FDS", + ".NES", + ".NEZ", + ".UNF", + ".UNIF", + ".ZIP" + ], + "UniqueFileExtensions": [] + }, + "RetroPieDirectoryName": "nes", + "WebEmulator": { + "Type": "EmulatorJS", + "Core": "nes", + "AvailableWebEmulators": [ + { + "EmulatorType": "EmulatorJS", + "AvailableWebEmulatorCores": [ + { + "Core": "nes", + "AlternateCoreName": "fceumm", + "Default": true }, { - "hash": "7f98d77d7a094ad7d069b74bd553ec98", - "description": "Game Genie add-on cartridge - Required for Game Genei Add-on emulation (Only supported on the fceumm core)", - "filename": "gamegenie.nes" + "Core": "nestopia", + "AlternateCoreName": "", + "Default": false } - ] + ] + } + ] }, - { - "igdbId": 21, - "igdbName": "Nintendo GameCube", - "igdbSlug": "ngc", - "alternateNames": [ - "GameCube", - "GC", - "GCN", - "ngc", - "Nintendo GameCube" - ], - "extensions": { - "supportedFileExtensions": [ - ".CISO", - ".GC", - ".GCM", - ".GCZ", - ".ISO", - ".RVZ" - ], - "uniqueFileExtensions": [ - ".CISO", - ".GC", - ".GCM", - ".GCZ", - ".RVZ" - ] - }, - "retroPieDirectoryName": "gc", - "webEmulator": { - "type": "", - "core": "" - }, - "bios": [] + "Bios": [ + { + "hash": "ca30b50f880eb660a320674ed365ef7a", + "description": "Family Computer Disk System BIOS - Required for Famicom Disk System emulation", + "filename": "disksys.rom" + }, + { + "hash": "7f98d77d7a094ad7d069b74bd553ec98", + "description": "Game Genie add-on cartridge - Required for Game Genei Add-on emulation (Only supported on the fceumm core)", + "filename": "gamegenie.nes" + } + ] + }, + { + "IGDBId": 51, + "IGDBName": "Family Computer Disk System", + "IGDBSlug": "fds", + "AlternateNames": [ + "Famicom Disk System, FDS" + ], + "Extensions": { + "SupportedFileExtensions": [ + ".7Z", + ".FDS", + ".NES", + ".NEZ", + ".UNF", + ".UNIF", + ".ZIP" + ], + "UniqueFileExtensions": [] }, - { - "igdbId": 7, - "igdbName": "PlayStation", - "igdbSlug": "ps", - "alternateNames": [ - "PlayStation", - "PS", - "PS1", - "PSOne", - "PSX", - "PSX, PSOne, PS", - "Sony PlayStation" - ], - "extensions": { - "supportedFileExtensions": [ - ".CCD", - ".CHD", - ".CUE", - ".EXE", - ".ISO", - ".M3U", - ".PBP", - ".TOC" - ], - "uniqueFileExtensions": [ - ".CCD", - ".EXE", - ".PBP", - ".TOC" - ] - }, - "retroPieDirectoryName": "psx", - "webEmulator": { - "type": "EmulatorJS", - "core": "psx", - "availableWebEmulators": [ - { - "emulatorType": "EmulatorJS", - "availableWebEmulatorCores": [ - { - "core": "psx", - "alternateCoreName": "pcsx_rearmed", - "default": true - }, - { - "core": "mednafen_psx" - } - ] - } - ] - }, - "bios": [ + "RetroPieDirectoryName": "fds", + "WebEmulator": { + "Type": "EmulatorJS", + "Core": "nes", + "AvailableWebEmulators": [ + { + "EmulatorType": "EmulatorJS", + "AvailableWebEmulatorCores": [ { - "hash": "8dd7d5296a650fac7319bce665a6a53c", - "description": "PS1 JP BIOS - Required for JP games", - "filename": "scph5500.bin" + "Core": "nes", + "AlternateCoreName": "fceumm", + "Default": true }, { - "hash": "490f666e1afb15b7362b406ed1cea246", - "description": "PS1 US BIOS - Required for US games", - "filename": "scph5501.bin" + "Core": "nestopia", + "AlternateCoreName": "", + "Default": false + } + ] + } + ] + }, + "Bios": [ + { + "hash": "ca30b50f880eb660a320674ed365ef7a", + "description": "Family Computer Disk System BIOS - Required for Famicom Disk System emulation", + "filename": "disksys.rom" + }, + { + "hash": "7f98d77d7a094ad7d069b74bd553ec98", + "description": "Game Genie add-on cartridge - Required for Game Genei Add-on emulation (Only supported on the fceumm core)", + "filename": "gamegenie.nes" + } + ] + }, + { + "IGDBId": 118, + "IGDBName": "FM Towns", + "IGDBSlug": "fm-towns", + "AlternateNames": [], + "Extensions": { + "SupportedFileExtensions": [], + "UniqueFileExtensions": [] + }, + "RetroPieDirectoryName": "", + "WebEmulator": { + "Type": "", + "Core": "", + "AvailableWebEmulators": null + }, + "Bios": [] + }, + { + "IGDBId": 152, + "IGDBName": "FM-7", + "IGDBSlug": "fm-7", + "AlternateNames": [ + "Fujitsu Micro 7" + ], + "Extensions": { + "SupportedFileExtensions": [], + "UniqueFileExtensions": [] + }, + "RetroPieDirectoryName": "", + "WebEmulator": { + "Type": "", + "Core": "", + "AvailableWebEmulators": null + }, + "Bios": [] + }, + { + "IGDBId": 378, + "IGDBName": "Gamate", + "IGDBSlug": "gamate", + "AlternateNames": [ + "Super Boy" + ], + "Extensions": { + "SupportedFileExtensions": [], + "UniqueFileExtensions": [] + }, + "RetroPieDirectoryName": "", + "WebEmulator": { + "Type": "", + "Core": "", + "AvailableWebEmulators": null + }, + "Bios": [] + }, + { + "IGDBId": 307, + "IGDBName": "Game & Watch", + "IGDBSlug": "g-and-w", + "AlternateNames": [ + "Tricotronic, GW, G&W" + ], + "Extensions": { + "SupportedFileExtensions": [], + "UniqueFileExtensions": [] + }, + "RetroPieDirectoryName": "", + "WebEmulator": { + "Type": "", + "Core": "", + "AvailableWebEmulators": null + }, + "Bios": [] + }, + { + "IGDBId": 33, + "IGDBName": "Game Boy", + "IGDBSlug": "gb", + "AlternateNames": [ + "Game Boy", + "GB" + ], + "Extensions": { + "SupportedFileExtensions": [ + ".7Z", + ".GB", + ".ZIP" + ], + "UniqueFileExtensions": [ + ".GB" + ] + }, + "RetroPieDirectoryName": "gb", + "WebEmulator": { + "Type": "EmulatorJS", + "Core": "gb", + "AvailableWebEmulators": [ + { + "EmulatorType": "EmulatorJS", + "AvailableWebEmulatorCores": [ + { + "Core": "gb", + "AlternateCoreName": "gambatte", + "Default": true }, { - "hash": "32736f17079d0b2b7024407c39bd3050", - "description": "PS1 EU BIOS - Required for EU games", - "filename": "scph5502.bin" + "Core": "mgba", + "AlternateCoreName": "", + "Default": false } - ] + ] + } + ] }, - { - "igdbId": 30, - "igdbName": "Sega 32X", - "igdbSlug": "sega32", - "alternateNames": [ - "Sega 32X", - "Sega32", - "Sega32X" - ], - "extensions": { - "supportedFileExtensions": [ - ".32X", - ".7Z", - ".BIN", - ".MD", - ".ZIP" - ], - "uniqueFileExtensions": [ - ".32X" - ] - }, - "retroPieDirectoryName": "sega32x", - "webEmulator": { - "type": "EmulatorJS", - "core": "sega32x", - "availableWebEmulators": [ - { - "emulatorType": "EmulatorJS", - "availableWebEmulatorCores": [ - { - "core": "sega32x", - "alternateCoreName": "picodrive", - "default": true - } - ] - } - ] - }, - "bios": [] + "Bios": [ + { + "hash": "32fbbd84168d3482956eb3c5051637f5", + "description": "BIOS for Game Boy", + "filename": "gb_bios.bin" + }, + { + "hash": "d574d4f9c12f305074798f54c091a8b4", + "description": "Super Game Boy", + "filename": "sgb_bios.bin" + } + ] + }, + { + "IGDBId": 24, + "IGDBName": "Game Boy Advance", + "IGDBSlug": "gba", + "AlternateNames": [ + "Game Boy Advance", + "GBA" + ], + "Extensions": { + "SupportedFileExtensions": [ + ".7Z", + ".GBA", + ".ZIP" + ], + "UniqueFileExtensions": [ + ".GBA" + ] }, - { - "igdbId": 78, - "igdbName": "Sega CD", - "igdbSlug": "segacd", - "alternateNames": [ - "Mega CD", - "Sega CD", - "Sega Mega-CD & Sega CD", - "segacd" - ], - "extensions": { - "supportedFileExtensions": [ - ".BIN", - ".CHD", - ".CUE", - ".ISO", - ".M3U" - ], - "uniqueFileExtensions": [] - }, - "retroPieDirectoryName": "segacd", - "webEmulator": { - "type": "EmulatorJS", - "core": "segaCD", - "availableWebEmulators": [ - { - "emulatorType": "EmulatorJS", - "availableWebEmulatorCores": [ - { - "core": "segaCD", - "alternateCoreName": "genesis_plus_gx", - "default": true - }, - { - "core": "picodrive" - } - ] - } - ] - }, - "bios": [ + "RetroPieDirectoryName": "gba", + "WebEmulator": { + "Type": "EmulatorJS", + "Core": "gba", + "AvailableWebEmulators": [ + { + "EmulatorType": "EmulatorJS", + "AvailableWebEmulatorCores": [ { - "hash": "e66fa1dc5820d254611fdcdba0662372", - "description": "MegaCD EU BIOS - Required", - "filename": "bios_CD_E.bin" + "Core": "gb", + "AlternateCoreName": "mgba", + "Default": true + } + ] + } + ] + }, + "Bios": [ + { + "hash": "a860e8c0b6d573d191e4ec7db1b1e4f6", + "description": "[BIOS] Game Boy Advance (World).gba", + "filename": "gba_bios.bin" + }, + { + "hash": "dbfce9db9deaa2567f6a84fde55f9680", + "description": "[BIOS] Nintendo Game Boy Color Boot ROM (World).gbc", + "filename": "gbc_bios.bin" + }, + { + "hash": "32fbbd84168d3482956eb3c5051637f5", + "description": "[BIOS] Nintendo Game Boy Boot ROM (World) (Rev 1).gb", + "filename": "gb_bios.bin" + }, + { + "hash": "d574d4f9c12f305074798f54c091a8b4", + "description": "SGB-CPU (World) (Enhancement Chip).bin", + "filename": "sgb_bios.bin" + } + ] + }, + { + "IGDBId": 22, + "IGDBName": "Game Boy Color", + "IGDBSlug": "gbc", + "AlternateNames": [ + "Game Boy Color", + "GBC" + ], + "Extensions": { + "SupportedFileExtensions": [ + ".7Z", + ".GBC", + ".ZIP" + ], + "UniqueFileExtensions": [ + ".GBC" + ] + }, + "RetroPieDirectoryName": "gbc", + "WebEmulator": { + "Type": "EmulatorJS", + "Core": "gb", + "AvailableWebEmulators": [ + { + "EmulatorType": "EmulatorJS", + "AvailableWebEmulatorCores": [ + { + "Core": "gb", + "AlternateCoreName": "gambatte", + "Default": true }, { - "hash": "278a9397d192149e84e820ac621a8edd", - "description": "MegaCD JP BIOS - Required", - "filename": "bios_CD_J.bin" + "Core": "mgba", + "AlternateCoreName": "", + "Default": false + } + ] + } + ] + }, + "Bios": [ + { + "hash": "dbfce9db9deaa2567f6a84fde55f9680", + "description": "BIOS for Game Boy Color", + "filename": "gbc_bios.bin" + }, + { + "hash": "d574d4f9c12f305074798f54c091a8b4", + "description": "Super Game Boy", + "filename": "sgb_bios.bin" + } + ] + }, + { + "IGDBId": 379, + "IGDBName": "Game.com", + "IGDBSlug": "game-dot-com", + "AlternateNames": [ + "Tiger Game.com" + ], + "Extensions": { + "SupportedFileExtensions": [], + "UniqueFileExtensions": [] + }, + "RetroPieDirectoryName": "", + "WebEmulator": { + "Type": "", + "Core": "", + "AvailableWebEmulators": null + }, + "Bios": [] + }, + { + "IGDBId": 170, + "IGDBName": "Google Stadia", + "IGDBSlug": "stadia", + "AlternateNames": [ + "Stadia" + ], + "Extensions": { + "SupportedFileExtensions": [], + "UniqueFileExtensions": [] + }, + "RetroPieDirectoryName": "", + "WebEmulator": { + "Type": "", + "Core": "", + "AvailableWebEmulators": null + }, + "Bios": [] + }, + { + "IGDBId": 411, + "IGDBName": "Handheld Electronic LCD", + "IGDBSlug": "handheld-electronic-lcd", + "AlternateNames": [ + "Handheld LCD Game" + ], + "Extensions": { + "SupportedFileExtensions": [], + "UniqueFileExtensions": [] + }, + "RetroPieDirectoryName": "", + "WebEmulator": { + "Type": "", + "Core": "", + "AvailableWebEmulators": null + }, + "Bios": [] + }, + { + "IGDBId": 67, + "IGDBName": "Intellivision", + "IGDBSlug": "intellivision", + "AlternateNames": [], + "Extensions": { + "SupportedFileExtensions": [], + "UniqueFileExtensions": [] + }, + "RetroPieDirectoryName": "", + "WebEmulator": { + "Type": "", + "Core": "", + "AvailableWebEmulators": null + }, + "Bios": [] + }, + { + "IGDBId": 382, + "IGDBName": "Intellivision Amico", + "IGDBSlug": "intellivision-amico", + "AlternateNames": [], + "Extensions": { + "SupportedFileExtensions": [], + "UniqueFileExtensions": [] + }, + "RetroPieDirectoryName": "", + "WebEmulator": { + "Type": "", + "Core": "", + "AvailableWebEmulators": null + }, + "Bios": [] + }, + { + "IGDBId": 39, + "IGDBName": "iOS", + "IGDBSlug": "ios", + "AlternateNames": [], + "Extensions": { + "SupportedFileExtensions": [], + "UniqueFileExtensions": [] + }, + "RetroPieDirectoryName": "", + "WebEmulator": { + "Type": "", + "Core": "", + "AvailableWebEmulators": null + }, + "Bios": [] + }, + { + "IGDBId": 413, + "IGDBName": "Leapster Explorer/LeadPad Explorer", + "IGDBSlug": "leapster-explorer-slash-leadpad-explorer", + "AlternateNames": [], + "Extensions": { + "SupportedFileExtensions": [], + "UniqueFileExtensions": [] + }, + "RetroPieDirectoryName": "", + "WebEmulator": { + "Type": "", + "Core": "", + "AvailableWebEmulators": null + }, + "Bios": [] + }, + { + "IGDBId": 409, + "IGDBName": "Legacy Computer", + "IGDBSlug": "legacy-computer", + "AlternateNames": [], + "Extensions": { + "SupportedFileExtensions": [], + "UniqueFileExtensions": [] + }, + "RetroPieDirectoryName": "", + "WebEmulator": { + "Type": "", + "Core": "", + "AvailableWebEmulators": null + }, + "Bios": [] + }, + { + "IGDBId": 55, + "IGDBName": "Legacy Mobile Device", + "IGDBSlug": "mobile", + "AlternateNames": [ + "Legacy Cellphone" + ], + "Extensions": { + "SupportedFileExtensions": [], + "UniqueFileExtensions": [] + }, + "RetroPieDirectoryName": "", + "WebEmulator": { + "Type": "", + "Core": "", + "AvailableWebEmulators": null + }, + "Bios": [] + }, + { + "IGDBId": 3, + "IGDBName": "Linux", + "IGDBSlug": "linux", + "AlternateNames": [ + "GNU/Linux" + ], + "Extensions": { + "SupportedFileExtensions": [], + "UniqueFileExtensions": [] + }, + "RetroPieDirectoryName": "", + "WebEmulator": { + "Type": "", + "Core": "", + "AvailableWebEmulators": null + }, + "Bios": [] + }, + { + "IGDBId": 14, + "IGDBName": "Mac", + "IGDBSlug": "mac", + "AlternateNames": [ + "Mac OS" + ], + "Extensions": { + "SupportedFileExtensions": [], + "UniqueFileExtensions": [] + }, + "RetroPieDirectoryName": "", + "WebEmulator": { + "Type": "", + "Core": "", + "AvailableWebEmulators": null + }, + "Bios": [] + }, + { + "IGDBId": 386, + "IGDBName": "Meta Quest 2", + "IGDBSlug": "meta-quest-2", + "AlternateNames": [ + "Quest 2" + ], + "Extensions": { + "SupportedFileExtensions": [], + "UniqueFileExtensions": [] + }, + "RetroPieDirectoryName": "", + "WebEmulator": { + "Type": "", + "Core": "", + "AvailableWebEmulators": null + }, + "Bios": [] + }, + { + "IGDBId": 112, + "IGDBName": "Microcomputer", + "IGDBSlug": "microcomputer--1", + "AlternateNames": [], + "Extensions": { + "SupportedFileExtensions": [], + "UniqueFileExtensions": [] + }, + "RetroPieDirectoryName": "", + "WebEmulator": { + "Type": "", + "Core": "", + "AvailableWebEmulators": null + }, + "Bios": [] + }, + { + "IGDBId": 27, + "IGDBName": "MSX", + "IGDBSlug": "msx", + "AlternateNames": [], + "Extensions": { + "SupportedFileExtensions": [], + "UniqueFileExtensions": [] + }, + "RetroPieDirectoryName": "", + "WebEmulator": { + "Type": "", + "Core": "", + "AvailableWebEmulators": null + }, + "Bios": [] + }, + { + "IGDBId": 53, + "IGDBName": "MSX2", + "IGDBSlug": "msx2", + "AlternateNames": [], + "Extensions": { + "SupportedFileExtensions": [], + "UniqueFileExtensions": [] + }, + "RetroPieDirectoryName": "", + "WebEmulator": { + "Type": "", + "Core": "", + "AvailableWebEmulators": null + }, + "Bios": [] + }, + { + "IGDBId": 157, + "IGDBName": "NEC PC-6000 Series", + "IGDBSlug": "nec-pc-6000-series", + "AlternateNames": [], + "Extensions": { + "SupportedFileExtensions": [], + "UniqueFileExtensions": [] + }, + "RetroPieDirectoryName": "", + "WebEmulator": { + "Type": "", + "Core": "", + "AvailableWebEmulators": null + }, + "Bios": [] + }, + { + "IGDBId": 80, + "IGDBName": "Neo Geo AES", + "IGDBSlug": "neogeoaes", + "AlternateNames": [ + "AES" + ], + "Extensions": { + "SupportedFileExtensions": [], + "UniqueFileExtensions": [] + }, + "RetroPieDirectoryName": "", + "WebEmulator": { + "Type": "", + "Core": "", + "AvailableWebEmulators": null + }, + "Bios": [] + }, + { + "IGDBId": 136, + "IGDBName": "Neo Geo CD", + "IGDBSlug": "neo-geo-cd", + "AlternateNames": [ + "NGCD" + ], + "Extensions": { + "SupportedFileExtensions": [], + "UniqueFileExtensions": [] + }, + "RetroPieDirectoryName": "", + "WebEmulator": { + "Type": "", + "Core": "", + "AvailableWebEmulators": null + }, + "Bios": [] + }, + { + "IGDBId": 79, + "IGDBName": "Neo Geo MVS", + "IGDBSlug": "neogeomvs", + "AlternateNames": [ + "Neo Geo Multi Video System" + ], + "Extensions": { + "SupportedFileExtensions": [], + "UniqueFileExtensions": [] + }, + "RetroPieDirectoryName": "", + "WebEmulator": { + "Type": "", + "Core": "", + "AvailableWebEmulators": null + }, + "Bios": [] + }, + { + "IGDBId": 120, + "IGDBName": "Neo Geo Pocket Color", + "IGDBSlug": "neo-geo-pocket-color", + "AlternateNames": [ + "NGPC" + ], + "Extensions": { + "SupportedFileExtensions": [], + "UniqueFileExtensions": [] + }, + "RetroPieDirectoryName": "", + "WebEmulator": { + "Type": "EmulatorJS", + "Core": "mednafen_ngp", + "AvailableWebEmulators": [ + { + "EmulatorType": "EmulatorJS", + "AvailableWebEmulatorCores": [ + { + "Core": "mednafen_ngp", + "AlternateCoreName": "", + "Default": true + } + ] + } + ] + }, + "Bios": [] + }, + { + "IGDBId": 137, + "IGDBName": "New Nintendo 3DS", + "IGDBSlug": "new-nintendo-3ds", + "AlternateNames": [ + "n3DS" + ], + "Extensions": { + "SupportedFileExtensions": [], + "UniqueFileExtensions": [] + }, + "RetroPieDirectoryName": "", + "WebEmulator": { + "Type": "", + "Core": "", + "AvailableWebEmulators": null + }, + "Bios": [] + }, + { + "IGDBId": 37, + "IGDBName": "Nintendo 3DS", + "IGDBSlug": "3ds", + "AlternateNames": [ + "3DS" + ], + "Extensions": { + "SupportedFileExtensions": [], + "UniqueFileExtensions": [] + }, + "RetroPieDirectoryName": "", + "WebEmulator": { + "Type": "", + "Core": "", + "AvailableWebEmulators": null + }, + "Bios": [] + }, + { + "IGDBId": 4, + "IGDBName": "Nintendo 64", + "IGDBSlug": "n64", + "AlternateNames": [ + "N64", + "Nintendo 64" + ], + "Extensions": { + "SupportedFileExtensions": [ + ".N64", + ".V64", + ".Z64", + ".ZIP" + ], + "UniqueFileExtensions": [ + ".N64", + ".V64", + ".Z64" + ] + }, + "RetroPieDirectoryName": "n64", + "WebEmulator": { + "Type": "EmulatorJS", + "Core": "n64", + "AvailableWebEmulators": [ + { + "EmulatorType": "EmulatorJS", + "AvailableWebEmulatorCores": [ + { + "Core": "n64", + "AlternateCoreName": "mupen64plus_next", + "Default": true + } + ] + } + ] + }, + "Bios": [] + }, + { + "IGDBId": 20, + "IGDBName": "Nintendo DS", + "IGDBSlug": "nds", + "AlternateNames": [ + "NDS" + ], + "Extensions": { + "SupportedFileExtensions": [ + ".BIN", + ".NDS", + ".ZIP" + ], + "UniqueFileExtensions": [ + ".NDS" + ] + }, + "RetroPieDirectoryName": "nds", + "WebEmulator": { + "Type": "EmulatorJS", + "Core": "nds", + "AvailableWebEmulators": [ + { + "EmulatorType": "EmulatorJS", + "AvailableWebEmulatorCores": [ + { + "Core": "nds", + "AlternateCoreName": "melonds", + "Default": true }, { - "hash": "2efd74e3232ff260e371b99f84024f7f", - "description": "SegaCD US BIOS - Required", - "filename": "bios_CD_U.bin" - } - ] - }, - { - "igdbId": 35, - "igdbName": "Sega Game Gear", - "igdbSlug": "gamegear", - "alternateNames": [ - "Game Gear", - "gamegear", - "GG", - "Sega Game Gear" - ], - "extensions": { - "supportedFileExtensions": [ - ".7Z", - ".BIN", - ".GG", - ".ZIP" - ], - "uniqueFileExtensions": [ - ".GG" - ] - }, - "retroPieDirectoryName": "gamegear", - "webEmulator": { - "type": "EmulatorJS", - "core": "segaGG", - "availableWebEmulators": [ - { - "emulatorType": "EmulatorJS", - "availableWebEmulatorCores": [ - { - "core": "segaGG", - "alternateCoreName": "genesis_plus_gx", - "default": true - } - ] - } - ] - }, - "bios": [ - { - "hash": "672e104c3be3a238301aceffc3b23fd6", - "description": "GameGear BIOS (bootrom) - Optional", - "filename": "bios.gg" - } - ] - }, - { - "igdbId": 64, - "igdbName": "Sega Master System/Mark III", - "igdbSlug": "sms", - "alternateNames": [ - "Sega Mark III & Master System", - "Sega Master System", - "Sega Master System/Mark III", - "sms", - "SMS, Mark III" - ], - "extensions": { - "supportedFileExtensions": [ - ".7Z", - ".BIN", - ".SMS", - ".ZIP" - ], - "uniqueFileExtensions": [ - ".SMS" - ] - }, - "retroPieDirectoryName": "mastersystem", - "webEmulator": { - "type": "EmulatorJS", - "core": "smsplus", - "availableWebEmulators": [ - { - "emulatorType": "EmulatorJS", - "availableWebEmulatorCores": [ - { - "core": "segaMS", - "alternateCoreName": "smsplus", - "default": true - }, - { - "core": "picodrive" - }, - { - "core": "genesis_plus_gx" - } - ] - } - ] - }, - "bios": [ - { - "hash": "840481177270d5642a14ca71ee72844c", - "description": "MasterSystem EU BIOS", - "filename": "bios_E.sms" + "Core": "desmume", + "AlternateCoreName": "", + "Default": false }, { - "hash": "24a519c53f67b00640d0048ef7089105", - "description": "MasterSystem JP BIOS", - "filename": "bios_J.sms" + "Core": "desmume2015", + "AlternateCoreName": "", + "Default": false + } + ] + } + ] + }, + "Bios": [ + { + "hash": "df692a80a5b1bc90728bc3dfc76cd948", + "description": "ARM7 BIOS", + "filename": "bios7.bin" + }, + { + "hash": "a392174eb3e572fed6447e956bde4b25", + "description": "ARM9 BIOS", + "filename": "bios9.bin" + }, + { + "hash": "145eaef5bd3037cbc247c213bb3da1b3", + "description": "NDS Firmware", + "filename": "firmware.bin" + } + ] + }, + { + "IGDBId": 159, + "IGDBName": "Nintendo DSi", + "IGDBSlug": "nintendo-dsi", + "AlternateNames": [], + "Extensions": { + "SupportedFileExtensions": [], + "UniqueFileExtensions": [] + }, + "RetroPieDirectoryName": "", + "WebEmulator": { + "Type": "", + "Core": "", + "AvailableWebEmulators": null + }, + "Bios": [] + }, + { + "IGDBId": 18, + "IGDBName": "Nintendo Entertainment System", + "IGDBSlug": "nes", + "AlternateNames": [ + "NES", + "Nintendo Entertainment System", + "Nintendo Famicom & Entertainment System" + ], + "Extensions": { + "SupportedFileExtensions": [ + ".7Z", + ".FDS", + ".NES", + ".NEZ", + ".UNF", + ".UNIF", + ".ZIP" + ], + "UniqueFileExtensions": [] + }, + "RetroPieDirectoryName": "nes", + "WebEmulator": { + "Type": "EmulatorJS", + "Core": "nes", + "AvailableWebEmulators": [ + { + "EmulatorType": "EmulatorJS", + "AvailableWebEmulatorCores": [ + { + "Core": "nes", + "AlternateCoreName": "fceumm", + "Default": true }, { - "hash": "840481177270d5642a14ca71ee72844c", - "description": "MasterSystem US BIOS", - "filename": "bios_U.sms" + "Core": "nestopia", + "AlternateCoreName": "", + "Default": false } - ] + ] + } + ] }, - { - "igdbId": 29, - "igdbName": "Sega Mega Drive/Genesis", - "igdbSlug": "genesis-slash-megadrive", - "alternateNames": [ - "genesis-slash-megadrive", - "Sega Genesis", - "Sega Mega Drive", - "Sega Mega Drive & Genesis", - "Sega Mega Drive/Genesis" - ], - "extensions": { - "supportedFileExtensions": [ - ".7Z", - ".BIN", - ".GEN", - ".MD", - ".SG", - ".SMD", - ".ZIP" - ], - "uniqueFileExtensions": [ - ".GEN", - ".SG" - ] - }, - "retroPieDirectoryName": "megadrive", - "webEmulator": { - "type": "EmulatorJS", - "core": "segaMD", - "availableWebEmulators": [ - { - "emulatorType": "EmulatorJS", - "availableWebEmulatorCores": [ - { - "core": "segaMD", - "alternateCoreName": "genesis_plus_gx", - "default": true - }, - { - "core": "picodrive" - } - ] - } - ] - }, - "bios": [ + "Bios": [ + { + "hash": "ca30b50f880eb660a320674ed365ef7a", + "description": "Family Computer Disk System BIOS - Required for Famicom Disk System emulation", + "filename": "disksys.rom" + }, + { + "hash": "7f98d77d7a094ad7d069b74bd553ec98", + "description": "Game Genie add-on cartridge - Required for Game Genei Add-on emulation (Only supported on the fceumm core)", + "filename": "gamegenie.nes" + } + ] + }, + { + "IGDBId": 21, + "IGDBName": "Nintendo GameCube", + "IGDBSlug": "ngc", + "AlternateNames": [ + "GameCube", + "GC", + "GCN", + "ngc", + "Nintendo GameCube" + ], + "Extensions": { + "SupportedFileExtensions": [ + ".CISO", + ".GC", + ".GCM", + ".GCZ", + ".ISO", + ".RVZ" + ], + "UniqueFileExtensions": [ + ".CISO", + ".GC", + ".GCM", + ".GCZ", + ".RVZ" + ] + }, + "RetroPieDirectoryName": "gc", + "WebEmulator": { + "Type": "", + "Core": "", + "AvailableWebEmulators": [] + }, + "Bios": [] + }, + { + "IGDBId": 130, + "IGDBName": "Nintendo Switch", + "IGDBSlug": "switch", + "AlternateNames": [ + "NX" + ], + "Extensions": { + "SupportedFileExtensions": [], + "UniqueFileExtensions": [] + }, + "RetroPieDirectoryName": "", + "WebEmulator": { + "Type": "", + "Core": "", + "AvailableWebEmulators": null + }, + "Bios": [] + }, + { + "IGDBId": 384, + "IGDBName": "Oculus Quest", + "IGDBSlug": "oculus-quest", + "AlternateNames": [ + "Quest" + ], + "Extensions": { + "SupportedFileExtensions": [], + "UniqueFileExtensions": [] + }, + "RetroPieDirectoryName": "", + "WebEmulator": { + "Type": "", + "Core": "", + "AvailableWebEmulators": null + }, + "Bios": [] + }, + { + "IGDBId": 162, + "IGDBName": "Oculus VR", + "IGDBSlug": "oculus-vr", + "AlternateNames": [], + "Extensions": { + "SupportedFileExtensions": [], + "UniqueFileExtensions": [] + }, + "RetroPieDirectoryName": "", + "WebEmulator": { + "Type": "", + "Core": "", + "AvailableWebEmulators": null + }, + "Bios": [] + }, + { + "IGDBId": 88, + "IGDBName": "Odyssey", + "IGDBSlug": "odyssey--1", + "AlternateNames": [ + "Magnavox Odyssey; Odysee; Odisea; Odissea" + ], + "Extensions": { + "SupportedFileExtensions": [], + "UniqueFileExtensions": [] + }, + "RetroPieDirectoryName": "", + "WebEmulator": { + "Type": "", + "Core": "", + "AvailableWebEmulators": null + }, + "Bios": [] + }, + { + "IGDBId": 133, + "IGDBName": "Odyssey 2 / Videopac G7000", + "IGDBSlug": "odyssey-2-slash-videopac-g7000", + "AlternateNames": [ + "Magnavox Odyssey²" + ], + "Extensions": { + "SupportedFileExtensions": [], + "UniqueFileExtensions": [] + }, + "RetroPieDirectoryName": "", + "WebEmulator": { + "Type": "", + "Core": "", + "AvailableWebEmulators": null + }, + "Bios": [] + }, + { + "IGDBId": 72, + "IGDBName": "Ouya", + "IGDBSlug": "ouya", + "AlternateNames": [], + "Extensions": { + "SupportedFileExtensions": [], + "UniqueFileExtensions": [] + }, + "RetroPieDirectoryName": "", + "WebEmulator": { + "Type": "", + "Core": "", + "AvailableWebEmulators": null + }, + "Bios": [] + }, + { + "IGDBId": 417, + "IGDBName": "Palm OS", + "IGDBSlug": "palm-os", + "AlternateNames": [ + "Garnet OS" + ], + "Extensions": { + "SupportedFileExtensions": [], + "UniqueFileExtensions": [] + }, + "RetroPieDirectoryName": "", + "WebEmulator": { + "Type": "", + "Core": "", + "AvailableWebEmulators": null + }, + "Bios": [] + }, + { + "IGDBId": 6, + "IGDBName": "PC (Microsoft Windows)", + "IGDBSlug": "win", + "AlternateNames": [ + "mswin" + ], + "Extensions": { + "SupportedFileExtensions": [], + "UniqueFileExtensions": [] + }, + "RetroPieDirectoryName": "", + "WebEmulator": { + "Type": "", + "Core": "", + "AvailableWebEmulators": null + }, + "Bios": [] + }, + { + "IGDBId": 142, + "IGDBName": "PC-50X Family", + "IGDBSlug": "pc-50x-family", + "AlternateNames": [], + "Extensions": { + "SupportedFileExtensions": [], + "UniqueFileExtensions": [] + }, + "RetroPieDirectoryName": "", + "WebEmulator": { + "Type": "", + "Core": "", + "AvailableWebEmulators": null + }, + "Bios": [] + }, + { + "IGDBId": 125, + "IGDBName": "PC-8800 Series", + "IGDBSlug": "pc-8800-series", + "AlternateNames": [], + "Extensions": { + "SupportedFileExtensions": [], + "UniqueFileExtensions": [] + }, + "RetroPieDirectoryName": "", + "WebEmulator": { + "Type": "", + "Core": "", + "AvailableWebEmulators": null + }, + "Bios": [] + }, + { + "IGDBId": 149, + "IGDBName": "PC-9800 Series", + "IGDBSlug": "pc-9800-series", + "AlternateNames": [ + "PC-98" + ], + "Extensions": { + "SupportedFileExtensions": [], + "UniqueFileExtensions": [] + }, + "RetroPieDirectoryName": "", + "WebEmulator": { + "Type": "", + "Core": "", + "AvailableWebEmulators": null + }, + "Bios": [] + }, + { + "IGDBId": 274, + "IGDBName": "PC-FX", + "IGDBSlug": "pc-fx", + "AlternateNames": [ + "NEC PC-FX", + "PC-FX" + ], + "Extensions": { + "SupportedFileExtensions": [], + "UniqueFileExtensions": [] + }, + "RetroPieDirectoryName": "", + "WebEmulator": { + "Type": "EmulatorJS", + "Core": "mednafen_pcfx", + "AvailableWebEmulators": [ + { + "EmulatorType": "EmulatorJS", + "AvailableWebEmulatorCores": [ { - "hash": "45e298905a08f9cfb38fd504cd6dbc84", - "description": "MegaDrive TMSS startup ROM", - "filename": "bios_MD.bin" + "Core": "mednafen_pcfx", + "AlternateCoreName": "mednafen_pcfx", + "Default": true } - ] + ] + } + ] }, - { - "igdbId": 32, - "igdbName": "Sega Saturn", - "igdbSlug": "saturn", - "alternateNames": [ - "Hi-Saturn", - "JVC Saturn", - "JVC Saturn, Hi-Saturn, Samsung Saturn, V-Saturn", - "Samsung Saturn", - "Saturn", - "Sega Saturn", - "V-Saturn" - ], - "extensions": { - "supportedFileExtensions": [ - ".BIN", - ".CUE", - ".ISO", - ".MDF" - ], - "uniqueFileExtensions": [ - ".MDF" - ] - }, - "retroPieDirectoryName": "saturn", - "webEmulator": { - "type": "EmulatorJS", - "core": "segaSaturn", - "availableWebEmulators": [ - { - "emulatorType": "EmulatorJS", - "availableWebEmulatorCores": [ - { - "core": "segaSaturn", - "alternateCoreName": "yabause", - "default": true - }, - { - "core": "mgba" - } - ] - } - ] - }, - "bios": [ + "Bios": [] + }, + { + "IGDBId": 96, + "IGDBName": "PDP-10", + "IGDBSlug": "pdp10", + "AlternateNames": [], + "Extensions": { + "SupportedFileExtensions": [], + "UniqueFileExtensions": [] + }, + "RetroPieDirectoryName": "", + "WebEmulator": { + "Type": "", + "Core": "", + "AvailableWebEmulators": null + }, + "Bios": [] + }, + { + "IGDBId": 108, + "IGDBName": "PDP-11", + "IGDBSlug": "pdp11", + "AlternateNames": [], + "Extensions": { + "SupportedFileExtensions": [], + "UniqueFileExtensions": [] + }, + "RetroPieDirectoryName": "", + "WebEmulator": { + "Type": "", + "Core": "", + "AvailableWebEmulators": null + }, + "Bios": [] + }, + { + "IGDBId": 97, + "IGDBName": "PDP-8", + "IGDBSlug": "pdp-8--1", + "AlternateNames": [], + "Extensions": { + "SupportedFileExtensions": [], + "UniqueFileExtensions": [] + }, + "RetroPieDirectoryName": "", + "WebEmulator": { + "Type": "", + "Core": "", + "AvailableWebEmulators": null + }, + "Bios": [] + }, + { + "IGDBId": 117, + "IGDBName": "Philips CD-i", + "IGDBSlug": "philips-cd-i", + "AlternateNames": [], + "Extensions": { + "SupportedFileExtensions": [], + "UniqueFileExtensions": [] + }, + "RetroPieDirectoryName": "", + "WebEmulator": { + "Type": "", + "Core": "", + "AvailableWebEmulators": null + }, + "Bios": [] + }, + { + "IGDBId": 381, + "IGDBName": "Playdate", + "IGDBSlug": "playdate", + "AlternateNames": [], + "Extensions": { + "SupportedFileExtensions": [], + "UniqueFileExtensions": [] + }, + "RetroPieDirectoryName": "", + "WebEmulator": { + "Type": "", + "Core": "", + "AvailableWebEmulators": null + }, + "Bios": [] + }, + { + "IGDBId": 7, + "IGDBName": "PlayStation", + "IGDBSlug": "ps", + "AlternateNames": [ + "PlayStation", + "PS", + "PS1", + "PSOne", + "PSX", + "PSX, PSOne, PS", + "Sony PlayStation" + ], + "Extensions": { + "SupportedFileExtensions": [ + ".CCD", + ".CHD", + ".CUE", + ".EXE", + ".ISO", + ".M3U", + ".PBP", + ".TOC" + ], + "UniqueFileExtensions": [ + ".EXE", + ".PBP", + ".TOC" + ] + }, + "RetroPieDirectoryName": "psx", + "WebEmulator": { + "Type": "EmulatorJS", + "Core": "psx", + "AvailableWebEmulators": [ + { + "EmulatorType": "EmulatorJS", + "AvailableWebEmulatorCores": [ { - "hash": "af5828fdff51384f99b3c4926be27762", - "description": "Saturn BIOS", - "filename": "saturn_bios.bin" + "Core": "psx", + "AlternateCoreName": "pcsx_rearmed", + "Default": true + }, + { + "Core": "mednafen_psx", + "AlternateCoreName": "", + "Default": false } - ] + ] + } + ] }, - { - "igdbId": 19, - "igdbName": "Super Nintendo Entertainment System", - "igdbSlug": "snes", - "alternateNames": [ - "Nintendo Super Famicom & Super Entertainment System", - "SNES", - "SNES, Super Nintendo", - "Super Nintendo", - "Super Nintendo Entertainment System" - ], - "extensions": { - "supportedFileExtensions": [ - ".7Z", - ".BIN", - ".BS", - ".FIG", - ".MGD", - ".SFC", - ".SMC", - ".SWC", - ".ZIP" - ], - "uniqueFileExtensions": [ - ".BS", - ".FIG", - ".MGD", - ".SFC", - ".SMC", - ".SWC" - ] - }, - "retroPieDirectoryName": "snes", - "webEmulator": { - "type": "EmulatorJS", - "core": "snes", - "availableWebEmulators": [ - { - "emulatorType": "EmulatorJS", - "availableWebEmulatorCores": [ - { - "core": "snes", - "alternateCoreName": "snes9x", - "default": true - } - ] - } - ] - }, - "bios": [] + "Bios": [ + { + "hash": "8dd7d5296a650fac7319bce665a6a53c", + "description": "PS1 JP BIOS - Required for JP games", + "filename": "scph5500.bin" + }, + { + "hash": "490f666e1afb15b7362b406ed1cea246", + "description": "PS1 US BIOS - Required for US games", + "filename": "scph5501.bin" + }, + { + "hash": "32736f17079d0b2b7024407c39bd3050", + "description": "PS1 EU BIOS - Required for EU games", + "filename": "scph5502.bin" + } + ] + }, + { + "IGDBId": 8, + "IGDBName": "PlayStation 2", + "IGDBSlug": "ps2", + "AlternateNames": [ + "PS2" + ], + "Extensions": { + "SupportedFileExtensions": [], + "UniqueFileExtensions": [] }, - { - "igdbId": 87, - "igdbName": "Virtual Boy", - "igdbSlug": "virtualboy", - "alternateNames": [ - "VB" - ], - "extensions": { - "supportedFileExtensions": [ - ".7Z", - ".VB", - ".ZIP" - ], - "uniqueFileExtensions": [ - ".VB" - ] - }, - "retroPieDirectoryName": "vb", - "webEmulator": { - "type": "EmulatorJS", - "core": "vb", - "availableWebEmulators": [ - { - "emulatorType": "EmulatorJS", - "availableWebEmulatorCores": [ - { - "core": "vb", - "alternateCoreName": "beetle_vb", - "default": true - } - ] - } - ] - }, - "bios": [] + "RetroPieDirectoryName": "", + "WebEmulator": { + "Type": "", + "Core": "", + "AvailableWebEmulators": null }, - { - "igdbId": 26, - "igdbName": "ZX Spectrum", - "igdbSlug": "zxs", - "alternateNames": [ - "Sinclair ZX", - "Sinclair ZX Spectrum", - "ZX Spectrum", - "zxs" - ], - "extensions": { - "supportedFileExtensions": [ - ".DSK", - ".GZ", - ".IMG", - ".MGT", - ".SCL", - ".SNA", - ".SZX", - ".TAP", - ".TRD", - ".TZX", - ".UDI", - ".Z80" - ], - "uniqueFileExtensions": [ - ".MGT", - ".SCL", - ".SNA", - ".SZX", - ".TRD", - ".TZX", - ".UDI", - ".Z80" - ] - }, - "retroPieDirectoryName": "zxspectrum", - "webEmulator": { - "type": "", - "core": "" - }, - "bios": [] - } + "Bios": [] + }, + { + "IGDBId": 9, + "IGDBName": "PlayStation 3", + "IGDBSlug": "ps3", + "AlternateNames": [ + "PS3" + ], + "Extensions": { + "SupportedFileExtensions": [], + "UniqueFileExtensions": [] + }, + "RetroPieDirectoryName": "", + "WebEmulator": { + "Type": "", + "Core": "", + "AvailableWebEmulators": null + }, + "Bios": [] + }, + { + "IGDBId": 48, + "IGDBName": "PlayStation 4", + "IGDBSlug": "ps4--1", + "AlternateNames": [ + "PS4" + ], + "Extensions": { + "SupportedFileExtensions": [], + "UniqueFileExtensions": [] + }, + "RetroPieDirectoryName": "", + "WebEmulator": { + "Type": "", + "Core": "", + "AvailableWebEmulators": null + }, + "Bios": [] + }, + { + "IGDBId": 167, + "IGDBName": "PlayStation 5", + "IGDBSlug": "ps5", + "AlternateNames": [ + "PS5" + ], + "Extensions": { + "SupportedFileExtensions": [], + "UniqueFileExtensions": [] + }, + "RetroPieDirectoryName": "", + "WebEmulator": { + "Type": "", + "Core": "", + "AvailableWebEmulators": null + }, + "Bios": [] + }, + { + "IGDBId": 38, + "IGDBName": "PlayStation Portable", + "IGDBSlug": "psp", + "AlternateNames": [ + "PSP" + ], + "Extensions": { + "SupportedFileExtensions": [], + "UniqueFileExtensions": [] + }, + "RetroPieDirectoryName": "", + "WebEmulator": { + "Type": "", + "Core": "", + "AvailableWebEmulators": null + }, + "Bios": [] + }, + { + "IGDBId": 46, + "IGDBName": "PlayStation Vita", + "IGDBSlug": "psvita", + "AlternateNames": [ + "PS Vita" + ], + "Extensions": { + "SupportedFileExtensions": [], + "UniqueFileExtensions": [] + }, + "RetroPieDirectoryName": "", + "WebEmulator": { + "Type": "", + "Core": "", + "AvailableWebEmulators": null + }, + "Bios": [] + }, + { + "IGDBId": 165, + "IGDBName": "PlayStation VR", + "IGDBSlug": "psvr", + "AlternateNames": [ + "PSVR" + ], + "Extensions": { + "SupportedFileExtensions": [], + "UniqueFileExtensions": [] + }, + "RetroPieDirectoryName": "", + "WebEmulator": { + "Type": "", + "Core": "", + "AvailableWebEmulators": null + }, + "Bios": [] + }, + { + "IGDBId": 377, + "IGDBName": "Plug & Play", + "IGDBSlug": "plug-and-play", + "AlternateNames": [ + "TV Game" + ], + "Extensions": { + "SupportedFileExtensions": [], + "UniqueFileExtensions": [] + }, + "RetroPieDirectoryName": "", + "WebEmulator": { + "Type": "", + "Core": "", + "AvailableWebEmulators": null + }, + "Bios": [] + }, + { + "IGDBId": 306, + "IGDBName": "Satellaview", + "IGDBSlug": "satellaview", + "AlternateNames": [], + "Extensions": { + "SupportedFileExtensions": [], + "UniqueFileExtensions": [] + }, + "RetroPieDirectoryName": "", + "WebEmulator": { + "Type": "", + "Core": "", + "AvailableWebEmulators": null + }, + "Bios": [] + }, + { + "IGDBId": 30, + "IGDBName": "Sega 32X", + "IGDBSlug": "sega32", + "AlternateNames": [ + "Sega 32X", + "Sega32", + "Sega32X" + ], + "Extensions": { + "SupportedFileExtensions": [ + ".32X", + ".7Z", + ".BIN", + ".MD", + ".ZIP" + ], + "UniqueFileExtensions": [ + ".32X" + ] + }, + "RetroPieDirectoryName": "sega32x", + "WebEmulator": { + "Type": "EmulatorJS", + "Core": "sega32x", + "AvailableWebEmulators": [ + { + "EmulatorType": "EmulatorJS", + "AvailableWebEmulatorCores": [ + { + "Core": "sega32x", + "AlternateCoreName": "picodrive", + "Default": true + } + ] + } + ] + }, + "Bios": [] + }, + { + "IGDBId": 78, + "IGDBName": "Sega CD", + "IGDBSlug": "segacd", + "AlternateNames": [ + "Mega CD", + "Sega CD", + "Sega Mega-CD & Sega CD", + "segacd" + ], + "Extensions": { + "SupportedFileExtensions": [ + ".BIN", + ".CHD", + ".CUE", + ".ISO", + ".M3U" + ], + "UniqueFileExtensions": [] + }, + "RetroPieDirectoryName": "segacd", + "WebEmulator": { + "Type": "EmulatorJS", + "Core": "segaCD", + "AvailableWebEmulators": [ + { + "EmulatorType": "EmulatorJS", + "AvailableWebEmulatorCores": [ + { + "Core": "segaCD", + "AlternateCoreName": "genesis_plus_gx", + "Default": true + }, + { + "Core": "picodrive", + "AlternateCoreName": "", + "Default": false + } + ] + } + ] + }, + "Bios": [ + { + "hash": "e66fa1dc5820d254611fdcdba0662372", + "description": "MegaCD EU BIOS - Required", + "filename": "bios_CD_E.bin" + }, + { + "hash": "278a9397d192149e84e820ac621a8edd", + "description": "MegaCD JP BIOS - Required", + "filename": "bios_CD_J.bin" + }, + { + "hash": "2efd74e3232ff260e371b99f84024f7f", + "description": "SegaCD US BIOS - Required", + "filename": "bios_CD_U.bin" + } + ] + }, + { + "IGDBId": 35, + "IGDBName": "Sega Game Gear", + "IGDBSlug": "gamegear", + "AlternateNames": [ + "Game Gear", + "gamegear", + "GG", + "Sega Game Gear" + ], + "Extensions": { + "SupportedFileExtensions": [ + ".7Z", + ".BIN", + ".GG", + ".ZIP" + ], + "UniqueFileExtensions": [ + ".GG" + ] + }, + "RetroPieDirectoryName": "gamegear", + "WebEmulator": { + "Type": "EmulatorJS", + "Core": "segaGG", + "AvailableWebEmulators": [ + { + "EmulatorType": "EmulatorJS", + "AvailableWebEmulatorCores": [ + { + "Core": "segaGG", + "AlternateCoreName": "genesis_plus_gx", + "Default": true + } + ] + } + ] + }, + "Bios": [ + { + "hash": "672e104c3be3a238301aceffc3b23fd6", + "description": "GameGear BIOS (bootrom) - Optional", + "filename": "bios.gg" + } + ] + }, + { + "IGDBId": 64, + "IGDBName": "Sega Master System/Mark III", + "IGDBSlug": "sms", + "AlternateNames": [ + "Sega Mark III & Master System", + "Sega Master System", + "Sega Master System/Mark III", + "sms", + "SMS, Mark III" + ], + "Extensions": { + "SupportedFileExtensions": [ + ".7Z", + ".BIN", + ".SMS", + ".ZIP" + ], + "UniqueFileExtensions": [ + ".SMS" + ] + }, + "RetroPieDirectoryName": "mastersystem", + "WebEmulator": { + "Type": "EmulatorJS", + "Core": "smsplus", + "AvailableWebEmulators": [ + { + "EmulatorType": "EmulatorJS", + "AvailableWebEmulatorCores": [ + { + "Core": "segaMS", + "AlternateCoreName": "smsplus", + "Default": true + }, + { + "Core": "picodrive", + "AlternateCoreName": "", + "Default": false + }, + { + "Core": "genesis_plus_gx", + "AlternateCoreName": "", + "Default": false + } + ] + } + ] + }, + "Bios": [ + { + "hash": "840481177270d5642a14ca71ee72844c", + "description": "MasterSystem EU BIOS", + "filename": "bios_E.sms" + }, + { + "hash": "24a519c53f67b00640d0048ef7089105", + "description": "MasterSystem JP BIOS", + "filename": "bios_J.sms" + }, + { + "hash": "840481177270d5642a14ca71ee72844c", + "description": "MasterSystem US BIOS", + "filename": "bios_U.sms" + } + ] + }, + { + "IGDBId": 29, + "IGDBName": "Sega Mega Drive/Genesis", + "IGDBSlug": "genesis-slash-megadrive", + "AlternateNames": [ + "genesis-slash-megadrive", + "Sega Genesis", + "Sega Mega Drive", + "Sega Mega Drive & Genesis", + "Sega Mega Drive/Genesis" + ], + "Extensions": { + "SupportedFileExtensions": [ + ".7Z", + ".BIN", + ".GEN", + ".MD", + ".SG", + ".SMD", + ".ZIP" + ], + "UniqueFileExtensions": [ + ".GEN", + ".SG", + ".SMD" + ] + }, + "RetroPieDirectoryName": "megadrive", + "WebEmulator": { + "Type": "EmulatorJS", + "Core": "segaMD", + "AvailableWebEmulators": [ + { + "EmulatorType": "EmulatorJS", + "AvailableWebEmulatorCores": [ + { + "Core": "segaMD", + "AlternateCoreName": "genesis_plus_gx", + "Default": true + }, + { + "Core": "picodrive", + "AlternateCoreName": "", + "Default": false + } + ] + } + ] + }, + "Bios": [ + { + "hash": "45e298905a08f9cfb38fd504cd6dbc84", + "description": "MegaDrive TMSS startup ROM", + "filename": "bios_MD.bin" + } + ] + }, + { + "IGDBId": 32, + "IGDBName": "Sega Saturn", + "IGDBSlug": "saturn", + "AlternateNames": [ + "Hi-Saturn", + "JVC Saturn", + "JVC Saturn, Hi-Saturn, Samsung Saturn, V-Saturn", + "Samsung Saturn", + "Saturn", + "Sega Saturn", + "V-Saturn" + ], + "Extensions": { + "SupportedFileExtensions": [ + ".BIN", + ".CUE", + ".ISO", + ".MDF" + ], + "UniqueFileExtensions": [ + ".MDF" + ] + }, + "RetroPieDirectoryName": "saturn", + "WebEmulator": { + "Type": "EmulatorJS", + "Core": "segaSaturn", + "AvailableWebEmulators": [ + { + "EmulatorType": "EmulatorJS", + "AvailableWebEmulatorCores": [ + { + "Core": "segaSaturn", + "AlternateCoreName": "yabause", + "Default": true + }, + { + "Core": "mgba", + "AlternateCoreName": "", + "Default": false + } + ] + } + ] + }, + "Bios": [ + { + "hash": "af5828fdff51384f99b3c4926be27762", + "description": "Saturn BIOS", + "filename": "saturn_bios.bin" + } + ] + }, + { + "IGDBId": 84, + "IGDBName": "SG-1000", + "IGDBSlug": "sg1000", + "AlternateNames": [ + "Sega Game 1000" + ], + "Extensions": { + "SupportedFileExtensions": [], + "UniqueFileExtensions": [] + }, + "RetroPieDirectoryName": "", + "WebEmulator": { + "Type": "", + "Core": "", + "AvailableWebEmulators": null + }, + "Bios": [] + }, + { + "IGDBId": 374, + "IGDBName": "Sharp MZ-2200", + "IGDBSlug": "sharp-mz-2200", + "AlternateNames": [], + "Extensions": { + "SupportedFileExtensions": [], + "UniqueFileExtensions": [] + }, + "RetroPieDirectoryName": "", + "WebEmulator": { + "Type": "", + "Core": "", + "AvailableWebEmulators": null + }, + "Bios": [] + }, + { + "IGDBId": 77, + "IGDBName": "Sharp X1", + "IGDBSlug": "x1", + "AlternateNames": [], + "Extensions": { + "SupportedFileExtensions": [], + "UniqueFileExtensions": [] + }, + "RetroPieDirectoryName": "", + "WebEmulator": { + "Type": "", + "Core": "", + "AvailableWebEmulators": null + }, + "Bios": [] + }, + { + "IGDBId": 121, + "IGDBName": "Sharp X68000", + "IGDBSlug": "sharp-x68000", + "AlternateNames": [], + "Extensions": { + "SupportedFileExtensions": [], + "UniqueFileExtensions": [] + }, + "RetroPieDirectoryName": "", + "WebEmulator": { + "Type": "", + "Core": "", + "AvailableWebEmulators": null + }, + "Bios": [] + }, + { + "IGDBId": 406, + "IGDBName": "Sinclair QL", + "IGDBSlug": "sinclair-ql", + "AlternateNames": [ + "Sinclair Quantum Leap" + ], + "Extensions": { + "SupportedFileExtensions": [], + "UniqueFileExtensions": [] + }, + "RetroPieDirectoryName": "", + "WebEmulator": { + "Type": "", + "Core": "", + "AvailableWebEmulators": null + }, + "Bios": [] + }, + { + "IGDBId": 373, + "IGDBName": "Sinclair ZX81", + "IGDBSlug": "sinclair-zx81", + "AlternateNames": [ + "ZX81" + ], + "Extensions": { + "SupportedFileExtensions": [], + "UniqueFileExtensions": [] + }, + "RetroPieDirectoryName": "", + "WebEmulator": { + "Type": "", + "Core": "", + "AvailableWebEmulators": null + }, + "Bios": [] + }, + { + "IGDBId": 163, + "IGDBName": "SteamVR", + "IGDBSlug": "steam-vr", + "AlternateNames": [], + "Extensions": { + "SupportedFileExtensions": [], + "UniqueFileExtensions": [] + }, + "RetroPieDirectoryName": "", + "WebEmulator": { + "Type": "", + "Core": "", + "AvailableWebEmulators": null + }, + "Bios": [] + }, + { + "IGDBId": 58, + "IGDBName": "Super Famicom", + "IGDBSlug": "sfam", + "AlternateNames": [ + "SFC" + ], + "Extensions": { + "SupportedFileExtensions": [], + "UniqueFileExtensions": [] + }, + "RetroPieDirectoryName": "", + "WebEmulator": { + "Type": "", + "Core": "", + "AvailableWebEmulators": null + }, + "Bios": [] + }, + { + "IGDBId": 19, + "IGDBName": "Super Nintendo Entertainment System", + "IGDBSlug": "snes", + "AlternateNames": [ + "Nintendo Super Famicom & Super Entertainment System", + "SNES", + "SNES, Super Nintendo", + "Super Nintendo", + "Super Nintendo Entertainment System" + ], + "Extensions": { + "SupportedFileExtensions": [ + ".7Z", + ".BIN", + ".BS", + ".FIG", + ".MGD", + ".SFC", + ".SMC", + ".SWC", + ".ZIP" + ], + "UniqueFileExtensions": [ + ".BS", + ".FIG", + ".MGD", + ".SFC", + ".SMC", + ".SWC" + ] + }, + "RetroPieDirectoryName": "snes", + "WebEmulator": { + "Type": "EmulatorJS", + "Core": "snes", + "AvailableWebEmulators": [ + { + "EmulatorType": "EmulatorJS", + "AvailableWebEmulatorCores": [ + { + "Core": "snes", + "AlternateCoreName": "snes9x", + "Default": true + } + ] + } + ] + }, + "Bios": [] + }, + { + "IGDBId": 44, + "IGDBName": "Tapwave Zodiac", + "IGDBSlug": "zod", + "AlternateNames": [], + "Extensions": { + "SupportedFileExtensions": [], + "UniqueFileExtensions": [] + }, + "RetroPieDirectoryName": "", + "WebEmulator": { + "Type": "", + "Core": "", + "AvailableWebEmulators": null + }, + "Bios": [] + }, + { + "IGDBId": 155, + "IGDBName": "Tatung Einstein", + "IGDBSlug": "tatung-einstein", + "AlternateNames": [], + "Extensions": { + "SupportedFileExtensions": [], + "UniqueFileExtensions": [] + }, + "RetroPieDirectoryName": "", + "WebEmulator": { + "Type": "", + "Core": "", + "AvailableWebEmulators": null + }, + "Bios": [] + }, + { + "IGDBId": 479, + "IGDBName": "Terebikko / See 'n Say Video Phone", + "IGDBSlug": "terebikko-slash-see-n-say-video-phone", + "AlternateNames": [], + "Extensions": { + "SupportedFileExtensions": [], + "UniqueFileExtensions": [] + }, + "RetroPieDirectoryName": "", + "WebEmulator": { + "Type": "", + "Core": "", + "AvailableWebEmulators": null + }, + "Bios": [] + }, + { + "IGDBId": 129, + "IGDBName": "Texas Instruments TI-99", + "IGDBSlug": "ti-99", + "AlternateNames": [ + "Texas Instruments TI-99/4A" + ], + "Extensions": { + "SupportedFileExtensions": [], + "UniqueFileExtensions": [] + }, + "RetroPieDirectoryName": "", + "WebEmulator": { + "Type": "", + "Core": "", + "AvailableWebEmulators": null + }, + "Bios": [] + }, + { + "IGDBId": 156, + "IGDBName": "Thomson MO5", + "IGDBSlug": "thomson-mo5", + "AlternateNames": [], + "Extensions": { + "SupportedFileExtensions": [], + "UniqueFileExtensions": [] + }, + "RetroPieDirectoryName": "", + "WebEmulator": { + "Type": "", + "Core": "", + "AvailableWebEmulators": null + }, + "Bios": [] + }, + { + "IGDBId": 126, + "IGDBName": "TRS-80", + "IGDBSlug": "trs-80", + "AlternateNames": [], + "Extensions": { + "SupportedFileExtensions": [], + "UniqueFileExtensions": [] + }, + "RetroPieDirectoryName": "", + "WebEmulator": { + "Type": "", + "Core": "", + "AvailableWebEmulators": null + }, + "Bios": [] + }, + { + "IGDBId": 151, + "IGDBName": "TRS-80 Color Computer", + "IGDBSlug": "trs-80-color-computer", + "AlternateNames": [ + "Tandy Color Computer" + ], + "Extensions": { + "SupportedFileExtensions": [], + "UniqueFileExtensions": [] + }, + "RetroPieDirectoryName": "", + "WebEmulator": { + "Type": "", + "Core": "", + "AvailableWebEmulators": null + }, + "Bios": [] + }, + { + "IGDBId": 86, + "IGDBName": "TurboGrafx-16/PC Engine", + "IGDBSlug": "turbografx16--1", + "AlternateNames": [ + "TG16" + ], + "Extensions": { + "SupportedFileExtensions": [ + ".7Z", + ".CCD", + ".CHD", + ".CUE", + ".PCE", + ".ZIP" + ], + "UniqueFileExtensions": [] + }, + "RetroPieDirectoryName": "pcengine", + "WebEmulator": { + "Type": "EmulatorJS", + "Core": "mednafen_pce", + "AvailableWebEmulators": [ + { + "EmulatorType": "EmulatorJS", + "AvailableWebEmulatorCores": [ + { + "Core": "mednafen_pce", + "AlternateCoreName": "pce", + "Default": true + } + ] + } + ] + }, + "Bios": [ + { + "hash": "6d2cb14fc3e1f65ceb135633d1694122", + "description": "[BIOS] Games Express CD Card (Japan).pce", + "filename": "gexpress.pce" + }, + { + "hash": "ccf8590e2e7ac4a08bcc1d77ec168917", + "description": "BIOS] Games Express CD Card (Japan) (Alt).pce", + "filename": "gexpress.pce" + }, + { + "hash": "2b7ccb3d86baa18f6402c176f3065082", + "description": "[BIOS] CD-ROM System (Japan) (v1.0).pce", + "filename": "syscard1.pce" + }, + { + "hash": "3a456f0eccff039eb5ff045f56ec1c3b", + "description": "[BIOS] CD-ROM System (Japan) (v2.0).pce", + "filename": "syscard2.pce" + }, + { + "hash": "3cdd6614a918616bfc41c862e889dd79", + "description": "[BIOS] CD-ROM System (Japan) (v2.1).pce", + "filename": "syscard2.pce" + }, + { + "hash": "94279f315e8b52904f65ab3108542afe", + "description": "[BIOS] TurboGrafx CD System Card (USA) (v2.0).pce", + "filename": "syscard2u.pce" + }, + { + "hash": "38179df8f4ac870017db21ebcbf53114", + "description": "[BIOS] Super CD-ROM System (Japan) (v3.0).pce", + "filename": "syscard3.pce" + }, + { + "hash": "0754f903b52e3b3342202bdafb13efa5", + "description": "[BIOS] TurboGrafx CD Super System Card (USA) (v3.0).pce", + "filename": "syscard3u.pce" + } + ] + }, + { + "IGDBId": 150, + "IGDBName": "Turbografx-16/PC Engine CD", + "IGDBSlug": "turbografx-16-slash-pc-engine-cd", + "AlternateNames": [ + "TG-16CD/PCECD" + ], + "Extensions": { + "SupportedFileExtensions": [ + ".7Z", + ".CCD", + ".CHD", + ".CUE", + ".PCE", + ".ZIP" + ], + "UniqueFileExtensions": [] + }, + "RetroPieDirectoryName": "pcengine", + "WebEmulator": { + "Type": "EmulatorJS", + "Core": "mednafen_pce", + "AvailableWebEmulators": [ + { + "EmulatorType": "EmulatorJS", + "AvailableWebEmulatorCores": [ + { + "Core": "mednafen_pce", + "AlternateCoreName": "pce", + "Default": true + } + ] + } + ] + }, + "Bios": [ + { + "hash": "6d2cb14fc3e1f65ceb135633d1694122", + "description": "[BIOS] Games Express CD Card (Japan).pce", + "filename": "gexpress.pce" + }, + { + "hash": "ccf8590e2e7ac4a08bcc1d77ec168917", + "description": "BIOS] Games Express CD Card (Japan) (Alt).pce", + "filename": "gexpress.pce" + }, + { + "hash": "2b7ccb3d86baa18f6402c176f3065082", + "description": "[BIOS] CD-ROM System (Japan) (v1.0).pce", + "filename": "syscard1.pce" + }, + { + "hash": "3a456f0eccff039eb5ff045f56ec1c3b", + "description": "[BIOS] CD-ROM System (Japan) (v2.0).pce", + "filename": "syscard2.pce" + }, + { + "hash": "3cdd6614a918616bfc41c862e889dd79", + "description": "[BIOS] CD-ROM System (Japan) (v2.1).pce", + "filename": "syscard2.pce" + }, + { + "hash": "94279f315e8b52904f65ab3108542afe", + "description": "[BIOS] TurboGrafx CD System Card (USA) (v2.0).pce", + "filename": "syscard2u.pce" + }, + { + "hash": "38179df8f4ac870017db21ebcbf53114", + "description": "[BIOS] Super CD-ROM System (Japan) (v3.0).pce", + "filename": "syscard3.pce" + }, + { + "hash": "0754f903b52e3b3342202bdafb13efa5", + "description": "[BIOS] TurboGrafx CD Super System Card (USA) (v3.0).pce", + "filename": "syscard3u.pce" + } + ] + }, + { + "IGDBId": 439, + "IGDBName": "V.Smile", + "IGDBSlug": "vsmile", + "AlternateNames": [ + "V.SMILE TV LEARNING SYSTEM" + ], + "Extensions": { + "SupportedFileExtensions": [], + "UniqueFileExtensions": [] + }, + "RetroPieDirectoryName": "", + "WebEmulator": { + "Type": "", + "Core": "", + "AvailableWebEmulators": null + }, + "Bios": [] + }, + { + "IGDBId": 138, + "IGDBName": "VC 4000", + "IGDBSlug": "vc-4000", + "AlternateNames": [], + "Extensions": { + "SupportedFileExtensions": [], + "UniqueFileExtensions": [] + }, + "RetroPieDirectoryName": "", + "WebEmulator": { + "Type": "", + "Core": "", + "AvailableWebEmulators": null + }, + "Bios": [] + }, + { + "IGDBId": 70, + "IGDBName": "Vectrex", + "IGDBSlug": "vectrex", + "AlternateNames": [], + "Extensions": { + "SupportedFileExtensions": [], + "UniqueFileExtensions": [] + }, + "RetroPieDirectoryName": "", + "WebEmulator": { + "Type": "", + "Core": "", + "AvailableWebEmulators": null + }, + "Bios": [] + }, + { + "IGDBId": 87, + "IGDBName": "Virtual Boy", + "IGDBSlug": "virtualboy", + "AlternateNames": [ + "VB" + ], + "Extensions": { + "SupportedFileExtensions": [ + ".7Z", + ".VB", + ".ZIP" + ], + "UniqueFileExtensions": [ + ".VB" + ] + }, + "RetroPieDirectoryName": "vb", + "WebEmulator": { + "Type": "EmulatorJS", + "Core": "vb", + "AvailableWebEmulators": [ + { + "EmulatorType": "EmulatorJS", + "AvailableWebEmulatorCores": [ + { + "Core": "vb", + "AlternateCoreName": "beetle_vb", + "Default": true + } + ] + } + ] + }, + "Bios": [] + }, + { + "IGDBId": 415, + "IGDBName": "Watara/QuickShot Supervision", + "IGDBSlug": "watara-slash-quickshot-supervision", + "AlternateNames": [], + "Extensions": { + "SupportedFileExtensions": [], + "UniqueFileExtensions": [] + }, + "RetroPieDirectoryName": "", + "WebEmulator": { + "Type": "", + "Core": "", + "AvailableWebEmulators": null + }, + "Bios": [] + }, + { + "IGDBId": 82, + "IGDBName": "Web browser", + "IGDBSlug": "browser", + "AlternateNames": [ + "Internet" + ], + "Extensions": { + "SupportedFileExtensions": [], + "UniqueFileExtensions": [] + }, + "RetroPieDirectoryName": "", + "WebEmulator": { + "Type": "", + "Core": "", + "AvailableWebEmulators": null + }, + "Bios": [] + }, + { + "IGDBId": 5, + "IGDBName": "Wii", + "IGDBSlug": "wii", + "AlternateNames": [ + "Revolution" + ], + "Extensions": { + "SupportedFileExtensions": [], + "UniqueFileExtensions": [] + }, + "RetroPieDirectoryName": "", + "WebEmulator": { + "Type": "", + "Core": "", + "AvailableWebEmulators": null + }, + "Bios": [] + }, + { + "IGDBId": 41, + "IGDBName": "Wii U", + "IGDBSlug": "wiiu", + "AlternateNames": [ + "Project Cafe" + ], + "Extensions": { + "SupportedFileExtensions": [], + "UniqueFileExtensions": [] + }, + "RetroPieDirectoryName": "", + "WebEmulator": { + "Type": "", + "Core": "", + "AvailableWebEmulators": null + }, + "Bios": [] + }, + { + "IGDBId": 405, + "IGDBName": "Windows Mobile", + "IGDBSlug": "windows-mobile", + "AlternateNames": [ + "Pocket PC" + ], + "Extensions": { + "SupportedFileExtensions": [], + "UniqueFileExtensions": [] + }, + "RetroPieDirectoryName": "", + "WebEmulator": { + "Type": "", + "Core": "", + "AvailableWebEmulators": null + }, + "Bios": [] + }, + { + "IGDBId": 74, + "IGDBName": "Windows Phone", + "IGDBSlug": "winphone", + "AlternateNames": [ + "WP" + ], + "Extensions": { + "SupportedFileExtensions": [], + "UniqueFileExtensions": [] + }, + "RetroPieDirectoryName": "", + "WebEmulator": { + "Type": "", + "Core": "", + "AvailableWebEmulators": null + }, + "Bios": [] + }, + { + "IGDBId": 57, + "IGDBName": "WonderSwan", + "IGDBSlug": "wonderswan", + "AlternateNames": [ + "WS" + ], + "Extensions": { + "SupportedFileExtensions": [], + "UniqueFileExtensions": [] + }, + "RetroPieDirectoryName": "", + "WebEmulator": { + "Type": "", + "Core": "", + "AvailableWebEmulators": null + }, + "Bios": [] + }, + { + "IGDBId": 123, + "IGDBName": "WonderSwan Color", + "IGDBSlug": "wonderswan-color", + "AlternateNames": [ + "WSC" + ], + "Extensions": { + "SupportedFileExtensions": [], + "UniqueFileExtensions": [] + }, + "RetroPieDirectoryName": "", + "WebEmulator": { + "Type": "", + "Core": "", + "AvailableWebEmulators": null + }, + "Bios": [] + }, + { + "IGDBId": 11, + "IGDBName": "Xbox", + "IGDBSlug": "xbox", + "AlternateNames": [], + "Extensions": { + "SupportedFileExtensions": [], + "UniqueFileExtensions": [] + }, + "RetroPieDirectoryName": "", + "WebEmulator": { + "Type": "", + "Core": "", + "AvailableWebEmulators": null + }, + "Bios": [] + }, + { + "IGDBId": 12, + "IGDBName": "Xbox 360", + "IGDBSlug": "xbox360", + "AlternateNames": [ + "X360" + ], + "Extensions": { + "SupportedFileExtensions": [], + "UniqueFileExtensions": [] + }, + "RetroPieDirectoryName": "", + "WebEmulator": { + "Type": "", + "Core": "", + "AvailableWebEmulators": null + }, + "Bios": [] + }, + { + "IGDBId": 49, + "IGDBName": "Xbox One", + "IGDBSlug": "xboxone", + "AlternateNames": [ + "XONE" + ], + "Extensions": { + "SupportedFileExtensions": [], + "UniqueFileExtensions": [] + }, + "RetroPieDirectoryName": "", + "WebEmulator": { + "Type": "", + "Core": "", + "AvailableWebEmulators": null + }, + "Bios": [] + }, + { + "IGDBId": 169, + "IGDBName": "Xbox Series X|S", + "IGDBSlug": "series-x-s", + "AlternateNames": [ + "XSX" + ], + "Extensions": { + "SupportedFileExtensions": [], + "UniqueFileExtensions": [] + }, + "RetroPieDirectoryName": "", + "WebEmulator": { + "Type": "", + "Core": "", + "AvailableWebEmulators": null + }, + "Bios": [] + }, + { + "IGDBId": 240, + "IGDBName": "Zeebo", + "IGDBSlug": "zeebo", + "AlternateNames": [], + "Extensions": { + "SupportedFileExtensions": [], + "UniqueFileExtensions": [] + }, + "RetroPieDirectoryName": "", + "WebEmulator": { + "Type": "", + "Core": "", + "AvailableWebEmulators": null + }, + "Bios": [] + }, + { + "IGDBId": 26, + "IGDBName": "ZX Spectrum", + "IGDBSlug": "zxs", + "AlternateNames": [ + "Sinclair ZX", + "Sinclair ZX Spectrum", + "ZX Spectrum", + "zxs" + ], + "Extensions": { + "SupportedFileExtensions": [ + ".DSK", + ".GZ", + ".IMG", + ".MGT", + ".SCL", + ".SNA", + ".SZX", + ".TAP", + ".TRD", + ".TZX", + ".UDI", + ".Z80" + ], + "UniqueFileExtensions": [ + ".MGT", + ".SCL", + ".SNA", + ".SZX", + ".TRD", + ".TZX", + ".UDI", + ".Z80" + ] + }, + "RetroPieDirectoryName": "zxspectrum", + "WebEmulator": { + "Type": "", + "Core": "", + "AvailableWebEmulators": [] + }, + "Bios": [] + } ] \ No newline at end of file diff --git a/gaseous-server/gaseous-server.csproj b/gaseous-server/gaseous-server.csproj index dbf32fe..b4a8ce2 100644 --- a/gaseous-server/gaseous-server.csproj +++ b/gaseous-server/gaseous-server.csproj @@ -67,6 +67,7 @@ + @@ -114,5 +115,6 @@ + \ No newline at end of file diff --git a/gaseous-server/wwwroot/.DS_Store b/gaseous-server/wwwroot/.DS_Store deleted file mode 100644 index d579713..0000000 Binary files a/gaseous-server/wwwroot/.DS_Store and /dev/null differ diff --git a/gaseous-server/wwwroot/emulators/EmulatorJS.html b/gaseous-server/wwwroot/emulators/EmulatorJS.html index fa789b0..05644cb 100644 --- a/gaseous-server/wwwroot/emulators/EmulatorJS.html +++ b/gaseous-server/wwwroot/emulators/EmulatorJS.html @@ -22,9 +22,11 @@ if (StateUrl) { console.log('Loading saved state from: ' + StateUrl); EJS_loadStateURL = StateUrl; - EJS_startOnLoaded = true; } + // start the emulator automatically when loaded + EJS_startOnLoaded = true; + // Path to the data directory EJS_pathtodata = '/emulators/EmulatorJS/data/'; @@ -42,10 +44,11 @@ EJS_Buttons = { saveSavFiles: false, - loadSavFiles: false + loadSavFiles: false, + exitEmulation: false } - EJS_onSaveState = function(e) { + EJS_onSaveState = function (e) { var returnValue = { "ScreenshotByteArrayBase64": btoa(Uint8ToString(e.screenshot)), "StateByteArrayBase64": btoa(Uint8ToString(e.state)) @@ -72,8 +75,12 @@ returnValue = undefined; } - EJS_onLoadState = function(e) { - showDialog('emulatorloadstate', { "romId": romId, "IsMediaGroup": IsMediaGroup }); + EJS_onLoadState = function (e) { + let rompath = decodeURIComponent(getQueryString('rompath', 'string')); + rompath = rompath.substring(rompath.lastIndexOf('/') + 1); + console.log(rompath); + let stateManager = new EmulatorStateManager(romId, IsMediaGroup, getQueryString('engine', 'string'), getQueryString('core', 'string'), platformId, gameId, rompath); + stateManager.open(); } \ No newline at end of file diff --git a/gaseous-server/wwwroot/images/Critical.svg b/gaseous-server/wwwroot/images/Critical.svg new file mode 100644 index 0000000..6ff3c34 --- /dev/null +++ b/gaseous-server/wwwroot/images/Critical.svg @@ -0,0 +1,16 @@ + + + + stop-warning + + + + + + + + + + + + \ No newline at end of file diff --git a/gaseous-server/wwwroot/images/NoIntro-logo.svg b/gaseous-server/wwwroot/images/NoIntro-logo.svg new file mode 100644 index 0000000..adbd546 --- /dev/null +++ b/gaseous-server/wwwroot/images/NoIntro-logo.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/gaseous-server/wwwroot/images/Ratings/ESRB/EC.svg b/gaseous-server/wwwroot/images/Ratings/ESRB/EC.svg new file mode 100644 index 0000000..8c9947a --- /dev/null +++ b/gaseous-server/wwwroot/images/Ratings/ESRB/EC.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/gaseous-server/wwwroot/images/Warning.svg b/gaseous-server/wwwroot/images/Warning.svg new file mode 100644 index 0000000..8468b37 --- /dev/null +++ b/gaseous-server/wwwroot/images/Warning.svg @@ -0,0 +1,17 @@ + + + + warning + + + + + + + + + + + + + \ No newline at end of file diff --git a/gaseous-server/wwwroot/images/arrow-down.svg b/gaseous-server/wwwroot/images/arrow-down.svg new file mode 100644 index 0000000..f567c9a --- /dev/null +++ b/gaseous-server/wwwroot/images/arrow-down.svg @@ -0,0 +1,12 @@ + + + + + + + + + \ No newline at end of file diff --git a/gaseous-server/wwwroot/images/arrow-left.svg b/gaseous-server/wwwroot/images/arrow-left.svg new file mode 100644 index 0000000..7a94e00 --- /dev/null +++ b/gaseous-server/wwwroot/images/arrow-left.svg @@ -0,0 +1,12 @@ + + + + + + + + + \ No newline at end of file diff --git a/gaseous-server/wwwroot/images/arrow-right.svg b/gaseous-server/wwwroot/images/arrow-right.svg new file mode 100644 index 0000000..ae43266 --- /dev/null +++ b/gaseous-server/wwwroot/images/arrow-right.svg @@ -0,0 +1,12 @@ + + + + + + + + + \ No newline at end of file diff --git a/gaseous-server/wwwroot/images/arrow-up.svg b/gaseous-server/wwwroot/images/arrow-up.svg new file mode 100644 index 0000000..f382c8a --- /dev/null +++ b/gaseous-server/wwwroot/images/arrow-up.svg @@ -0,0 +1,11 @@ + + + + + + + + + \ No newline at end of file diff --git a/gaseous-server/wwwroot/images/bios.svg b/gaseous-server/wwwroot/images/bios.svg new file mode 100644 index 0000000..d9dd14a --- /dev/null +++ b/gaseous-server/wwwroot/images/bios.svg @@ -0,0 +1,69 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/gaseous-server/wwwroot/images/cross.svg b/gaseous-server/wwwroot/images/cross.svg new file mode 100644 index 0000000..15609cc --- /dev/null +++ b/gaseous-server/wwwroot/images/cross.svg @@ -0,0 +1,17 @@ + + + + + cross-circle + Created with Sketch Beta. + + + + + + + + + + + \ No newline at end of file diff --git a/gaseous-server/wwwroot/images/discord.svg b/gaseous-server/wwwroot/images/discord.svg new file mode 100644 index 0000000..024e083 --- /dev/null +++ b/gaseous-server/wwwroot/images/discord.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/gaseous-server/wwwroot/images/github-mark.svg b/gaseous-server/wwwroot/images/github-mark.svg new file mode 100644 index 0000000..37fa923 --- /dev/null +++ b/gaseous-server/wwwroot/images/github-mark.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/gaseous-server/wwwroot/images/hasheous.svg b/gaseous-server/wwwroot/images/hasheous.svg new file mode 100644 index 0000000..4d6be79 --- /dev/null +++ b/gaseous-server/wwwroot/images/hasheous.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/gaseous-server/wwwroot/images/home.svg b/gaseous-server/wwwroot/images/home.svg new file mode 100644 index 0000000..657b7af --- /dev/null +++ b/gaseous-server/wwwroot/images/home.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/gaseous-server/wwwroot/images/pending.svg b/gaseous-server/wwwroot/images/pending.svg new file mode 100644 index 0000000..19f04bb --- /dev/null +++ b/gaseous-server/wwwroot/images/pending.svg @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/gaseous-server/wwwroot/images/processing.svg b/gaseous-server/wwwroot/images/processing.svg new file mode 100644 index 0000000..1e11477 --- /dev/null +++ b/gaseous-server/wwwroot/images/processing.svg @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/gaseous-server/wwwroot/images/recent.svg b/gaseous-server/wwwroot/images/recent.svg new file mode 100644 index 0000000..4d090f3 --- /dev/null +++ b/gaseous-server/wwwroot/images/recent.svg @@ -0,0 +1,70 @@ + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + \ No newline at end of file diff --git a/gaseous-server/wwwroot/images/start-task.svg b/gaseous-server/wwwroot/images/start-task.svg new file mode 100644 index 0000000..45d8c6e --- /dev/null +++ b/gaseous-server/wwwroot/images/start-task.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/gaseous-server/wwwroot/images/tick.svg b/gaseous-server/wwwroot/images/tick.svg index 930e337..e74d3f2 100644 --- a/gaseous-server/wwwroot/images/tick.svg +++ b/gaseous-server/wwwroot/images/tick.svg @@ -1,5 +1,5 @@ - - - - + + + + \ No newline at end of file diff --git a/gaseous-server/wwwroot/images/viewicon.svg b/gaseous-server/wwwroot/images/viewicon.svg new file mode 100644 index 0000000..fe1e89b --- /dev/null +++ b/gaseous-server/wwwroot/images/viewicon.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/gaseous-server/wwwroot/images/viewinfinite.svg b/gaseous-server/wwwroot/images/viewinfinite.svg new file mode 100644 index 0000000..19ba8a8 --- /dev/null +++ b/gaseous-server/wwwroot/images/viewinfinite.svg @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/gaseous-server/wwwroot/images/viewlist.svg b/gaseous-server/wwwroot/images/viewlist.svg new file mode 100644 index 0000000..52825de --- /dev/null +++ b/gaseous-server/wwwroot/images/viewlist.svg @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/gaseous-server/wwwroot/images/viewpaged.svg b/gaseous-server/wwwroot/images/viewpaged.svg new file mode 100644 index 0000000..f4365f2 --- /dev/null +++ b/gaseous-server/wwwroot/images/viewpaged.svg @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/gaseous-server/wwwroot/index.html b/gaseous-server/wwwroot/index.html index 6b94d8d..08312b8 100644 --- a/gaseous-server/wwwroot/index.html +++ b/gaseous-server/wwwroot/index.html @@ -1,23 +1,9 @@ - + + - - - - - - - - - - - - - - - @@ -25,165 +11,138 @@ Gaseous Games + + +
+
+
- - - - - -
- -
- - - - - - + +
- - + \ No newline at end of file diff --git a/gaseous-server/wwwroot/pages/banner.html b/gaseous-server/wwwroot/pages/banner.html new file mode 100644 index 0000000..239ec53 --- /dev/null +++ b/gaseous-server/wwwroot/pages/banner.html @@ -0,0 +1,53 @@ + + + + + \ No newline at end of file diff --git a/gaseous-server/wwwroot/pages/banner.js b/gaseous-server/wwwroot/pages/banner.js new file mode 100644 index 0000000..fb81176 --- /dev/null +++ b/gaseous-server/wwwroot/pages/banner.js @@ -0,0 +1,103 @@ +function setupBanner() { + // attach event listeners to the banner elements + let userMenu = document.getElementById("banner_user"); + if (userMenu) { + userMenu.addEventListener('click', () => { + document.getElementById("myDropdown").classList.toggle("show"); + }); + } + + let userMenuLogoff = document.getElementById("banner_user_logoff"); + if (userMenuLogoff) { + userMenuLogoff.addEventListener('click', () => { + ajaxCall( + '/api/v1.1/Account/LogOff', + 'POST', + function (result) { + location.replace("/index.html"); + }, + function (error) { + location.replace("/index.html"); + } + ); + }); + } + + let bannerCog = document.getElementById("banner_cog"); + if (bannerCog) { + bannerCog.addEventListener('click', () => { + window.location.href = '/index.html?page=settings'; + }); + } + + let bannerUpload = document.getElementById("banner_upload"); + if (bannerUpload) { + bannerUpload.addEventListener('click', () => { + const uploadDialog = new UploadRom(); + uploadDialog.open(); + }); + } + + let bannerCollection = document.getElementById("banner_collections"); + if (bannerCollection) { + bannerCollection.addEventListener('click', () => { + window.location.href = '/index.html?page=collections'; + }); + } + + let bannerLibrary = document.getElementById("banner_library"); + if (bannerLibrary) { + bannerLibrary.addEventListener('click', () => { + window.location.href = '/index.html?page=library'; + }); + } + + let bannerHome = document.getElementById("banner_home"); + if (bannerHome) { + bannerHome.addEventListener('click', () => { + window.location.href = '/index.html?page=home'; + }); + } + + // set avatar + let avatarBox = document.getElementById('banner_user_image_box'); + let avatar = new Avatar(userProfile.profileId, 30, 30); + avatarBox.style = 'pointer-events: none;'; + avatar.setAttribute('style', 'margin-top: 5px; pointer-events: none; width: 30px; height: 30px;'); + avatarBox.appendChild(avatar); + + // set profile card in drop down + let profileCard = document.getElementById('banner_user_profilecard'); + let profileCardContent = new ProfileCard(userProfile.profileId, true); + profileCard.appendChild(profileCardContent); + + // hide the upload button if it's not permitted + let uploadButton = document.getElementById('banner_upload'); + if (!userProfile.roles.includes("Admin") && !userProfile.roles.includes("Gamer")) { + uploadButton.style.display = 'none'; + } + + // Close the dropdown menu if the user clicks outside of it + window.onclick = function (event) { + if (!event.target.matches('.dropbtn')) { + let dropdowns = document.getElementsByClassName("dropdown-content"); + for (let i = 0; i < dropdowns.length; i++) { + let openDropdown = dropdowns[i]; + if (openDropdown.classList.contains('show')) { + openDropdown.classList.remove('show'); + } + } + } + } + // event for preferences drop down item + document.getElementById('dropdown-menu-preferences').addEventListener('click', function () { + const prefsDialog = new PreferencesWindow(); + prefsDialog.open(); + }); + // event for account drop down item + document.getElementById('dropdown-menu-account').addEventListener('click', function () { + const accountDialog = new AccountWindow(); accountDialog.open(); + }); +} + +setupBanner(); \ No newline at end of file diff --git a/gaseous-server/wwwroot/pages/collections.html b/gaseous-server/wwwroot/pages/collections.html index ce3f057..a853b3e 100644 --- a/gaseous-server/wwwroot/pages/collections.html +++ b/gaseous-server/wwwroot/pages/collections.html @@ -1,4 +1,5 @@ -
+
@@ -12,88 +13,12 @@
-
- Wallpaper by andrea16 / Wallpaper Cave -
+ \ No newline at end of file diff --git a/gaseous-server/wwwroot/pages/collections.js b/gaseous-server/wwwroot/pages/collections.js new file mode 100644 index 0000000..6330c11 --- /dev/null +++ b/gaseous-server/wwwroot/pages/collections.js @@ -0,0 +1,85 @@ +function SetupPage() { + backgroundImageHandler = new BackgroundImageRotator(['/images/CollectionsWallpaper.jpg']); + + var newCollectionButton = document.getElementById('collection_new'); + if (userProfile.roles.includes("Admin") || userProfile.roles.includes("Gamer")) { + newCollectionButton.style.display = ''; + } else { + newCollectionButton.style.display = 'none'; + } + + GetCollections(); +} + +function GetCollections() { + ajaxCall('/api/v1.1/Collections', 'GET', function (result) { + if (result) { + var targetDiv = document.getElementById('collection_table_location'); + targetDiv.innerHTML = ''; + + var newTable = document.createElement('table'); + newTable.id = 'romtable'; + newTable.className = 'romtable'; + newTable.setAttribute('cellspacing', 0); + newTable.appendChild(createTableRow(true, ['Name', 'Description', 'Download Status', 'Size', ''])); + + for (var i = 0; i < result.length; i++) { + var statusText = result[i].buildStatus; + var downloadLink = ''; + var packageSize = '-'; + var inProgress = false; + switch (result[i].buildStatus) { + case 'NoStatus': + statusText = '-'; + break; + case "WaitingForBuild": + statusText = 'Build pending'; + inProgress = true; + break; + case "Building": + statusText = 'Building'; + inProgress = true; + break; + case "Completed": + statusText = 'Available'; + downloadLink = ''; + packageSize = formatBytes(result[i].collectionBuiltSizeBytes); + break; + case "Failed": + statusText = 'Build error'; + break; + default: + statusText = result[i].buildStatus; + break; + } + + if (inProgress == true) { + setTimeout(GetCollections, 10000); + } + + var editButton = ''; + var deleteButton = ''; + + if (userProfile.roles.includes("Admin") || userProfile.roles.includes("Gamer")) { + editButton = ''; + + deleteButton = ''; + } + + var newRow = [ + result[i].name, + result[i].description, + statusText, + packageSize, + '
' + downloadLink + editButton + deleteButton + '
' + ]; + + newTable.appendChild(createTableRow(false, newRow, 'romrow', 'romcell')); + } + + targetDiv.appendChild(newTable); + } + }); +} + +SetupPage(); \ No newline at end of file diff --git a/gaseous-server/wwwroot/pages/dialogs/collectionedit.html b/gaseous-server/wwwroot/pages/dialogs/collectionedit.html index 1dcd9cf..ad136d3 100644 --- a/gaseous-server/wwwroot/pages/dialogs/collectionedit.html +++ b/gaseous-server/wwwroot/pages/dialogs/collectionedit.html @@ -10,7 +10,8 @@ -
+
@@ -26,7 +27,8 @@ - + @@ -50,17 +52,24 @@ - + - + + @@ -74,7 +83,8 @@

Standard layout: /<IGDB Platform Slug>/<IGDB Game Slug>/Game ROMs

Example: /genesis-slash-megadrive/sonic-the-hedgehog/Sonic the Hedgehog.smd

-
Platforms
Player Perspectives +
Themes
Maximum size per platform (bytes)
Maximum collection size (bytes)
Directory Layout - + - @@ -118,15 +129,18 @@
-
+
- +
-
- +
+
-
+
@@ -151,9 +165,9 @@ url: '/api/v1.1/Filter', processResults: function (data) { var filter = data['platforms']; - + var arr = []; - + for (var i = 0; i < filter.length; i++) { arr.push({ id: filter[i].id, @@ -272,7 +286,7 @@ ajaxCall( '/api/v1.1/Collections/' + modalVariables, 'GET', - function(result) { + function (result) { if (result.name) { document.getElementById('collection_name').value = result.name; } if (result.description) { document.getElementById('collection_description').value = result.description; } if (result.minimumRating != -1) { document.getElementById('collection_userrating_min').value = result.minimumRating; } @@ -358,10 +372,10 @@ ajaxCall( '/api/v1.1/Collections/' + modalVariables, 'PATCH', - function(result) { + function (result) { location.reload(); }, - function(error) { + function (error) { alert(error); }, JSON.stringify(item) @@ -371,10 +385,10 @@ ajaxCall( '/api/v1.1/Collections', 'POST', - function(result) { + function (result) { location.reload(); }, - function(error) { + function (error) { alert(error); }, JSON.stringify(item) @@ -440,10 +454,10 @@ ajaxCall( '/api/v1.1/Collections/Preview', 'POST', - function(result) { + function (result) { DisplayPreview(result, 'collectionedit_previewbox_content'); }, - function(error) { + function (error) { console.log(JSON.stringify(error)); }, JSON.stringify(item) @@ -531,12 +545,12 @@ var gameItemRow = document.createElement('tr'); gameItemRow.className = bgalt; - + // game title var gameTitleCell = document.createElement('th'); gameTitleCell.setAttribute('colspan', 2); gameTitleCell.className = 'collections_preview_gametitlecell'; - + // game always include popup var gameTitleInclusion = document.createElement('select'); gameTitleInclusion.id = 'collections_preview_always_' + platformItem.id + '_' + gameItem.id; @@ -584,15 +598,15 @@ // game cover var gameDetailRow = document.createElement('tr'); gameDetailRow.className = bgalt; - + var gameCoverCell = document.createElement('td'); gameCoverCell.className = 'collections_preview_gamecovercell'; - + var gameImage = document.createElement('img'); gameImage.className = 'game_tile_image game_tile_image_small lazy'; gameImage.src = '/images/unknowngame.png'; if (gameItem.cover) { - gameImage.setAttribute('data-src', '/api/v1.1/Games/' + gameItem.id + '/cover/image/cover_small/' + gameItem.coverItem.imageId + '.jpg'); + gameImage.setAttribute('data-src', '/api/v1.1/Games/' + gameItem.id + '/cover/' + gameItem.cover.id + '/image/cover_small/' + gameItem.coverItem.imageId + '.jpg'); } else { gameImage.className = 'game_tile_image game_tile_image_small unknown'; } @@ -603,7 +617,7 @@ // game detail var gameDetailCell = document.createElement('td'); gameDetailCell.className = 'collections_preview_gamedetailcell'; - + // loop roms for (var r = 0; r < gameItem.roms.length; r++) { var romItem = gameItem.roms[r]; @@ -658,7 +672,7 @@ var labelToDisplay = ''; if (directoryLayoutMode) { - switch(directoryLayoutMode) { + switch (directoryLayoutMode) { case "Gaseous": // standard mode labelToDisplay = 'collection_directorylayout_gaseous_label'; @@ -669,7 +683,7 @@ break; } - + document.getElementById(labelToDisplay).style.display = ''; } } diff --git a/gaseous-server/wwwroot/pages/dialogs/emulatorloadstate.html b/gaseous-server/wwwroot/pages/dialogs/emulatorloadstate.html deleted file mode 100644 index 33ee3a5..0000000 --- a/gaseous-server/wwwroot/pages/dialogs/emulatorloadstate.html +++ /dev/null @@ -1,159 +0,0 @@ -
- -
- - \ No newline at end of file diff --git a/gaseous-server/wwwroot/pages/dialogs/librarydelete.html b/gaseous-server/wwwroot/pages/dialogs/librarydelete.html deleted file mode 100644 index 3e81f63..0000000 --- a/gaseous-server/wwwroot/pages/dialogs/librarydelete.html +++ /dev/null @@ -1,27 +0,0 @@ -

Are you sure you want to delete this library?

-

Warning: This cannot be undone!

-
-
- -
-
- -
-
- - \ No newline at end of file diff --git a/gaseous-server/wwwroot/pages/dialogs/librarynew.html b/gaseous-server/wwwroot/pages/dialogs/librarynew.html deleted file mode 100644 index 8525fce..0000000 --- a/gaseous-server/wwwroot/pages/dialogs/librarynew.html +++ /dev/null @@ -1,87 +0,0 @@ - - - - - - - - - - - - - -
Name
Default Platform
Path
- -
-
- -
-
- -
-
- - \ No newline at end of file diff --git a/gaseous-server/wwwroot/pages/dialogs/mediagroupdelete.html b/gaseous-server/wwwroot/pages/dialogs/mediagroupdelete.html deleted file mode 100644 index d61d073..0000000 --- a/gaseous-server/wwwroot/pages/dialogs/mediagroupdelete.html +++ /dev/null @@ -1,29 +0,0 @@ -

Are you sure you want to delete this media group and all associated saved states?

-

Warning: This cannot be undone!

-
-
- -
-
- -
-
- - \ No newline at end of file diff --git a/gaseous-server/wwwroot/pages/dialogs/platformmapedit.html b/gaseous-server/wwwroot/pages/dialogs/platformmapedit.html deleted file mode 100644 index 14d1f64..0000000 --- a/gaseous-server/wwwroot/pages/dialogs/platformmapedit.html +++ /dev/null @@ -1,391 +0,0 @@ -
-
-

Title Matching

- - - - - - - - - -
-

Alternative Names

-
- -
-

Supported File Extensions

-
- -
- -

Collections

- - - - - - - - - - - - -
-

Standard Directory Naming

-
- -
Note: Standard directory naming uses the IGDB slug for the platform and is not editable.
-

RetroPie Directory Naming

-
- -
- -

Web Emulator

- - - - - - - - - - - - - - - - - -
-

Web Emulator

-
- -
-

Engine

-
- -
-

Core

-
- -
- - - -
- -

BIOS/Firmware

-
-
-
- -
- -
- - - - \ No newline at end of file diff --git a/gaseous-server/wwwroot/pages/dialogs/romdelete.html b/gaseous-server/wwwroot/pages/dialogs/romdelete.html deleted file mode 100644 index 62af3b3..0000000 --- a/gaseous-server/wwwroot/pages/dialogs/romdelete.html +++ /dev/null @@ -1,18 +0,0 @@ -

Are you sure you want to delete this ROM and all associated saved states?

-

Warning: This cannot be undone!

-
-
- -
-
- -
-
- - \ No newline at end of file diff --git a/gaseous-server/wwwroot/pages/dialogs/rominfo.html b/gaseous-server/wwwroot/pages/dialogs/rominfo.html deleted file mode 100644 index 3e848b9..0000000 --- a/gaseous-server/wwwroot/pages/dialogs/rominfo.html +++ /dev/null @@ -1,396 +0,0 @@ -
-
General
- - -
Title Match
- -
-
- - - - - - - - - -
- - \ No newline at end of file diff --git a/gaseous-server/wwwroot/pages/dialogs/romsdelete.html b/gaseous-server/wwwroot/pages/dialogs/romsdelete.html deleted file mode 100644 index 4e2ab32..0000000 --- a/gaseous-server/wwwroot/pages/dialogs/romsdelete.html +++ /dev/null @@ -1,10 +0,0 @@ -

Are you sure you want to delete the selected ROMs and any associated saved states?

-

Warning: This cannot be undone!

-
-
- -
-
- -
-
\ No newline at end of file diff --git a/gaseous-server/wwwroot/pages/dialogs/settingsuserdelete.html b/gaseous-server/wwwroot/pages/dialogs/settingsuserdelete.html deleted file mode 100644 index 4241261..0000000 --- a/gaseous-server/wwwroot/pages/dialogs/settingsuserdelete.html +++ /dev/null @@ -1,29 +0,0 @@ -

Are you sure you want to delete the selected user?

-

Warning: This cannot be undone!

-
-
- -
-
- -
-
- - \ No newline at end of file diff --git a/gaseous-server/wwwroot/pages/dialogs/settingsuseredit.html b/gaseous-server/wwwroot/pages/dialogs/settingsuseredit.html deleted file mode 100644 index 401335c..0000000 --- a/gaseous-server/wwwroot/pages/dialogs/settingsuseredit.html +++ /dev/null @@ -1,392 +0,0 @@ -
-
Password
-
Role
-
Content Restrictions
- -
- -
- - - - - -
-
- -
- - \ No newline at end of file diff --git a/gaseous-server/wwwroot/pages/dialogs/settingsusernew.html b/gaseous-server/wwwroot/pages/dialogs/settingsusernew.html deleted file mode 100644 index 1c04efa..0000000 --- a/gaseous-server/wwwroot/pages/dialogs/settingsusernew.html +++ /dev/null @@ -1,104 +0,0 @@ -

New User

- - - - - - - - - - - - - - - - - - - - - - - -
- Email - - -
- Password - - -
- Confirm password - - -
- -
- - \ No newline at end of file diff --git a/gaseous-server/wwwroot/pages/dialogs/upload.html b/gaseous-server/wwwroot/pages/dialogs/upload.html deleted file mode 100644 index 3eef481..0000000 --- a/gaseous-server/wwwroot/pages/dialogs/upload.html +++ /dev/null @@ -1,130 +0,0 @@ - - - -
-
-
- - - - - -
- Override automatic platform detection: - - -
- - \ No newline at end of file diff --git a/gaseous-server/wwwroot/pages/dialogs/userprofile.html b/gaseous-server/wwwroot/pages/dialogs/userprofile.html deleted file mode 100644 index 8196332..0000000 --- a/gaseous-server/wwwroot/pages/dialogs/userprofile.html +++ /dev/null @@ -1,449 +0,0 @@ -
-
Preferences
-
Avatar
-
Account
-
-
- - - -
- - diff --git a/gaseous-server/wwwroot/pages/emulator.html b/gaseous-server/wwwroot/pages/emulator.html index fd1f62d..f81d479 100644 --- a/gaseous-server/wwwroot/pages/emulator.html +++ b/gaseous-server/wwwroot/pages/emulator.html @@ -5,101 +5,6 @@
+ // setup the page + SetupPage(); + \ No newline at end of file diff --git a/gaseous-server/wwwroot/pages/emulator.js b/gaseous-server/wwwroot/pages/emulator.js new file mode 100644 index 0000000..a87e672 --- /dev/null +++ b/gaseous-server/wwwroot/pages/emulator.js @@ -0,0 +1,91 @@ +var gameId = getQueryString('gameid', 'int'); +var romId = getQueryString('romid', 'int'); +var platformId = getQueryString('platformid', 'int'); +var IsMediaGroupInt = getQueryString('mediagroup', 'int'); +var IsMediaGroup = false; + +var StateUrl = undefined; + +var gameData; +var artworks = null; +var artworksPosition = 0; + +var emuGameTitle = ''; +var emuBios = ''; +var emuBackground = ''; + +// statistics +var SessionId = undefined; + +function SetupPage() { + if (IsMediaGroupInt == 1) { IsMediaGroup = true; } + if (getQueryString('stateid', 'int')) { + StateUrl = '/api/v1.1/StateManager/' + romId + '/' + getQueryString('stateid', 'int') + '/State/savestate.state?StateOnly=true&IsMediaGroup=' + IsMediaGroup; + } + + console.log("Loading rom url: " + decodeURIComponent(getQueryString('rompath', 'string'))); + + ajaxCall('/api/v1.1/Games/' + gameId, 'GET', function (result) { + gameData = result; + + if (result.cover) { + emuBackground = '/api/v1.1/Games/' + gameId + '/cover/' + result.cover.id + '/image/original/' + result.cover.imageId + '.jpg'; + } + + emuGameTitle = gameData.name; + }); + + ajaxCall('/api/v1.1/Bios/' + platformId, 'GET', function (result) { + if (result.length == 0) { + emuBios = ''; + } else { + emuBios = '/api/v1.1/Bios/zip/' + platformId + '/' + gameId + '?filtered=true'; + console.log("Using BIOS link: " + emuBios); + } + + switch (getQueryString('engine', 'string')) { + case 'EmulatorJS': + console.log("Emulator: " + getQueryString('engine', 'string')); + console.log("Core: " + getQueryString('core', 'string')); + + $('#emulator').load('/emulators/EmulatorJS.html?v=' + AppVersion); + break; + } + }); + + setInterval(SaveStatistics, 60000); +} + +function rotateBackground() { + if (artworks) { + artworksPosition += 1; + if (artworks[artworksPosition] == null) { + artworksPosition = 0; + } + var bg = document.getElementById('bgImage'); + bg.setAttribute('style', 'background-image: url("/api/v1.1/Games/' + gameId + '/artwork/' + artworks[artworksPosition] + '/image/original/' + artworks[artworksPosition] + '.jpg"); background-position: center; background-repeat: no-repeat; background-size: cover; filter: blur(10px); -webkit-filter: blur(10px);'); + } +} + +function SaveStatistics() { + var model; + if (SessionId == undefined) { + ajaxCall( + '/api/v1.1/Statistics/Games/' + gameId + '/' + platformId + '/' + romId + '?IsMediaGroup=' + IsMediaGroup, + 'POST', + function (success) { + SessionId = success.sessionId; + } + ); + } else { + ajaxCall( + '/api/v1.1/Statistics/Games/' + gameId + '/' + platformId + '/' + romId + '/' + SessionId + '?IsMediaGroup=' + IsMediaGroup, + 'PUT', + function (success) { + + } + ); + } +} + +SetupPage(); \ No newline at end of file diff --git a/gaseous-server/wwwroot/pages/first.html b/gaseous-server/wwwroot/pages/first.html index 3e44bbd..e27d6ff 100644 --- a/gaseous-server/wwwroot/pages/first.html +++ b/gaseous-server/wwwroot/pages/first.html @@ -1,176 +1,107 @@ - - - - - - - - - - - - - - - - - - - - - - - Gaseous Games + +