├── .dockerignore
├── .env.sample
├── .github
└── workflows
│ └── codeql-analysis.yml
├── .gitignore
├── .gitmodules
├── .rubocop.yml
├── Dockerfile
├── Gemfile
├── Gemfile.lock
├── LICENSE
├── Procfile
├── README.md
├── Rakefile
├── base.json
├── bg_gen
├── background-base-extra.png
├── background-base.png
├── bottom_mask.png
├── dot-tile.png
├── dot-tilea.png
├── dot.png
├── dot.rb
├── main.rb
├── mask-white.png
├── mask.hlsl
├── mask.png
├── minus-mask-extra.png
├── minus-mask.png
├── orig.jpg
├── side-mask.png
└── sub.hlsl
├── config.ru
├── data
├── effects
│ └── frpt-pjsekai.fixed.json
├── engines
│ └── frpt-pjsekai.extended.json
├── particles
│ └── frpt-pjsekai.json
└── skins
│ └── frpt-pjsekai.extended.json
├── dist
├── bg
│ └── .gitkeep
├── bgm
│ └── .gitkeep
├── conv
│ └── .gitkeep
├── data-overrides
│ └── .gitkeep
├── modify
│ └── .gitkeep
└── skin
│ └── .gitkeep
├── docker-compose.yml
├── effect
├── audio
│ ├── flick.mp3
│ ├── flick_critical.mp3
│ ├── good.mp3
│ ├── great.mp3
│ ├── long.mp3
│ ├── long_critical.mp3
│ ├── perfect.mp3
│ ├── perfect_critical.mp3
│ ├── tap.mp3
│ ├── tick.mp3
│ ├── tick_critical.mp3
│ ├── trace.mp3
│ ├── trace_critical.mp3
│ └── trace_flick.mp3
└── clips.yml
├── falcon.rb
├── info.json
├── info_down.json
├── info_official.json
├── info_old.json
├── info_system.json
├── info_test.json
├── launch.bat
├── main.rb
├── overrides
└── .gitkeep
├── public
└── repo
│ ├── EffectAudio.zip
│ ├── EffectData.gz
│ ├── background-base-extra.png
│ ├── background-base.png
│ ├── banner.png
│ ├── config.gz
│ ├── data.gz
│ ├── delete.png
│ ├── extra_off.png
│ ├── extra_on.png
│ ├── flick_off.png
│ ├── flick_on.png
│ ├── folder.png
│ ├── sounds
│ ├── connect.mp3
│ ├── trace.mp3
│ ├── trace_crit.mp3
│ └── trace_flick.mp3
│ └── welcome.mp3
├── setup.bat
├── skin
├── data.json
└── texture.png
└── views
├── index.erb
├── level.erb
└── level_404.erb
/.dockerignore:
--------------------------------------------------------------------------------
1 | bg_server
2 | dist/*/*
3 | bg_gen_python
--------------------------------------------------------------------------------
/.env.sample:
--------------------------------------------------------------------------------
1 | RUBY_PORT=4567
2 | PUBLIC=false
--------------------------------------------------------------------------------
/.github/workflows/codeql-analysis.yml:
--------------------------------------------------------------------------------
1 | # For most projects, this workflow file will not need changing; you simply need
2 | # to commit it to your repository.
3 | #
4 | # You may wish to alter this file to override the set of languages analyzed,
5 | # or to provide custom queries or build logic.
6 | #
7 | # ******** NOTE ********
8 | # We have attempted to detect the languages in your repository. Please check
9 | # the `language` matrix defined below to confirm you have the correct set of
10 | # supported CodeQL languages.
11 | #
12 | name: "CodeQL"
13 |
14 | on:
15 | push:
16 | branches: [ main ]
17 | pull_request:
18 | # The branches below must be a subset of the branches above
19 | branches: [ main ]
20 | schedule:
21 | - cron: '20 22 * * 0'
22 |
23 | jobs:
24 | analyze:
25 | name: Analyze
26 | runs-on: ubuntu-latest
27 | permissions:
28 | actions: read
29 | contents: read
30 | security-events: write
31 |
32 | strategy:
33 | fail-fast: false
34 | matrix:
35 | language: [ 'ruby' ]
36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
37 | # Learn more about CodeQL language support at https://git.io/codeql-language-support
38 |
39 | steps:
40 | - name: Checkout repository
41 | uses: actions/checkout@v3
42 |
43 | # Initializes the CodeQL tools for scanning.
44 | - name: Initialize CodeQL
45 | uses: github/codeql-action/init@v2
46 | with:
47 | languages: ${{ matrix.language }}
48 | # If you wish to specify custom queries, you can do so here or in a config file.
49 | # By default, queries listed here will override any specified in a config file.
50 | # Prefix the list here with "+" to use these queries and those in the config file.
51 | # queries: ./path/to/local/query, your-org/your-repo/queries@main
52 |
53 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
54 | # If this step fails, then you should remove it and run the build manually (see below)
55 | - name: Autobuild
56 | uses: github/codeql-action/autobuild@v2
57 |
58 | # ℹ️ Command-line programs to run using the OS shell.
59 | # 📚 https://git.io/JvXDl
60 |
61 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
62 | # and modify them (or add more) to build your code if your project
63 | # uses a compiled language
64 |
65 | #- run: |
66 | # make bootstrap
67 | # make release
68 |
69 | - name: Perform CodeQL Analysis
70 | uses: github/codeql-action/analyze@v2
71 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 |
2 | # Created by https://www.toptal.com/developers/gitignore/api/ruby,python
3 | # Edit at https://www.toptal.com/developers/gitignore?templates=ruby,python
4 |
5 | ### Python ###
6 | # Byte-compiled / optimized / DLL files
7 | __pycache__/
8 | *.py[cod]
9 | *$py.class
10 |
11 | # C extensions
12 | *.so
13 |
14 | # Distribution / packaging
15 | .Python
16 | build/
17 | develop-eggs/
18 | # dist/
19 | downloads/
20 | eggs/
21 | .eggs/
22 | lib/
23 | lib64/
24 | parts/
25 | sdist/
26 | var/
27 | wheels/
28 | share/python-wheels/
29 | *.egg-info/
30 | .installed.cfg
31 | *.egg
32 | MANIFEST
33 |
34 | # PyInstaller
35 | # Usually these files are written by a python script from a template
36 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
37 | *.manifest
38 | *.spec
39 |
40 | # Installer logs
41 | pip-log.txt
42 | pip-delete-this-directory.txt
43 |
44 | # Unit test / coverage reports
45 | htmlcov/
46 | .tox/
47 | .nox/
48 | .coverage
49 | .coverage.*
50 | .cache
51 | nosetests.xml
52 | coverage.xml
53 | *.cover
54 | *.py,cover
55 | .hypothesis/
56 | .pytest_cache/
57 | cover/
58 |
59 | # Translations
60 | *.mo
61 | *.pot
62 |
63 | # Django stuff:
64 | *.log
65 | local_settings.py
66 | db.sqlite3
67 | db.sqlite3-journal
68 |
69 | # Flask stuff:
70 | instance/
71 | .webassets-cache
72 |
73 | # Scrapy stuff:
74 | .scrapy
75 |
76 | # Sphinx documentation
77 | docs/_build/
78 |
79 | # PyBuilder
80 | .pybuilder/
81 | target/
82 |
83 | # Jupyter Notebook
84 | .ipynb_checkpoints
85 |
86 | # IPython
87 | profile_default/
88 | ipython_config.py
89 |
90 | # pyenv
91 | # For a library or package, you might want to ignore these files since the code is
92 | # intended to run in multiple environments; otherwise, check them in:
93 | # .python-version
94 |
95 | # pipenv
96 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
97 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
98 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
99 | # install all needed dependencies.
100 | #Pipfile.lock
101 |
102 | # poetry
103 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
104 | # This is especially recommended for binary packages to ensure reproducibility, and is more
105 | # commonly ignored for libraries.
106 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
107 | #poetry.lock
108 |
109 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow
110 | __pypackages__/
111 |
112 | # Celery stuff
113 | celerybeat-schedule
114 | celerybeat.pid
115 |
116 | # SageMath parsed files
117 | *.sage.py
118 |
119 | # Environments
120 | .env
121 | .venv
122 | env/
123 | venv/
124 | ENV/
125 | env.bak/
126 | venv.bak/
127 |
128 | # Spyder project settings
129 | .spyderproject
130 | .spyproject
131 |
132 | # Rope project settings
133 | .ropeproject
134 |
135 | # mkdocs documentation
136 | /site
137 |
138 | # mypy
139 | .mypy_cache/
140 | .dmypy.json
141 | dmypy.json
142 |
143 | # Pyre type checker
144 | .pyre/
145 |
146 | # pytype static type analyzer
147 | .pytype/
148 |
149 | # Cython debug symbols
150 | cython_debug/
151 |
152 | # PyCharm
153 | # JetBrains specific template is maintainted in a separate JetBrains.gitignore that can
154 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
155 | # and can be added to the global gitignore or merged into this file. For a more nuclear
156 | # option (not recommended) you can uncomment the following to ignore the entire idea folder.
157 | #.idea/
158 |
159 | ### Ruby ###
160 | *.gem
161 | *.rbc
162 | /.config
163 | /coverage/
164 | /InstalledFiles
165 | /pkg/
166 | /spec/reports/
167 | /spec/examples.txt
168 | /test/tmp/
169 | /test/version_tmp/
170 | /tmp/
171 |
172 | # Used by dotenv library to load environment variables.
173 | # .env
174 |
175 | # Ignore Byebug command history file.
176 | .byebug_history
177 |
178 | ## Specific to RubyMotion:
179 | .dat*
180 | .repl_history
181 | *.bridgesupport
182 | build-iPhoneOS/
183 | build-iPhoneSimulator/
184 |
185 | ## Specific to RubyMotion (use of CocoaPods):
186 | #
187 | # We recommend against adding the Pods directory to your .gitignore. However
188 | # you should judge for yourself, the pros and cons are mentioned at:
189 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
190 | # vendor/Pods/
191 |
192 | ## Documentation cache and generated files:
193 | /.yardoc/
194 | /_yardoc/
195 | /doc/
196 | /rdoc/
197 |
198 | ## Environment normalization:
199 | /.bundle/
200 | /vendor/bundle
201 | /lib/bundler/man/
202 |
203 | # for a library or gem, you might want to ignore these files since the code is
204 | # intended to run in multiple environments; otherwise, check them in:
205 | # Gemfile.lock
206 | # .ruby-version
207 | # .ruby-gemset
208 |
209 | # unless supporting rvm < 1.11.0 or doing something fancy, ignore this:
210 | .rvmrc
211 |
212 | # Used by RuboCop. Remote config files pulled in from inherit_from directive.
213 | # .rubocop-https?--*
214 |
215 | # End of https://www.toptal.com/developers/gitignore/api/ruby,python
216 |
217 | # Some generated files
218 | dist/**/*.*
219 | !dist/**/.gitkeep
220 | levels/*
221 | !levels/.gitkeep
222 | level_datas/*
223 | !level_datas/.gitkeep
224 | overrides/*
225 | !overrides/.gitkeep
226 |
227 | *.psd
228 | config.yml
229 | blocked_authors.txt
230 |
--------------------------------------------------------------------------------
/.gitmodules:
--------------------------------------------------------------------------------
1 | [submodule "bg_gen_python"]
2 | path = bg_gen_python
3 | url = https://github.com/sevenc-nanashi/SP-imgserver
4 |
--------------------------------------------------------------------------------
/.rubocop.yml:
--------------------------------------------------------------------------------
1 | inherit_gem:
2 | syntax_tree: config/rubocop.yml
3 |
4 | AllCops:
5 | NewCops: enable
6 | TargetRubyVersion: 3.0
7 |
8 | Style/Documentation:
9 | Enabled: false
10 |
11 | Lint/ScriptPermission:
12 | Enabled: false
13 |
14 | Metrics:
15 | Enabled: false
16 |
17 | Style/GuardClause:
18 | Enabled: false
19 |
20 | Naming/MethodParameterName:
21 | Enabled: false
22 |
23 | Lint/AssignmentInCondition:
24 | Enabled: false
25 |
26 | Style/RedundantSelf:
27 | Enabled: false
28 |
29 | Style/GlobalVars:
30 | Enabled: false
31 |
32 | Naming/VariableNumber:
33 | Enabled: false
34 |
35 | Style/IfUnlessModifier:
36 | Enabled: false
37 |
38 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | # == Engine ==================================================================
2 | FROM node:latest AS build
3 | RUN apt-get update -y && apt-get install -y \
4 | git tzdata && \
5 | apt-get clean && \
6 | rm -rf /var/lib/apt/lists/*
7 | ENV TZ=Asia/Tokyo
8 | WORKDIR /
9 |
10 | # -- Installations -----------------------------------------------------------
11 | ADD http://www.random.org/strings/?num=10&len=8&digits=on&upperalpha=on&loweralpha=on&unique=on&format=plain&rnd=new uuid
12 | RUN git clone https://github.com/sevenc-nanashi/sonolus-pjsekai-engine-extended.git engine
13 |
14 | # -- Compile -----------------------------------------------------------------
15 | RUN cd engine && git checkout cc5ab1d && npm install && npm run build
16 |
17 | # == Server ==================================================================
18 | FROM ruby:3.1
19 | RUN apt-get update -y && apt-get install -y \
20 | ffmpeg
21 | WORKDIR /root
22 |
23 | # -- Installations -----------------------------------------------------------
24 | COPY Gemfile .
25 | COPY Gemfile.lock .
26 | RUN bundle config with production; \
27 | bundle install
28 |
29 | # -- Startup -----------------------------------------------------------------
30 | COPY --from=build /engine/dist/EngineData engine/dist/EngineData
31 | COPY --from=build /engine/dist/EngineConfiguration engine/dist/EngineConfiguration
32 | COPY . .
33 | ENV RUBYOPTS=--jit
34 | ENV RACK_ENV=production
35 | EXPOSE 4567
36 | CMD ["/bin/sh", "-c", "bundle exec falcon host"]
37 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | source "https://rubygems.org"
4 |
5 | git_source(:github) { |repo_name| "https://github.com/#{repo_name}" }
6 |
7 | # gem "rails"
8 |
9 | gem "sinatra", "~> 2.1"
10 |
11 | gem "sinatra-reloader", "~> 1.0"
12 |
13 | gem "rack", "~> 2.2"
14 |
15 | gem "http", "~> 5.0.4"
16 |
17 | gem "dxruby", "~> 1.4"
18 |
19 | ruby ">= 3.0.3"
20 |
21 | gem "sinatra-contrib", "~> 2.1"
22 |
23 | gem "puma", "~> 5.6"
24 |
25 | gem "rubocop", "~> 1.26.1"
26 |
27 | gem "falcon", "~> 0.39.2"
28 | gem "unf_ext", "~> 0.0.8.2"
29 |
30 | gem "syntax_tree", "~> 3.6"
31 |
32 | gem "rubyzip", "~> 2.3"
33 |
--------------------------------------------------------------------------------
/Gemfile.lock:
--------------------------------------------------------------------------------
1 | GEM
2 | remote: https://rubygems.org/
3 | specs:
4 | addressable (2.8.1)
5 | public_suffix (>= 2.0.2, < 6.0)
6 | ast (2.4.2)
7 | async (2.3.0)
8 | console (~> 1.10)
9 | io-event (~> 1.1)
10 | timers (~> 4.1)
11 | async-container (0.16.12)
12 | async
13 | async-io
14 | async-http (0.56.6)
15 | async (>= 1.25)
16 | async-io (>= 1.28)
17 | async-pool (>= 0.2)
18 | protocol-http (~> 0.22.0)
19 | protocol-http1 (~> 0.14.0)
20 | protocol-http2 (~> 0.14.0)
21 | traces (~> 0.4.0)
22 | async-http-cache (0.4.3)
23 | async-http (~> 0.56)
24 | async-io (1.34.0)
25 | async
26 | async-pool (0.3.12)
27 | async (>= 1.25)
28 | build-environment (1.13.0)
29 | console (1.16.2)
30 | fiber-local
31 | domain_name (0.5.20190701)
32 | unf (>= 0.0.5, < 1.0.0)
33 | dxruby (1.4.7)
34 | falcon (0.39.2)
35 | async
36 | async-container (~> 0.16.0)
37 | async-http (~> 0.56.0)
38 | async-http-cache (~> 0.4.0)
39 | async-io (~> 1.22)
40 | build-environment (~> 1.13)
41 | bundler
42 | localhost (~> 1.1)
43 | process-metrics (~> 0.2.0)
44 | rack (>= 1.0)
45 | samovar (~> 2.1)
46 | ffi (1.15.5)
47 | ffi (1.15.5-x64-mingw-ucrt)
48 | ffi (1.15.5-x64-mingw32)
49 | ffi (1.15.5-x86-mingw32)
50 | ffi-compiler (1.0.1)
51 | ffi (>= 1.0.0)
52 | rake
53 | fiber-local (1.0.0)
54 | http (5.0.4)
55 | addressable (~> 2.8)
56 | http-cookie (~> 1.0)
57 | http-form_data (~> 2.2)
58 | llhttp-ffi (~> 0.4.0)
59 | http-cookie (1.0.5)
60 | domain_name (~> 0.5)
61 | http-form_data (2.3.0)
62 | io-event (1.1.4)
63 | llhttp-ffi (0.4.0)
64 | ffi-compiler (~> 1.0)
65 | rake (~> 13.0)
66 | localhost (1.1.9)
67 | mapping (1.1.1)
68 | multi_json (1.15.0)
69 | mustermann (2.0.2)
70 | ruby2_keywords (~> 0.0.1)
71 | nio4r (2.5.8)
72 | parallel (1.22.1)
73 | parser (3.1.3.0)
74 | ast (~> 2.4.1)
75 | prettier_print (1.1.0)
76 | process-metrics (0.2.1)
77 | console (~> 1.8)
78 | samovar (~> 2.1)
79 | protocol-hpack (1.4.2)
80 | protocol-http (0.22.9)
81 | protocol-http1 (0.14.6)
82 | protocol-http (~> 0.22)
83 | protocol-http2 (0.14.2)
84 | protocol-hpack (~> 1.4)
85 | protocol-http (~> 0.18)
86 | public_suffix (5.0.1)
87 | puma (5.6.5)
88 | nio4r (~> 2.0)
89 | rack (2.2.4)
90 | rack-protection (2.2.3)
91 | rack
92 | rainbow (3.1.1)
93 | rake (13.0.6)
94 | regexp_parser (2.6.1)
95 | rexml (3.2.5)
96 | rubocop (1.26.1)
97 | parallel (~> 1.10)
98 | parser (>= 3.1.0.0)
99 | rainbow (>= 2.2.2, < 4.0)
100 | regexp_parser (>= 1.8, < 3.0)
101 | rexml
102 | rubocop-ast (>= 1.16.0, < 2.0)
103 | ruby-progressbar (~> 1.7)
104 | unicode-display_width (>= 1.4.0, < 3.0)
105 | rubocop-ast (1.24.0)
106 | parser (>= 3.1.1.0)
107 | ruby-progressbar (1.11.0)
108 | ruby2_keywords (0.0.5)
109 | rubyzip (2.3.2)
110 | samovar (2.1.4)
111 | console (~> 1.0)
112 | mapping (~> 1.0)
113 | sinatra (2.2.3)
114 | mustermann (~> 2.0)
115 | rack (~> 2.2)
116 | rack-protection (= 2.2.3)
117 | tilt (~> 2.0)
118 | sinatra-contrib (2.2.3)
119 | multi_json
120 | mustermann (~> 2.0)
121 | rack-protection (= 2.2.3)
122 | sinatra (= 2.2.3)
123 | tilt (~> 2.0)
124 | sinatra-reloader (1.0)
125 | sinatra-contrib
126 | syntax_tree (3.6.3)
127 | prettier_print
128 | tilt (2.0.11)
129 | timers (4.3.5)
130 | traces (0.4.1)
131 | unf (0.1.4)
132 | unf_ext
133 | unf_ext (0.0.8.2)
134 | unicode-display_width (2.3.0)
135 |
136 | PLATFORMS
137 | x64-mingw-ucrt
138 | x64-mingw32
139 | x86-mingw32
140 | x86_64-linux
141 |
142 | DEPENDENCIES
143 | dxruby (~> 1.4)
144 | falcon (~> 0.39.2)
145 | http (~> 5.0.4)
146 | puma (~> 5.6)
147 | rack (~> 2.2)
148 | rubocop (~> 1.26.1)
149 | rubyzip (~> 2.3)
150 | sinatra (~> 2.1)
151 | sinatra-contrib (~> 2.1)
152 | sinatra-reloader (~> 1.0)
153 | syntax_tree (~> 3.6)
154 | unf_ext (~> 0.0.8.2)
155 |
156 | RUBY VERSION
157 | ruby 3.0.3p157
158 |
159 | BUNDLED WITH
160 | 2.2.32
161 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | GNU GENERAL PUBLIC LICENSE
2 | Version 3, 29 June 2007
3 |
4 | Copyright (C) 2007 Free Software Foundation, Inc.
5 | Everyone is permitted to copy and distribute verbatim copies
6 | of this license document, but changing it is not allowed.
7 |
8 | Preamble
9 |
10 | The GNU General Public License is a free, copyleft license for
11 | software and other kinds of works.
12 |
13 | The licenses for most software and other practical works are designed
14 | to take away your freedom to share and change the works. By contrast,
15 | the GNU General Public License is intended to guarantee your freedom to
16 | share and change all versions of a program--to make sure it remains free
17 | software for all its users. We, the Free Software Foundation, use the
18 | GNU General Public License for most of our software; it applies also to
19 | any other work released this way by its authors. You can apply it to
20 | your programs, too.
21 |
22 | When we speak of free software, we are referring to freedom, not
23 | price. Our General Public Licenses are designed to make sure that you
24 | have the freedom to distribute copies of free software (and charge for
25 | them if you wish), that you receive source code or can get it if you
26 | want it, that you can change the software or use pieces of it in new
27 | free programs, and that you know you can do these things.
28 |
29 | To protect your rights, we need to prevent others from denying you
30 | these rights or asking you to surrender the rights. Therefore, you have
31 | certain responsibilities if you distribute copies of the software, or if
32 | you modify it: responsibilities to respect the freedom of others.
33 |
34 | For example, if you distribute copies of such a program, whether
35 | gratis or for a fee, you must pass on to the recipients the same
36 | freedoms that you received. You must make sure that they, too, receive
37 | or can get the source code. And you must show them these terms so they
38 | know their rights.
39 |
40 | Developers that use the GNU GPL protect your rights with two steps:
41 | (1) assert copyright on the software, and (2) offer you this License
42 | giving you legal permission to copy, distribute and/or modify it.
43 |
44 | For the developers' and authors' protection, the GPL clearly explains
45 | that there is no warranty for this free software. For both users' and
46 | authors' sake, the GPL requires that modified versions be marked as
47 | changed, so that their problems will not be attributed erroneously to
48 | authors of previous versions.
49 |
50 | Some devices are designed to deny users access to install or run
51 | modified versions of the software inside them, although the manufacturer
52 | can do so. This is fundamentally incompatible with the aim of
53 | protecting users' freedom to change the software. The systematic
54 | pattern of such abuse occurs in the area of products for individuals to
55 | use, which is precisely where it is most unacceptable. Therefore, we
56 | have designed this version of the GPL to prohibit the practice for those
57 | products. If such problems arise substantially in other domains, we
58 | stand ready to extend this provision to those domains in future versions
59 | of the GPL, as needed to protect the freedom of users.
60 |
61 | Finally, every program is threatened constantly by software patents.
62 | States should not allow patents to restrict development and use of
63 | software on general-purpose computers, but in those that do, we wish to
64 | avoid the special danger that patents applied to a free program could
65 | make it effectively proprietary. To prevent this, the GPL assures that
66 | patents cannot be used to render the program non-free.
67 |
68 | The precise terms and conditions for copying, distribution and
69 | modification follow.
70 |
71 | TERMS AND CONDITIONS
72 |
73 | 0. Definitions.
74 |
75 | "This License" refers to version 3 of the GNU General Public License.
76 |
77 | "Copyright" also means copyright-like laws that apply to other kinds of
78 | works, such as semiconductor masks.
79 |
80 | "The Program" refers to any copyrightable work licensed under this
81 | License. Each licensee is addressed as "you". "Licensees" and
82 | "recipients" may be individuals or organizations.
83 |
84 | To "modify" a work means to copy from or adapt all or part of the work
85 | in a fashion requiring copyright permission, other than the making of an
86 | exact copy. The resulting work is called a "modified version" of the
87 | earlier work or a work "based on" the earlier work.
88 |
89 | A "covered work" means either the unmodified Program or a work based
90 | on the Program.
91 |
92 | To "propagate" a work means to do anything with it that, without
93 | permission, would make you directly or secondarily liable for
94 | infringement under applicable copyright law, except executing it on a
95 | computer or modifying a private copy. Propagation includes copying,
96 | distribution (with or without modification), making available to the
97 | public, and in some countries other activities as well.
98 |
99 | To "convey" a work means any kind of propagation that enables other
100 | parties to make or receive copies. Mere interaction with a user through
101 | a computer network, with no transfer of a copy, is not conveying.
102 |
103 | An interactive user interface displays "Appropriate Legal Notices"
104 | to the extent that it includes a convenient and prominently visible
105 | feature that (1) displays an appropriate copyright notice, and (2)
106 | tells the user that there is no warranty for the work (except to the
107 | extent that warranties are provided), that licensees may convey the
108 | work under this License, and how to view a copy of this License. If
109 | the interface presents a list of user commands or options, such as a
110 | menu, a prominent item in the list meets this criterion.
111 |
112 | 1. Source Code.
113 |
114 | The "source code" for a work means the preferred form of the work
115 | for making modifications to it. "Object code" means any non-source
116 | form of a work.
117 |
118 | A "Standard Interface" means an interface that either is an official
119 | standard defined by a recognized standards body, or, in the case of
120 | interfaces specified for a particular programming language, one that
121 | is widely used among developers working in that language.
122 |
123 | The "System Libraries" of an executable work include anything, other
124 | than the work as a whole, that (a) is included in the normal form of
125 | packaging a Major Component, but which is not part of that Major
126 | Component, and (b) serves only to enable use of the work with that
127 | Major Component, or to implement a Standard Interface for which an
128 | implementation is available to the public in source code form. A
129 | "Major Component", in this context, means a major essential component
130 | (kernel, window system, and so on) of the specific operating system
131 | (if any) on which the executable work runs, or a compiler used to
132 | produce the work, or an object code interpreter used to run it.
133 |
134 | The "Corresponding Source" for a work in object code form means all
135 | the source code needed to generate, install, and (for an executable
136 | work) run the object code and to modify the work, including scripts to
137 | control those activities. However, it does not include the work's
138 | System Libraries, or general-purpose tools or generally available free
139 | programs which are used unmodified in performing those activities but
140 | which are not part of the work. For example, Corresponding Source
141 | includes interface definition files associated with source files for
142 | the work, and the source code for shared libraries and dynamically
143 | linked subprograms that the work is specifically designed to require,
144 | such as by intimate data communication or control flow between those
145 | subprograms and other parts of the work.
146 |
147 | The Corresponding Source need not include anything that users
148 | can regenerate automatically from other parts of the Corresponding
149 | Source.
150 |
151 | The Corresponding Source for a work in source code form is that
152 | same work.
153 |
154 | 2. Basic Permissions.
155 |
156 | All rights granted under this License are granted for the term of
157 | copyright on the Program, and are irrevocable provided the stated
158 | conditions are met. This License explicitly affirms your unlimited
159 | permission to run the unmodified Program. The output from running a
160 | covered work is covered by this License only if the output, given its
161 | content, constitutes a covered work. This License acknowledges your
162 | rights of fair use or other equivalent, as provided by copyright law.
163 |
164 | You may make, run and propagate covered works that you do not
165 | convey, without conditions so long as your license otherwise remains
166 | in force. You may convey covered works to others for the sole purpose
167 | of having them make modifications exclusively for you, or provide you
168 | with facilities for running those works, provided that you comply with
169 | the terms of this License in conveying all material for which you do
170 | not control copyright. Those thus making or running the covered works
171 | for you must do so exclusively on your behalf, under your direction
172 | and control, on terms that prohibit them from making any copies of
173 | your copyrighted material outside their relationship with you.
174 |
175 | Conveying under any other circumstances is permitted solely under
176 | the conditions stated below. Sublicensing is not allowed; section 10
177 | makes it unnecessary.
178 |
179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
180 |
181 | No covered work shall be deemed part of an effective technological
182 | measure under any applicable law fulfilling obligations under article
183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or
184 | similar laws prohibiting or restricting circumvention of such
185 | measures.
186 |
187 | When you convey a covered work, you waive any legal power to forbid
188 | circumvention of technological measures to the extent such circumvention
189 | is effected by exercising rights under this License with respect to
190 | the covered work, and you disclaim any intention to limit operation or
191 | modification of the work as a means of enforcing, against the work's
192 | users, your or third parties' legal rights to forbid circumvention of
193 | technological measures.
194 |
195 | 4. Conveying Verbatim Copies.
196 |
197 | You may convey verbatim copies of the Program's source code as you
198 | receive it, in any medium, provided that you conspicuously and
199 | appropriately publish on each copy an appropriate copyright notice;
200 | keep intact all notices stating that this License and any
201 | non-permissive terms added in accord with section 7 apply to the code;
202 | keep intact all notices of the absence of any warranty; and give all
203 | recipients a copy of this License along with the Program.
204 |
205 | You may charge any price or no price for each copy that you convey,
206 | and you may offer support or warranty protection for a fee.
207 |
208 | 5. Conveying Modified Source Versions.
209 |
210 | You may convey a work based on the Program, or the modifications to
211 | produce it from the Program, in the form of source code under the
212 | terms of section 4, provided that you also meet all of these conditions:
213 |
214 | a) The work must carry prominent notices stating that you modified
215 | it, and giving a relevant date.
216 |
217 | b) The work must carry prominent notices stating that it is
218 | released under this License and any conditions added under section
219 | 7. This requirement modifies the requirement in section 4 to
220 | "keep intact all notices".
221 |
222 | c) You must license the entire work, as a whole, under this
223 | License to anyone who comes into possession of a copy. This
224 | License will therefore apply, along with any applicable section 7
225 | additional terms, to the whole of the work, and all its parts,
226 | regardless of how they are packaged. This License gives no
227 | permission to license the work in any other way, but it does not
228 | invalidate such permission if you have separately received it.
229 |
230 | d) If the work has interactive user interfaces, each must display
231 | Appropriate Legal Notices; however, if the Program has interactive
232 | interfaces that do not display Appropriate Legal Notices, your
233 | work need not make them do so.
234 |
235 | A compilation of a covered work with other separate and independent
236 | works, which are not by their nature extensions of the covered work,
237 | and which are not combined with it such as to form a larger program,
238 | in or on a volume of a storage or distribution medium, is called an
239 | "aggregate" if the compilation and its resulting copyright are not
240 | used to limit the access or legal rights of the compilation's users
241 | beyond what the individual works permit. Inclusion of a covered work
242 | in an aggregate does not cause this License to apply to the other
243 | parts of the aggregate.
244 |
245 | 6. Conveying Non-Source Forms.
246 |
247 | You may convey a covered work in object code form under the terms
248 | of sections 4 and 5, provided that you also convey the
249 | machine-readable Corresponding Source under the terms of this License,
250 | in one of these ways:
251 |
252 | a) Convey the object code in, or embodied in, a physical product
253 | (including a physical distribution medium), accompanied by the
254 | Corresponding Source fixed on a durable physical medium
255 | customarily used for software interchange.
256 |
257 | b) Convey the object code in, or embodied in, a physical product
258 | (including a physical distribution medium), accompanied by a
259 | written offer, valid for at least three years and valid for as
260 | long as you offer spare parts or customer support for that product
261 | model, to give anyone who possesses the object code either (1) a
262 | copy of the Corresponding Source for all the software in the
263 | product that is covered by this License, on a durable physical
264 | medium customarily used for software interchange, for a price no
265 | more than your reasonable cost of physically performing this
266 | conveying of source, or (2) access to copy the
267 | Corresponding Source from a network server at no charge.
268 |
269 | c) Convey individual copies of the object code with a copy of the
270 | written offer to provide the Corresponding Source. This
271 | alternative is allowed only occasionally and noncommercially, and
272 | only if you received the object code with such an offer, in accord
273 | with subsection 6b.
274 |
275 | d) Convey the object code by offering access from a designated
276 | place (gratis or for a charge), and offer equivalent access to the
277 | Corresponding Source in the same way through the same place at no
278 | further charge. You need not require recipients to copy the
279 | Corresponding Source along with the object code. If the place to
280 | copy the object code is a network server, the Corresponding Source
281 | may be on a different server (operated by you or a third party)
282 | that supports equivalent copying facilities, provided you maintain
283 | clear directions next to the object code saying where to find the
284 | Corresponding Source. Regardless of what server hosts the
285 | Corresponding Source, you remain obligated to ensure that it is
286 | available for as long as needed to satisfy these requirements.
287 |
288 | e) Convey the object code using peer-to-peer transmission, provided
289 | you inform other peers where the object code and Corresponding
290 | Source of the work are being offered to the general public at no
291 | charge under subsection 6d.
292 |
293 | A separable portion of the object code, whose source code is excluded
294 | from the Corresponding Source as a System Library, need not be
295 | included in conveying the object code work.
296 |
297 | A "User Product" is either (1) a "consumer product", which means any
298 | tangible personal property which is normally used for personal, family,
299 | or household purposes, or (2) anything designed or sold for incorporation
300 | into a dwelling. In determining whether a product is a consumer product,
301 | doubtful cases shall be resolved in favor of coverage. For a particular
302 | product received by a particular user, "normally used" refers to a
303 | typical or common use of that class of product, regardless of the status
304 | of the particular user or of the way in which the particular user
305 | actually uses, or expects or is expected to use, the product. A product
306 | is a consumer product regardless of whether the product has substantial
307 | commercial, industrial or non-consumer uses, unless such uses represent
308 | the only significant mode of use of the product.
309 |
310 | "Installation Information" for a User Product means any methods,
311 | procedures, authorization keys, or other information required to install
312 | and execute modified versions of a covered work in that User Product from
313 | a modified version of its Corresponding Source. The information must
314 | suffice to ensure that the continued functioning of the modified object
315 | code is in no case prevented or interfered with solely because
316 | modification has been made.
317 |
318 | If you convey an object code work under this section in, or with, or
319 | specifically for use in, a User Product, and the conveying occurs as
320 | part of a transaction in which the right of possession and use of the
321 | User Product is transferred to the recipient in perpetuity or for a
322 | fixed term (regardless of how the transaction is characterized), the
323 | Corresponding Source conveyed under this section must be accompanied
324 | by the Installation Information. But this requirement does not apply
325 | if neither you nor any third party retains the ability to install
326 | modified object code on the User Product (for example, the work has
327 | been installed in ROM).
328 |
329 | The requirement to provide Installation Information does not include a
330 | requirement to continue to provide support service, warranty, or updates
331 | for a work that has been modified or installed by the recipient, or for
332 | the User Product in which it has been modified or installed. Access to a
333 | network may be denied when the modification itself materially and
334 | adversely affects the operation of the network or violates the rules and
335 | protocols for communication across the network.
336 |
337 | Corresponding Source conveyed, and Installation Information provided,
338 | in accord with this section must be in a format that is publicly
339 | documented (and with an implementation available to the public in
340 | source code form), and must require no special password or key for
341 | unpacking, reading or copying.
342 |
343 | 7. Additional Terms.
344 |
345 | "Additional permissions" are terms that supplement the terms of this
346 | License by making exceptions from one or more of its conditions.
347 | Additional permissions that are applicable to the entire Program shall
348 | be treated as though they were included in this License, to the extent
349 | that they are valid under applicable law. If additional permissions
350 | apply only to part of the Program, that part may be used separately
351 | under those permissions, but the entire Program remains governed by
352 | this License without regard to the additional permissions.
353 |
354 | When you convey a copy of a covered work, you may at your option
355 | remove any additional permissions from that copy, or from any part of
356 | it. (Additional permissions may be written to require their own
357 | removal in certain cases when you modify the work.) You may place
358 | additional permissions on material, added by you to a covered work,
359 | for which you have or can give appropriate copyright permission.
360 |
361 | Notwithstanding any other provision of this License, for material you
362 | add to a covered work, you may (if authorized by the copyright holders of
363 | that material) supplement the terms of this License with terms:
364 |
365 | a) Disclaiming warranty or limiting liability differently from the
366 | terms of sections 15 and 16 of this License; or
367 |
368 | b) Requiring preservation of specified reasonable legal notices or
369 | author attributions in that material or in the Appropriate Legal
370 | Notices displayed by works containing it; or
371 |
372 | c) Prohibiting misrepresentation of the origin of that material, or
373 | requiring that modified versions of such material be marked in
374 | reasonable ways as different from the original version; or
375 |
376 | d) Limiting the use for publicity purposes of names of licensors or
377 | authors of the material; or
378 |
379 | e) Declining to grant rights under trademark law for use of some
380 | trade names, trademarks, or service marks; or
381 |
382 | f) Requiring indemnification of licensors and authors of that
383 | material by anyone who conveys the material (or modified versions of
384 | it) with contractual assumptions of liability to the recipient, for
385 | any liability that these contractual assumptions directly impose on
386 | those licensors and authors.
387 |
388 | All other non-permissive additional terms are considered "further
389 | restrictions" within the meaning of section 10. If the Program as you
390 | received it, or any part of it, contains a notice stating that it is
391 | governed by this License along with a term that is a further
392 | restriction, you may remove that term. If a license document contains
393 | a further restriction but permits relicensing or conveying under this
394 | License, you may add to a covered work material governed by the terms
395 | of that license document, provided that the further restriction does
396 | not survive such relicensing or conveying.
397 |
398 | If you add terms to a covered work in accord with this section, you
399 | must place, in the relevant source files, a statement of the
400 | additional terms that apply to those files, or a notice indicating
401 | where to find the applicable terms.
402 |
403 | Additional terms, permissive or non-permissive, may be stated in the
404 | form of a separately written license, or stated as exceptions;
405 | the above requirements apply either way.
406 |
407 | 8. Termination.
408 |
409 | You may not propagate or modify a covered work except as expressly
410 | provided under this License. Any attempt otherwise to propagate or
411 | modify it is void, and will automatically terminate your rights under
412 | this License (including any patent licenses granted under the third
413 | paragraph of section 11).
414 |
415 | However, if you cease all violation of this License, then your
416 | license from a particular copyright holder is reinstated (a)
417 | provisionally, unless and until the copyright holder explicitly and
418 | finally terminates your license, and (b) permanently, if the copyright
419 | holder fails to notify you of the violation by some reasonable means
420 | prior to 60 days after the cessation.
421 |
422 | Moreover, your license from a particular copyright holder is
423 | reinstated permanently if the copyright holder notifies you of the
424 | violation by some reasonable means, this is the first time you have
425 | received notice of violation of this License (for any work) from that
426 | copyright holder, and you cure the violation prior to 30 days after
427 | your receipt of the notice.
428 |
429 | Termination of your rights under this section does not terminate the
430 | licenses of parties who have received copies or rights from you under
431 | this License. If your rights have been terminated and not permanently
432 | reinstated, you do not qualify to receive new licenses for the same
433 | material under section 10.
434 |
435 | 9. Acceptance Not Required for Having Copies.
436 |
437 | You are not required to accept this License in order to receive or
438 | run a copy of the Program. Ancillary propagation of a covered work
439 | occurring solely as a consequence of using peer-to-peer transmission
440 | to receive a copy likewise does not require acceptance. However,
441 | nothing other than this License grants you permission to propagate or
442 | modify any covered work. These actions infringe copyright if you do
443 | not accept this License. Therefore, by modifying or propagating a
444 | covered work, you indicate your acceptance of this License to do so.
445 |
446 | 10. Automatic Licensing of Downstream Recipients.
447 |
448 | Each time you convey a covered work, the recipient automatically
449 | receives a license from the original licensors, to run, modify and
450 | propagate that work, subject to this License. You are not responsible
451 | for enforcing compliance by third parties with this License.
452 |
453 | An "entity transaction" is a transaction transferring control of an
454 | organization, or substantially all assets of one, or subdividing an
455 | organization, or merging organizations. If propagation of a covered
456 | work results from an entity transaction, each party to that
457 | transaction who receives a copy of the work also receives whatever
458 | licenses to the work the party's predecessor in interest had or could
459 | give under the previous paragraph, plus a right to possession of the
460 | Corresponding Source of the work from the predecessor in interest, if
461 | the predecessor has it or can get it with reasonable efforts.
462 |
463 | You may not impose any further restrictions on the exercise of the
464 | rights granted or affirmed under this License. For example, you may
465 | not impose a license fee, royalty, or other charge for exercise of
466 | rights granted under this License, and you may not initiate litigation
467 | (including a cross-claim or counterclaim in a lawsuit) alleging that
468 | any patent claim is infringed by making, using, selling, offering for
469 | sale, or importing the Program or any portion of it.
470 |
471 | 11. Patents.
472 |
473 | A "contributor" is a copyright holder who authorizes use under this
474 | License of the Program or a work on which the Program is based. The
475 | work thus licensed is called the contributor's "contributor version".
476 |
477 | A contributor's "essential patent claims" are all patent claims
478 | owned or controlled by the contributor, whether already acquired or
479 | hereafter acquired, that would be infringed by some manner, permitted
480 | by this License, of making, using, or selling its contributor version,
481 | but do not include claims that would be infringed only as a
482 | consequence of further modification of the contributor version. For
483 | purposes of this definition, "control" includes the right to grant
484 | patent sublicenses in a manner consistent with the requirements of
485 | this License.
486 |
487 | Each contributor grants you a non-exclusive, worldwide, royalty-free
488 | patent license under the contributor's essential patent claims, to
489 | make, use, sell, offer for sale, import and otherwise run, modify and
490 | propagate the contents of its contributor version.
491 |
492 | In the following three paragraphs, a "patent license" is any express
493 | agreement or commitment, however denominated, not to enforce a patent
494 | (such as an express permission to practice a patent or covenant not to
495 | sue for patent infringement). To "grant" such a patent license to a
496 | party means to make such an agreement or commitment not to enforce a
497 | patent against the party.
498 |
499 | If you convey a covered work, knowingly relying on a patent license,
500 | and the Corresponding Source of the work is not available for anyone
501 | to copy, free of charge and under the terms of this License, through a
502 | publicly available network server or other readily accessible means,
503 | then you must either (1) cause the Corresponding Source to be so
504 | available, or (2) arrange to deprive yourself of the benefit of the
505 | patent license for this particular work, or (3) arrange, in a manner
506 | consistent with the requirements of this License, to extend the patent
507 | license to downstream recipients. "Knowingly relying" means you have
508 | actual knowledge that, but for the patent license, your conveying the
509 | covered work in a country, or your recipient's use of the covered work
510 | in a country, would infringe one or more identifiable patents in that
511 | country that you have reason to believe are valid.
512 |
513 | If, pursuant to or in connection with a single transaction or
514 | arrangement, you convey, or propagate by procuring conveyance of, a
515 | covered work, and grant a patent license to some of the parties
516 | receiving the covered work authorizing them to use, propagate, modify
517 | or convey a specific copy of the covered work, then the patent license
518 | you grant is automatically extended to all recipients of the covered
519 | work and works based on it.
520 |
521 | A patent license is "discriminatory" if it does not include within
522 | the scope of its coverage, prohibits the exercise of, or is
523 | conditioned on the non-exercise of one or more of the rights that are
524 | specifically granted under this License. You may not convey a covered
525 | work if you are a party to an arrangement with a third party that is
526 | in the business of distributing software, under which you make payment
527 | to the third party based on the extent of your activity of conveying
528 | the work, and under which the third party grants, to any of the
529 | parties who would receive the covered work from you, a discriminatory
530 | patent license (a) in connection with copies of the covered work
531 | conveyed by you (or copies made from those copies), or (b) primarily
532 | for and in connection with specific products or compilations that
533 | contain the covered work, unless you entered into that arrangement,
534 | or that patent license was granted, prior to 28 March 2007.
535 |
536 | Nothing in this License shall be construed as excluding or limiting
537 | any implied license or other defenses to infringement that may
538 | otherwise be available to you under applicable patent law.
539 |
540 | 12. No Surrender of Others' Freedom.
541 |
542 | If conditions are imposed on you (whether by court order, agreement or
543 | otherwise) that contradict the conditions of this License, they do not
544 | excuse you from the conditions of this License. If you cannot convey a
545 | covered work so as to satisfy simultaneously your obligations under this
546 | License and any other pertinent obligations, then as a consequence you may
547 | not convey it at all. For example, if you agree to terms that obligate you
548 | to collect a royalty for further conveying from those to whom you convey
549 | the Program, the only way you could satisfy both those terms and this
550 | License would be to refrain entirely from conveying the Program.
551 |
552 | 13. Use with the GNU Affero General Public License.
553 |
554 | Notwithstanding any other provision of this License, you have
555 | permission to link or combine any covered work with a work licensed
556 | under version 3 of the GNU Affero General Public License into a single
557 | combined work, and to convey the resulting work. The terms of this
558 | License will continue to apply to the part which is the covered work,
559 | but the special requirements of the GNU Affero General Public License,
560 | section 13, concerning interaction through a network will apply to the
561 | combination as such.
562 |
563 | 14. Revised Versions of this License.
564 |
565 | The Free Software Foundation may publish revised and/or new versions of
566 | the GNU General Public License from time to time. Such new versions will
567 | be similar in spirit to the present version, but may differ in detail to
568 | address new problems or concerns.
569 |
570 | Each version is given a distinguishing version number. If the
571 | Program specifies that a certain numbered version of the GNU General
572 | Public License "or any later version" applies to it, you have the
573 | option of following the terms and conditions either of that numbered
574 | version or of any later version published by the Free Software
575 | Foundation. If the Program does not specify a version number of the
576 | GNU General Public License, you may choose any version ever published
577 | by the Free Software Foundation.
578 |
579 | If the Program specifies that a proxy can decide which future
580 | versions of the GNU General Public License can be used, that proxy's
581 | public statement of acceptance of a version permanently authorizes you
582 | to choose that version for the Program.
583 |
584 | Later license versions may give you additional or different
585 | permissions. However, no additional obligations are imposed on any
586 | author or copyright holder as a result of your choosing to follow a
587 | later version.
588 |
589 | 15. Disclaimer of Warranty.
590 |
591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
599 |
600 | 16. Limitation of Liability.
601 |
602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
610 | SUCH DAMAGES.
611 |
612 | 17. Interpretation of Sections 15 and 16.
613 |
614 | If the disclaimer of warranty and limitation of liability provided
615 | above cannot be given local legal effect according to their terms,
616 | reviewing courts shall apply local law that most closely approximates
617 | an absolute waiver of all civil liability in connection with the
618 | Program, unless a warranty or assumption of liability accompanies a
619 | copy of the Program in return for a fee.
620 |
621 | END OF TERMS AND CONDITIONS
622 |
623 | How to Apply These Terms to Your New Programs
624 |
625 | If you develop a new program, and you want it to be of the greatest
626 | possible use to the public, the best way to achieve this is to make it
627 | free software which everyone can redistribute and change under these terms.
628 |
629 | To do so, attach the following notices to the program. It is safest
630 | to attach them to the start of each source file to most effectively
631 | state the exclusion of warranty; and each file should have at least
632 | the "copyright" line and a pointer to where the full notice is found.
633 |
634 |
635 | Copyright (C)
636 |
637 | This program is free software: you can redistribute it and/or modify
638 | it under the terms of the GNU General Public License as published by
639 | the Free Software Foundation, either version 3 of the License, or
640 | (at your option) any later version.
641 |
642 | This program is distributed in the hope that it will be useful,
643 | but WITHOUT ANY WARRANTY; without even the implied warranty of
644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
645 | GNU General Public License for more details.
646 |
647 | You should have received a copy of the GNU General Public License
648 | along with this program. If not, see .
649 |
650 | Also add information on how to contact you by electronic and paper mail.
651 |
652 | If the program does terminal interaction, make it output a short
653 | notice like this when it starts in an interactive mode:
654 |
655 | Copyright (C)
656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
657 | This is free software, and you are welcome to redistribute it
658 | under certain conditions; type `show c' for details.
659 |
660 | The hypothetical commands `show w' and `show c' should show the appropriate
661 | parts of the General Public License. Of course, your program's commands
662 | might be different; for a GUI interface, you would use an "about box".
663 |
664 | You should also get your employer (if you work as a programmer) or school,
665 | if any, to sign a "copyright disclaimer" for the program, if necessary.
666 | For more information on this, and how to apply and follow the GNU GPL, see
667 | .
668 |
669 | The GNU General Public License does not permit incorporating your program
670 | into proprietary programs. If your program is a subroutine library, you
671 | may consider it more useful to permit linking proprietary applications with
672 | the library. If this is what you want to do, use the GNU Lesser General
673 | Public License instead of this License. But first, please read
674 | .
675 |
--------------------------------------------------------------------------------
/Procfile:
--------------------------------------------------------------------------------
1 | web: puma -p $PORT
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # FriedPotato:プロセカ創作譜面サーバー
2 |
3 | https://potato.purplepalette.net に少しだけ機能を追加したサーバー。
4 | https://fp.sevenc7c.com から遊ぶことが出来ます。
5 | また、 https://fp.sevenc7c.com/tests/テストサーバーID でテストサーバーに接続できます。
6 |
7 | ## 追加機能
8 |
9 | - 背景画像の追加
10 | - 中間点の音変更
11 | - 旧譜面変換
12 | - エンジンの差し替え
13 |
14 | ## 必須事項
15 |
16 | - Ruby 3.0以上
17 | - DXRubyが動く環境
18 | - bundler
19 |
20 | ## 動かし方
21 |
22 | 1. [Rubyをインストールします。](https://rubyinstaller.org)[Ruby3.0、x86](https://github.com/oneclick/rubyinstaller2/releases/download/RubyInstaller-3.0.3-1/rubyinstaller-devkit-3.0.3-1-x86.exe)が推奨です。
23 | 1. PCを再起動します。
24 | 1. 右上のCodeボタンから`Download ZIP`でZipをダウンロードします。
25 | 1. 適当なところに解凍します。
26 | 1. `setup.bat`を起動します。
27 | 1. `launch.bat`を起動します。
28 | 1. Sonolusに画面のサーバーをカスタムサーバーとして登録します。
29 | 2. `(サーバー)/tests/テストサーバーID` でテストサーバーに接続できます。
30 |
31 | 2回目以降は`launch.bat`を動かせばサーバーを起動できます。
32 |
33 | ## 背景生成
34 |
35 | 背景の生成を有効にするには3つの方法があります。
36 |
37 | ### Web(Python)
38 | 1. 1度FriedPotatoを起動します。
39 | 3. config.ymlを開き、`background_engine`を`web`にします。
40 |
41 | ### DXRuby(Ruby)
42 |
43 | 1. 1度FriedPotatoを起動します。
44 | 2. [DXRubyの環境を整えます。](https://qiita.com/noanoa07/items/7df5886c619781d8d2ee#-d3dx9_40dll%E3%81%AE%E3%82%A4%E3%83%B3%E3%82%B9%E3%83%88%E3%83%BC%E3%83%AB%E6%96%B9%E6%B3%95)
45 | 3. config.ymlを開き、`background_engine`を`dxruby`にします。
46 |
47 | ### Pillow(Python)
48 |
49 | 1. 1度FriedPotatoを起動します。
50 | 2. [ここ](https://python.org/downloads/)か[ここ](https://pythonlinks.python.jp/ja/index.html)からPythonをダウンロードします。推奨は3.9.xです。
51 | 3. PCを再起動します。
52 | 4. `setup_python.bat`を起動します。
53 | 5. `background_engine`を`pillow`にします。
54 |
55 |
56 | ## ライセンス
57 |
58 | GPLv3で公開しています。
59 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 | require "zlib"
3 | require "json"
4 | require "yaml"
5 | require "zip"
6 |
7 | task :effect do
8 | clips = YAML.load_file("effect/clips.yml")
9 | effect_data = {clips: clips}
10 |
11 | clips.each do |clip|
12 | raise "Missing clip: #{clip}" unless File.exist?("effect/audio/#{clip["filename"]}")
13 | end
14 | Zlib::GzipWriter.open("public/repo/EffectData.gz") do |gz|
15 | gz.write(JSON.dump(effect_data))
16 | end
17 |
18 | File.delete("public/repo/EffectAudio.zip") if File.exist?("public/repo/EffectAudio.zip")
19 | Zip::File.open("public/repo/EffectAudio.zip", Zip::File::CREATE) do |zipfile|
20 | Dir["effect/audio/*"].each do |file|
21 | zipfile.add(file.sub("effect/audio/", ""), file)
22 | end
23 | end
24 |
25 | end
26 |
27 | task :lint do
28 | sh "rubocop *.rb"
29 | end
30 |
--------------------------------------------------------------------------------
/base.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "",
3 | "version": 1,
4 | "rating": 28,
5 | "engine": {
6 | "name": "pjsekai",
7 | "version": 4,
8 | "title": "プロセカ",
9 | "subtitle": "プロジェクトセカイ カラフルステージ!",
10 | "author": "Burrito",
11 | "skin": {
12 | "name": "pjsekai.classic",
13 | "version": 2,
14 | "title": "Project Sekai",
15 | "subtitle": "Project Sekai: Colorful Stage!",
16 | "author": "Sonolus",
17 | "thumbnail": {
18 | "type": "SkinThumbnail",
19 | "hash": "24faf30cc2e0d0f51aeca3815ef523306b627289",
20 | "url": "https://servers.purplepalette.net/repository/SkinThumbnail/24faf30cc2e0d0f51aeca3815ef523306b627289"
21 | },
22 | "data": {
23 | "type": "SkinData",
24 | "hash": "ad8a6ffa2ef4f742fee5ec3b917933cc3d2654af",
25 | "url": "https://servers.purplepalette.net/repository/SkinData/ad8a6ffa2ef4f742fee5ec3b917933cc3d2654af"
26 | },
27 | "texture": {
28 | "type": "SkinTexture",
29 | "hash": "2ed3b0d09918f89e167df8b2f17ad8601162c33c",
30 | "url": "https://servers.purplepalette.net/repository/SkinTexture/2ed3b0d09918f89e167df8b2f17ad8601162c33c"
31 | }
32 | },
33 | "background": {
34 | "name": "!name!",
35 | "version": 2,
36 | "title": "!title!",
37 | "subtitle": "!artist! / !author!",
38 | "thumbnail": {
39 | "type": "BackgroundThumbnail",
40 | "hash": "d934e5fc3d98a2aca954299061aecf9e7e1bc210",
41 | "url": "https://servers.purplepalette.net/repository/!name!/cover.png"
42 | },
43 | "data": {
44 | "type": "BackgroundData",
45 | "hash": "5e32e7fc235b0952da1b7aa0a03e7745e1a7b3d2",
46 | "url": "/repo/data.gz"
47 | },
48 | "image": {
49 | "type": "BackgroundImage",
50 | "url": "/generate/!name!"
51 | },
52 | "configuration": {
53 | "type": "BackgroundConfiguration",
54 | "hash": "6c5ad5e5f20ea90f16b63022064472e8840513d4",
55 | "url": "/repo/config.gz"
56 | }
57 | },
58 | "effect": {
59 | "name": "pjsekai.fixed",
60 | "version": 2,
61 | "title": "プロセカ",
62 | "subtitle": "プロジェクトセカイ カラフルステージ!",
63 | "author": "Sonolus",
64 | "thumbnail": {
65 | "type": "EffectThumbnail",
66 | "hash": "e5f439916eac9bbd316276e20aed999993653560",
67 | "url": "https://servers.purplepalette.net/repository/EffectThumbnail/e5f439916eac9bbd316276e20aed999993653560"
68 | },
69 | "data": {
70 | "type": "EffectData",
71 | "hash": "68b207281277c14cfd5e069d203c1de3b7f01f4f",
72 | "url": "/repo/seconfig.gz"
73 | }
74 | },
75 | "particle": {
76 | "name": "pjsekai.classic",
77 | "version": 1,
78 | "title": "Project Sekai",
79 | "subtitle": "Project Sekai: Colorful Stage!",
80 | "author": "Sonolus",
81 | "thumbnail": {
82 | "type": "ParticleThumbnail",
83 | "hash": "e5f439916eac9bbd316276e20aed999993653560",
84 | "url": "https://servers.purplepalette.net/repository/ParticleThumbnail/e5f439916eac9bbd316276e20aed999993653560"
85 | },
86 | "data": {
87 | "type": "ParticleData",
88 | "hash": "f84c5dead70ad62a00217589a73a07e7421818a8",
89 | "url": "https://servers.purplepalette.net/repository/ParticleData/f84c5dead70ad62a00217589a73a07e7421818a8"
90 | },
91 | "texture": {
92 | "type": "ParticleTexture",
93 | "hash": "4850a8f335204108c439def535bcf693c7f8d050",
94 | "url": "https://servers.purplepalette.net/repository/ParticleTexture/4850a8f335204108c439def535bcf693c7f8d050"
95 | }
96 | },
97 | "thumbnail": {
98 | "type": "EngineThumbnail",
99 | "hash": "e5f439916eac9bbd316276e20aed999993653560",
100 | "url": "https://servers.purplepalette.net/repository/EngineThumbnail/e5f439916eac9bbd316276e20aed999993653560"
101 | },
102 | "data": {
103 | "type": "EngineData",
104 | "hash": "86773c786f00b8b6cd2f6f99be11f62281385133",
105 | "url": "https://servers.purplepalette.net/repository/EngineData/86773c786f00b8b6cd2f6f99be11f62281385133"
106 | },
107 | "configuration": {
108 | "type": "EngineConfiguration",
109 | "hash": "55ada0ef19553e6a6742cffbb66f7dce9f85a7ee",
110 | "url": "https://servers.purplepalette.net/repository/EngineConfiguration/55ada0ef19553e6a6742cffbb66f7dce9f85a7ee"
111 | }
112 | },
113 | "useSkin": {
114 | "useDefault": true
115 | },
116 | "useBackground": {
117 | "useDefault": true
118 | },
119 | "useEffect": {
120 | "useDefault": true
121 | },
122 | "useParticle": {
123 | "useDefault": true
124 | },
125 | "title": "!title!",
126 | "artists": "!artist!",
127 | "author": "!author!",
128 | "cover": {
129 | "type": "LevelCover",
130 | "url": "/repo/levels/!name!/cover.png"
131 | },
132 | "bgm": {
133 | "type": "LevelBgm",
134 | "url": "/repo/levels/!name!/bgm.mp3"
135 | },
136 | "data": {
137 | "type": "LevelData",
138 | "url": "/repo/levels/!name!/data.gz"
139 | }
140 | }
--------------------------------------------------------------------------------
/bg_gen/background-base-extra.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sevenc-nanashi/FriedPotato/05893682caa4ee83877710e56c9ab97925e4487b/bg_gen/background-base-extra.png
--------------------------------------------------------------------------------
/bg_gen/background-base.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sevenc-nanashi/FriedPotato/05893682caa4ee83877710e56c9ab97925e4487b/bg_gen/background-base.png
--------------------------------------------------------------------------------
/bg_gen/bottom_mask.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sevenc-nanashi/FriedPotato/05893682caa4ee83877710e56c9ab97925e4487b/bg_gen/bottom_mask.png
--------------------------------------------------------------------------------
/bg_gen/dot-tile.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sevenc-nanashi/FriedPotato/05893682caa4ee83877710e56c9ab97925e4487b/bg_gen/dot-tile.png
--------------------------------------------------------------------------------
/bg_gen/dot-tilea.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sevenc-nanashi/FriedPotato/05893682caa4ee83877710e56c9ab97925e4487b/bg_gen/dot-tilea.png
--------------------------------------------------------------------------------
/bg_gen/dot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sevenc-nanashi/FriedPotato/05893682caa4ee83877710e56c9ab97925e4487b/bg_gen/dot.png
--------------------------------------------------------------------------------
/bg_gen/dot.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 | require "dxruby"
3 |
4 | dot = Image.load("dot.png")
5 | base = Image.load("background-base.png")
6 | target = RenderTarget.new(base.width, base.height)
7 |
8 | target.draw_tile(0, 0, [[0]], [dot], 0, 0, base.width, base.height)
9 |
10 | target.to_image.save("dot-tile.png")
11 |
12 | puts "done"
13 |
--------------------------------------------------------------------------------
/bg_gen/main.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 | require "dxruby"
3 | require "http"
4 |
5 | base = Image.load("bg_gen/background-base.png")
6 | base_extra = Image.load("bg_gen/background-base-extra.png")
7 | core_mask = Shader::Core.new(File.read("bg_gen/mask.hlsl"), { mask: :texture, alpha: :float })
8 | core_sub = Shader::Core.new(File.read("bg_gen/sub.hlsl"), { mask: :texture, alpha: :float })
9 |
10 | if File.exist?("overrides/#{name.delete_suffix(".extra")}/thumbnail.png")
11 | jacket = Image.load("overrides/#{name.delete_suffix(".extra")}/thumbnail.png")
12 | else
13 | url = if name.start_with?("l_")
14 | "https://PurplePalette.github.io/sonolus/repository/levels/#{name[2..].delete_suffix(".extra")}/jacket.jpg"
15 | elsif name.start_with?("level-")
16 | "https://minio.dnaroma.eu/sekai-assets/music/jacket/jacket_s_{id}_rip/jacket_s_{id}.png".gsub("{id}",
17 | name[6..].split(".")[0])
18 | else
19 | "https://servers.purplepalette.net/repository/#{name.delete_suffix(".extra")}/cover.png"
20 | end
21 | jacket = Image.load_from_file_in_memory(
22 | HTTP.get(
23 | url
24 | ).body.to_s
25 | )
26 | end
27 | mask_img = Image.load("bg_gen/mask-white.png")
28 | mask_img_sub = base
29 | dot_tile = Image.load("bg_gen/dot-tile.png")
30 | mask = Shader.new(core_mask, "render_alpha")
31 | mask.mask = mask_img
32 | mask.alpha = 0.3
33 |
34 | side_mask = Shader.new(core_mask, "render_alpha")
35 | side_mask.mask = Image.load("bg_gen/side-mask.png")
36 | side_mask.alpha = 0.8
37 |
38 | bottom_mask = Shader.new(core_mask, "render_red")
39 | bottom_mask.mask = Image.load("bg_gen/bottom_mask.png")
40 | bottom_mask.alpha = 0.7
41 |
42 | sub_mask = Shader.new(core_sub)
43 | sub_mask.mask = mask_img_sub
44 | sub_mask.alpha = 0.8
45 |
46 | orig = Image.load("bg_gen/orig.jpg")
47 |
48 | rt = RenderTarget.new(base.width, base.height)
49 | center_rt = RenderTarget.new(base.width, base.height)
50 | side_rt = RenderTarget.new(base.width, base.height)
51 | side_sub_rt = RenderTarget.new(base.width, base.height)
52 | bottom_rt = RenderTarget.new(base.width, base.height)
53 | bottom_trans_rt = RenderTarget.new(base.width, base.height)
54 | final_rt = RenderTarget.new(orig.width, orig.height)
55 |
56 | # 795 1255/193
57 | # 804 1245/628
58 |
59 | if name.end_with?(".extra")
60 | rt.draw(0, 0, base_extra)
61 | else
62 | rt.draw(0, 0, base)
63 | end
64 |
65 | center_rt.draw_morph(798, 193, 1252, 193, 1246, 635, 801, 635, jacket)
66 |
67 | rt.draw_shader(0, 0, center_rt, sub_mask)
68 |
69 | #449,194 : 1136,99 : 1152, 789 : 465 804
70 |
71 | side_rt.draw_morph(449, 114, 1136, 99, 1152, 789, 465, 804, jacket)
72 |
73 | # 1040, 145 : 1663, 70 : 1619, 707 : 1628, 622
74 |
75 | side_rt.draw_morph(1018, 92, 1635, 51, 1630, 740, 1026, 756, jacket)
76 |
77 | side_sub_rt.draw_shader(0, 0, side_rt, side_mask)
78 |
79 | rt.draw_shader(0, 0, side_sub_rt, sub_mask)
80 | # rt.draw(0, 0, rt4)
81 |
82 | bottom_rt.draw_morph(795, 1152, 1252, 1152, 1252, 713, 795, 713, jacket)
83 |
84 | bottom_trans_rt.draw_shader(0, 0, bottom_rt, bottom_mask)
85 | # bottom_trans_rt.draw_alpha(0, 0, bottom_rt, 256 * 0.)
86 |
87 | rt.draw(0, 0, bottom_trans_rt)
88 |
89 | rt.draw_shader(0, 0, dot_tile, mask)
90 |
91 | final_rt.draw(orig.width - base.width, (orig.height - base.height) / 2, rt)
92 |
93 | final_rt.to_image.save("dist/bg/#{name}.png")
94 |
--------------------------------------------------------------------------------
/bg_gen/mask-white.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sevenc-nanashi/FriedPotato/05893682caa4ee83877710e56c9ab97925e4487b/bg_gen/mask-white.png
--------------------------------------------------------------------------------
/bg_gen/mask.hlsl:
--------------------------------------------------------------------------------
1 | texture tex0;
2 | texture mask;
3 | float alpha;
4 |
5 | sampler Samp0 = sampler_state { Texture = ; };
6 |
7 | sampler Samp1 = sampler_state { Texture = ; };
8 |
9 | float4 PS(float2 input : TEXCOORD0) : COLOR0 {
10 | float4 output;
11 | float4 mask;
12 |
13 | output = tex2D(Samp0, input);
14 | mask = tex2D(Samp1, input);
15 | output.a *= mask.a * alpha;
16 | return output;
17 | }
18 |
19 | float4 PS2(float2 input : TEXCOORD0) : COLOR0 {
20 | float4 output;
21 | float4 mask;
22 |
23 | output = tex2D(Samp0, input);
24 | mask = tex2D(Samp1, input);
25 | output.a *= mask.r * alpha;
26 | return output;
27 | }
28 | technique render_alpha {
29 | pass { PixelShader = compile ps_2_0 PS(); }
30 | }
31 | technique render_red {
32 | pass { PixelShader = compile ps_2_0 PS2(); }
33 | }
--------------------------------------------------------------------------------
/bg_gen/mask.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sevenc-nanashi/FriedPotato/05893682caa4ee83877710e56c9ab97925e4487b/bg_gen/mask.png
--------------------------------------------------------------------------------
/bg_gen/minus-mask-extra.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sevenc-nanashi/FriedPotato/05893682caa4ee83877710e56c9ab97925e4487b/bg_gen/minus-mask-extra.png
--------------------------------------------------------------------------------
/bg_gen/minus-mask.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sevenc-nanashi/FriedPotato/05893682caa4ee83877710e56c9ab97925e4487b/bg_gen/minus-mask.png
--------------------------------------------------------------------------------
/bg_gen/orig.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sevenc-nanashi/FriedPotato/05893682caa4ee83877710e56c9ab97925e4487b/bg_gen/orig.jpg
--------------------------------------------------------------------------------
/bg_gen/side-mask.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sevenc-nanashi/FriedPotato/05893682caa4ee83877710e56c9ab97925e4487b/bg_gen/side-mask.png
--------------------------------------------------------------------------------
/bg_gen/sub.hlsl:
--------------------------------------------------------------------------------
1 | texture tex0;
2 | texture mask;
3 | float alpha;
4 |
5 | sampler Samp0 = sampler_state { Texture = ; };
6 |
7 | sampler Samp1 = sampler_state { Texture = ; };
8 |
9 | float4 PS(float2 input : TEXCOORD0) : COLOR0 {
10 | float4 output;
11 | float4 output2;
12 | float4 output3;
13 | float tmpalpha;
14 |
15 | output = tex2D(Samp0, input);
16 | output2 = tex2D(Samp1, input);
17 | output3 = 0, 0, 0, 0;
18 | tmpalpha = (output2.r + output2.g + output2.b) / 3.0;
19 | // output3.r = output.r * (1.0 - tmpalpha);
20 | // output3.g = output.g * (1.0 - tmpalpha);
21 | // output3.b = output.b * (1.0 - tmpalpha);
22 | // output3.a = output.a;
23 | output.a *= 1.0 - tmpalpha * 3;
24 | output.a *= alpha;
25 | // output.rgb = 1.0 - tmpalpha * alpha;
26 | return output;
27 | }
28 |
29 | technique {
30 | pass { PixelShader = compile ps_2_0 PS(); }
31 | }
--------------------------------------------------------------------------------
/config.ru:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 | require_relative "main"
3 |
4 | run Sinatra::Application
5 |
--------------------------------------------------------------------------------
/data/effects/frpt-pjsekai.fixed.json:
--------------------------------------------------------------------------------
1 | {
2 | "audio": {
3 | "hash": "978a6f1cc1e93700acf1e227eeef231a59ca47bd8162f0ad4826a1b8ce4514b7",
4 | "type": "EffectAudio",
5 | "url": "/repo/EffectAudio.zip"
6 | },
7 | "author": "Sonolus",
8 | "data": {
9 | "hash": "9b1fa72565dfc8277ebf3a7f592ba2c4cd6a142b8360960d844a5cb5600bb7d2",
10 | "type": "EffectData",
11 | "url": "/repo/EffectData.gz"
12 | },
13 | "name": "frpt-pjsekai.fixed",
14 | "subtitle": "From servers.sonolus.com/pjsekai",
15 | "description": "PJSekai + Trace notes",
16 | "thumbnail": {
17 | "hash": "e5f439916eac9bbd316276e20aed999993653560",
18 | "type": "EffectThumbnail",
19 | "url": "https://servers.sonolus.com/pjsekai/sonolus/repository/EffectThumbnail/e5f439916eac9bbd316276e20aed999993653560"
20 | },
21 | "title": "PJSekai",
22 | "version": 4
23 | }
24 |
--------------------------------------------------------------------------------
/data/engines/frpt-pjsekai.extended.json:
--------------------------------------------------------------------------------
1 | {
2 | "author": "Nanashi. (Forked from Burrito)",
3 | "background": {
4 | "author": "Sonolus",
5 | "configuration": {
6 | "hash": "d4367d5b719299e702ca26a2923ce5ef3235c1c7",
7 | "type": "BackgroundConfiguration",
8 | "url": "https://servers.sonolus.com/pjsekai/sonolus/repository/BackgroundConfiguration/d4367d5b719299e702ca26a2923ce5ef3235c1c7"
9 | },
10 | "data": {
11 | "hash": "5e32e7fc235b0952da1b7aa0a03e7745e1a7b3d2",
12 | "type": "BackgroundData",
13 | "url": "https://servers.sonolus.com/pjsekai/sonolus/repository/BackgroundData/5e32e7fc235b0952da1b7aa0a03e7745e1a7b3d2"
14 | },
15 | "image": {
16 | "hash": "8dd5a1d679ffdd22d109fca9ccef37272a4fc5db",
17 | "type": "BackgroundImage",
18 | "url": "https://servers.sonolus.com/pjsekai/sonolus/repository/BackgroundImage/8dd5a1d679ffdd22d109fca9ccef37272a4fc5db"
19 | },
20 | "name": "pjsekai",
21 | "subtitle": "プロジェクトセカイ カラフルステージ!",
22 | "thumbnail": {
23 | "hash": "bc97c960f8cb509ed17ebfe7f15bf2a089a98b90",
24 | "type": "BackgroundThumbnail",
25 | "url": "https://servers.sonolus.com/pjsekai/sonolus/repository/BackgroundThumbnail/bc97c960f8cb509ed17ebfe7f15bf2a089a98b90"
26 | },
27 | "title": "Live",
28 | "version": 2
29 | },
30 | "configuration": {
31 | "hash": "cb325b7b2225d96e2eff1e9942c96bef85270ac3ec8f807318f605731f07f27a",
32 | "type": "EngineConfiguration",
33 | "url": "/engine/configuration"
34 | },
35 | "data": {
36 | "hash": "3784971ff2b869cc405871b8f2488c07d8547fc33427abfe8e5885ea9aa9afb8",
37 | "type": "EngineData",
38 | "url": "/engine/data"
39 | },
40 | "name": "frpt-pjsekai.extended",
41 | "particle": "!import:particles/frpt-pjsekai.json",
42 | "skin": "!import:skins/frpt-pjsekai.extended.json",
43 | "effect": "!import:effects/frpt-pjsekai.fixed.json",
44 | "description": "PJSekai + Trace notes, custom judgement, etc.",
45 | "subtitle": "From servers.sonolus.com/pjsekai",
46 | "thumbnail": {
47 | "hash": "e5f439916eac9bbd316276e20aed999993653560",
48 | "type": "EngineThumbnail",
49 | "url": "https://servers.sonolus.com/pjsekai/sonolus/repository/EngineThumbnail/e5f439916eac9bbd316276e20aed999993653560"
50 | },
51 | "title": "PJSekai+",
52 | "version": 7
53 | }
54 |
--------------------------------------------------------------------------------
/data/particles/frpt-pjsekai.json:
--------------------------------------------------------------------------------
1 | {
2 | "author": "Sonolus",
3 | "data": {
4 | "hash": "f84c5dead70ad62a00217589a73a07e7421818a8",
5 | "url": "https://cc.sevenc7c.com/sonolus/assets/particles/ParticleData",
6 | "type": "ParticleData"
7 | },
8 | "name": "frpt-pjsekai",
9 | "subtitle": "From servers.sonolus.com/pjsekai",
10 | "description": "Nothing changed.",
11 | "texture": {
12 | "hash": "57b4bd504f814150dea87b41f39c2c7a63f29518",
13 | "url": "https://cc.sevenc7c.com/sonolus/assets/particles/ParticleTexture",
14 | "type": "ParticleTexture"
15 | },
16 | "thumbnail": {
17 | "hash": "e5f439916eac9bbd316276e20aed999993653560",
18 | "url": "https://cc.sevenc7c.com/sonolus/assets/particles/ParticleThumbnail",
19 | "type": "ParticleThumbnail"
20 | },
21 | "title": "PJSekai",
22 | "version": 1
23 | }
24 |
--------------------------------------------------------------------------------
/data/skins/frpt-pjsekai.extended.json:
--------------------------------------------------------------------------------
1 | {
2 | "author": "Sonolus + Nanashi.",
3 | "data": {
4 | "hash": "e6c48925a0947ebe80458525ec0932453edfe52e434b6774e564c260da9e59d7",
5 | "type": "SkinData",
6 | "url": "/skin/data"
7 | },
8 | "name": "frpt-pjsekai.extended",
9 | "subtitle": "PJSekai Extended",
10 | "description": "PjSekai + Trace notes",
11 | "texture": {
12 | "hash": "22004dc0538b0df80ca7ec54186c6b385d3f6f0cdcc46527167bf1a99876d862",
13 | "type": "SkinTexture",
14 | "url": "/skin/texture"
15 | },
16 | "thumbnail": {
17 | "hash": "24faf30cc2e0d0f51aeca3815ef523306b627289",
18 | "type": "SkinThumbnail",
19 | "url": "https://servers.sonolus.com/pjsekai/sonolus/repository/SkinThumbnail/24faf30cc2e0d0f51aeca3815ef523306b627289"
20 | },
21 | "title": "PJSekai+",
22 | "version": 2
23 | }
--------------------------------------------------------------------------------
/dist/bg/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sevenc-nanashi/FriedPotato/05893682caa4ee83877710e56c9ab97925e4487b/dist/bg/.gitkeep
--------------------------------------------------------------------------------
/dist/bgm/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sevenc-nanashi/FriedPotato/05893682caa4ee83877710e56c9ab97925e4487b/dist/bgm/.gitkeep
--------------------------------------------------------------------------------
/dist/conv/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sevenc-nanashi/FriedPotato/05893682caa4ee83877710e56c9ab97925e4487b/dist/conv/.gitkeep
--------------------------------------------------------------------------------
/dist/data-overrides/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sevenc-nanashi/FriedPotato/05893682caa4ee83877710e56c9ab97925e4487b/dist/data-overrides/.gitkeep
--------------------------------------------------------------------------------
/dist/modify/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sevenc-nanashi/FriedPotato/05893682caa4ee83877710e56c9ab97925e4487b/dist/modify/.gitkeep
--------------------------------------------------------------------------------
/dist/skin/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sevenc-nanashi/FriedPotato/05893682caa4ee83877710e56c9ab97925e4487b/dist/skin/.gitkeep
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '2.4'
2 | services:
3 | main:
4 | build: .
5 | ports:
6 | - "${RUBY_PORT:?}:4567"
7 | environment:
8 | - DOCKER=true
9 | - PORT=4567
10 | cpus: 0.2
11 | volumes:
12 | - "./dist:/root/dist"
13 |
--------------------------------------------------------------------------------
/effect/audio/flick.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sevenc-nanashi/FriedPotato/05893682caa4ee83877710e56c9ab97925e4487b/effect/audio/flick.mp3
--------------------------------------------------------------------------------
/effect/audio/flick_critical.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sevenc-nanashi/FriedPotato/05893682caa4ee83877710e56c9ab97925e4487b/effect/audio/flick_critical.mp3
--------------------------------------------------------------------------------
/effect/audio/good.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sevenc-nanashi/FriedPotato/05893682caa4ee83877710e56c9ab97925e4487b/effect/audio/good.mp3
--------------------------------------------------------------------------------
/effect/audio/great.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sevenc-nanashi/FriedPotato/05893682caa4ee83877710e56c9ab97925e4487b/effect/audio/great.mp3
--------------------------------------------------------------------------------
/effect/audio/long.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sevenc-nanashi/FriedPotato/05893682caa4ee83877710e56c9ab97925e4487b/effect/audio/long.mp3
--------------------------------------------------------------------------------
/effect/audio/long_critical.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sevenc-nanashi/FriedPotato/05893682caa4ee83877710e56c9ab97925e4487b/effect/audio/long_critical.mp3
--------------------------------------------------------------------------------
/effect/audio/perfect.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sevenc-nanashi/FriedPotato/05893682caa4ee83877710e56c9ab97925e4487b/effect/audio/perfect.mp3
--------------------------------------------------------------------------------
/effect/audio/perfect_critical.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sevenc-nanashi/FriedPotato/05893682caa4ee83877710e56c9ab97925e4487b/effect/audio/perfect_critical.mp3
--------------------------------------------------------------------------------
/effect/audio/tap.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sevenc-nanashi/FriedPotato/05893682caa4ee83877710e56c9ab97925e4487b/effect/audio/tap.mp3
--------------------------------------------------------------------------------
/effect/audio/tick.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sevenc-nanashi/FriedPotato/05893682caa4ee83877710e56c9ab97925e4487b/effect/audio/tick.mp3
--------------------------------------------------------------------------------
/effect/audio/tick_critical.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sevenc-nanashi/FriedPotato/05893682caa4ee83877710e56c9ab97925e4487b/effect/audio/tick_critical.mp3
--------------------------------------------------------------------------------
/effect/audio/trace.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sevenc-nanashi/FriedPotato/05893682caa4ee83877710e56c9ab97925e4487b/effect/audio/trace.mp3
--------------------------------------------------------------------------------
/effect/audio/trace_critical.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sevenc-nanashi/FriedPotato/05893682caa4ee83877710e56c9ab97925e4487b/effect/audio/trace_critical.mp3
--------------------------------------------------------------------------------
/effect/audio/trace_flick.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sevenc-nanashi/FriedPotato/05893682caa4ee83877710e56c9ab97925e4487b/effect/audio/trace_flick.mp3
--------------------------------------------------------------------------------
/effect/clips.yml:
--------------------------------------------------------------------------------
1 | ---
2 | - id: 1
3 | filename: perfect.mp3
4 | - id: 2
5 | filename: great.mp3
6 | - id: 3
7 | filename: good.mp3
8 | - id: 100
9 | filename: long.mp3
10 | - id: 1001
11 | filename: flick.mp3
12 | - id: 1002
13 | filename: flick.mp3
14 | - id: 1003
15 | filename: flick.mp3
16 | - id: 10000
17 | filename: tap.mp3
18 | - id: 100201
19 | filename: perfect_critical.mp3
20 | - id: 100202
21 | filename: tick_critical.mp3
22 | - id: 100203
23 | filename: flick_critical.mp3
24 | - id: 100204
25 | filename: tick.mp3
26 | - id: 100205
27 | filename: trace.mp3
28 | - id: 100206
29 | filename: trace_critical.mp3
30 | - id: 100207
31 | filename: trace_flick.mp3
32 | - id: 100208
33 | filename: long_critical.mp3
34 |
--------------------------------------------------------------------------------
/falcon.rb:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env -S falcon host
2 | # frozen_string_literal: true
3 |
4 | load :rack
5 |
6 | rack "0.0.0.0" do
7 | scheme "http"
8 | protocol { Async::HTTP::Protocol::HTTP1 }
9 | endpoint { Async::HTTP::Endpoint.for(scheme, "0.0.0.0", port: ENV["PORT"] || "4567", protocol: protocol) }
10 | end
11 |
--------------------------------------------------------------------------------
/info.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "name": "frpt-system",
4 | "version": 1,
5 | "rating": 0,
6 | "engine": {
7 | "name": "frpt-message",
8 | "version": 4,
9 | "title": "(メッセージ)",
10 | "subtitle": "(メッセージ用)",
11 | "author": "名無し。"
12 | },
13 | "title": "FriedPotatoへようこそ!",
14 | "artists": "https://github.com/sevenc-nanashi/FriedPotato にてソースが公開されています!",
15 | "author": "by 名無し。",
16 | "cover": {
17 | "type": "LevelCover",
18 | "url": ""
19 | },
20 | "bgm": {
21 | "type": "LevelBgm",
22 | "url": ""
23 | },
24 | "data": {
25 | "type": "LevelData",
26 | "url": ""
27 | }
28 | },
29 | {
30 | "name": "frpt-system",
31 | "version": 1,
32 | "rating": 0,
33 | "engine": {
34 | "name": "frpt-message",
35 | "version": 4,
36 | "title": "(メッセージ)",
37 | "subtitle": "(メッセージ用)",
38 | "author": "名無し。"
39 | },
40 | "title": "下の「もっと」ボタンで譜面を表示します。",
41 | "artists": " ",
42 | "author": "by 名無し。",
43 | "cover": {
44 | "type": "LevelCover",
45 | "url": ""
46 | },
47 | "bgm": {
48 | "type": "LevelBgm",
49 | "url": ""
50 | },
51 | "data": {
52 | "type": "LevelData",
53 | "url": ""
54 | }
55 | }
56 | ]
57 |
--------------------------------------------------------------------------------
/info_down.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "frpt-system",
3 | "version": 1,
4 | "rating": 0,
5 | "engine": {
6 | "name": "frpt-message",
7 | "version": 4,
8 | "title": "-",
9 | "subtitle": "(メッセージ用)",
10 | "author": "名無し。"
11 | },
12 | "useSkin": {
13 | "useDefault": true
14 | },
15 | "useBackground": {
16 | "useDefault": true
17 | },
18 | "useEffect": {
19 | "useDefault": true
20 | },
21 | "useParticle": {
22 | "useDefault": true
23 | },
24 | "title": "SweetPotato is Down!",
25 | "artists": "SweetPotato is down so you can't play for now. Please try again later.",
26 | "author": "by 名無し。",
27 | "cover": {
28 | "type": "LevelCover",
29 | "url": ""
30 | },
31 | "bgm": {
32 | "type": "LevelBgm",
33 | "url": "/repo/welcome.mp3"
34 | },
35 | "data": {
36 | "type": "LevelData",
37 | "url": "/repo/data.gz"
38 | }
39 | }
--------------------------------------------------------------------------------
/info_official.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "name": "frpt-system",
4 | "version": 1,
5 | "rating": 0,
6 | "engine": {
7 | "name": "frpt-message",
8 | "version": 4,
9 | "title": "(メッセージ)",
10 | "subtitle": "(メッセージ用)",
11 | "author": "名無し。"
12 | },
13 | "useSkin": {
14 | "useDefault": true
15 | },
16 | "useBackground": {
17 | "useDefault": true
18 | },
19 | "useEffect": {
20 | "useDefault": true
21 | },
22 | "useParticle": {
23 | "useDefault": true
24 | },
25 | "title": "FriedPotatoへようこそ!",
26 | "artists": "本家譜面サーバーを表示しています。「もっと」で譜面を表示します。",
27 | "author": "by 名無し。",
28 | "cover": {
29 | "type": "LevelCover",
30 | "url": ""
31 | },
32 | "bgm": {
33 | "type": "LevelBgm",
34 | "url": "/repo/welcome.mp3"
35 | },
36 | "data": {
37 | "type": "LevelData",
38 | "url": "/repo/data.gz"
39 | }
40 | }
41 | ]
42 |
--------------------------------------------------------------------------------
/info_old.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "name": "frpt-system",
4 | "version": 1,
5 | "rating": 0,
6 | "engine": {
7 | "name": "frpt-message",
8 | "version": 4,
9 | "title": "(メッセージ)",
10 | "subtitle": "(メッセージ用)",
11 | "author": "名無し。"
12 | },
13 | "useSkin": {
14 | "useDefault": true
15 | },
16 | "useBackground": {
17 | "useDefault": true
18 | },
19 | "useEffect": {
20 | "useDefault": true
21 | },
22 | "useParticle": {
23 | "useDefault": true
24 | },
25 | "title": "FriedPotatoは0.5.13以下に対応していません。",
26 | "artists": "Sonolusを0.6.0にアップデートして下さい。",
27 | "author": "by 名無し。",
28 | "cover": {
29 | "type": "LevelCover",
30 | "url": ""
31 | },
32 | "bgm": {
33 | "type": "LevelBgm",
34 | "url": "/repo/welcome.mp3"
35 | },
36 | "data": {
37 | "type": "LevelData",
38 | "url": "/repo/data.gz"
39 | }
40 | }
41 | ]
42 |
--------------------------------------------------------------------------------
/info_system.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "frpt-system",
3 | "version": 1,
4 | "rating": 0,
5 | "engine": {
6 | "name": "frpt-message",
7 | "version": 4,
8 | "title": "(メッセージ)",
9 | "subtitle": "(メッセージ用)",
10 | "author": "名無し。"
11 | },
12 | "title": "このレベルはプレイできません!",
13 | "artists": "このレベルはメッセージの表示をするためだけに作られています。",
14 | "author": "by 名無し。",
15 | "cover": {
16 | "type": "LevelCover",
17 | "url": ""
18 | },
19 | "bgm": {
20 | "type": "LevelBgm",
21 | "url": ""
22 | },
23 | "data": {
24 | "type": "LevelData",
25 | "url": ""
26 | }
27 | }
--------------------------------------------------------------------------------
/info_test.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "name": "frpt-system",
4 | "version": 1,
5 | "rating": 0,
6 | "useSkin": {
7 | "useDefault": true
8 | },
9 | "useBackground": {
10 | "useDefault": true
11 | },
12 | "useEffect": {
13 | "useDefault": true
14 | },
15 | "useParticle": {
16 | "useDefault": true
17 | },
18 | "title": "FriedPotatoへようこそ!",
19 | "artists": "/tests/{test_id}のサーバーを表示しています。「もっと」で譜面を表示します。",
20 | "author": "by 名無し。",
21 | "cover": {
22 | "type": "LevelCover",
23 | "url": ""
24 | },
25 | "bgm": {
26 | "type": "LevelBgm",
27 | "url": "/repo/welcome.mp3"
28 | },
29 | "data": {
30 | "type": "LevelData",
31 | "url": "/repo/data.gz"
32 | }
33 | }
34 | ]
--------------------------------------------------------------------------------
/launch.bat:
--------------------------------------------------------------------------------
1 | @echo off
2 | call bundle exec ruby main.rb
3 | pause
4 |
--------------------------------------------------------------------------------
/main.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 | require "json"
3 | require "uri"
4 | require "sinatra"
5 | require "fileutils"
6 | require "sinatra/json"
7 | require "http"
8 | require "digest"
9 | require "zlib"
10 | require "yaml"
11 | require "socket"
12 | require "open3"
13 | require "sinatra/namespace"
14 | # require "async"
15 |
16 | class Config
17 | KEYS = {
18 | engine_path: {
19 | description: "エンジンのパス。",
20 | default: "../sonolus-pjsekai-engine"
21 | },
22 | trace_enabled: {
23 | description: "TRACEノーツを使用するかどうか。32分スライドが置き換わります。",
24 | default: false
25 | },
26 | background_engine: {
27 | description: "背景生成のエンジン。dxruby、web、noneのいずれかを指定して下さい。",
28 | default: "web"
29 | },
30 | sonolus_5_10: {
31 | description: "Sonolusがv0.5.10かどうか。",
32 | default: true
33 | },
34 | port: {
35 | description: "ポート番号。",
36 | default: 4567
37 | },
38 | public: {
39 | description: "公開サーバーモードで起動するか。",
40 | default: false
41 | }
42 | }.freeze
43 |
44 | def initialize
45 | load
46 | end
47 |
48 | def method_missing(name, value = nil)
49 | if name.end_with?("=")
50 | raise "unknown key: #{name}" unless KEYS.key?(name.to_s.chop.to_sym)
51 | @config[name.to_s.chop.to_sym] = value
52 | save
53 | else
54 | load
55 | raise "unknown key: #{name}" if @config.key?(name).nil?
56 | @config[name]
57 | end
58 | end
59 |
60 | def respond_to_missing?(name, _priv)
61 | if name.end_with?("=")
62 | @config.key?(name.to_s.chop.to_sym)
63 | else
64 | @config.key?(name)
65 | end
66 | end
67 |
68 | def save
69 | File.open("./config.yml", "w") do |y|
70 | @config.each do |key, value|
71 | y.puts "# #{KEYS[key][:description]}"
72 | y.puts "# デフォルト: #{KEYS[key][:default]}"
73 | y.puts "#{key}: #{value}"
74 | y.puts
75 | end
76 | end
77 | end
78 |
79 | def load
80 | @config =
81 | if File.exist?("#{__dir__}/config.yml")
82 | YAML.safe_load(
83 | File.read("#{__dir__}/config.yml"),
84 | symbolize_names: true
85 | )
86 | else
87 | {}
88 | end
89 | save_flag = false
90 | (KEYS.keys - @config.keys).each do |key|
91 | save_flag = true
92 | @config[key] = KEYS[key][:default]
93 | end
94 | save if save_flag
95 | end
96 | end
97 |
98 | class Integer
99 | def zfill(length)
100 | self.to_s.rjust(length, "0")
101 | end
102 | end
103 |
104 | $config = Config.new
105 |
106 | def load_datafile(path)
107 | data = File.read("./data/#{path}.json")
108 | data.gsub!(/"!import:(.+?)"/) do |_match|
109 | File.read("./data/#{Regexp.last_match[1]}")
110 | end
111 | JSON.parse(data, symbolize_names: true)
112 | end
113 |
114 | def load_engine
115 | base = load_datafile("engines/frpt-pjsekai.extended")
116 | if Dir.exist?($config.engine_path)
117 | base[:data][:url] = "/engine/data"
118 | base[:data][:hash] = get_file_hash("#{$config.engine_path}/dist/EngineData")
119 | base[:configuration][:url] = "/engine/configuration"
120 | base[:configuration][:hash] = get_file_hash(
121 | "#{$config.engine_path}/dist/EngineConfiguration"
122 | )
123 | end
124 | base[:skin][:name] = "frpt-pjsekai.extended"
125 | base[:skin][:data][:url] = "/skin/data"
126 | skin_data_hash = get_file_hash("./skin/data.json")
127 | if File.exist?("./dist/skin/#{skin_data_hash}.gz")
128 | base[:skin][:data][:hash] = get_file_hash(
129 | "./dist/skin/#{skin_data_hash}.gz"
130 | )
131 | else
132 | base[:skin][:data].delete(:hash)
133 | end
134 |
135 | base
136 | end
137 |
138 | if ENV["DOCKER"] == "true"
139 | $config.public = true
140 | $config.engine_path = "./engine"
141 | $config.background_engine = "web"
142 | end
143 | require "sinatra/reloader" unless $config.public
144 | $hash_cache = {}
145 | BLOCKED_AUTHORS =
146 | (
147 | if File.exist?("./blocked_authors.txt")
148 | File.read("./blocked_authors.txt").split("\n")
149 | else
150 | []
151 | end
152 | )
153 | OFFICIAL_CHARACTERS =
154 | JSON
155 | .parse(
156 | HTTP
157 | .get(
158 | "https://sekai-world.github.io/sekai-master-db-diff/gameCharacters.json"
159 | )
160 | .body
161 | .to_s,
162 | symbolize_names: true
163 | )
164 | .to_h { |c| [c[:id], c] }
165 | OUTSIDE_CHARACTERS =
166 | JSON
167 | .parse(
168 | HTTP
169 | .get(
170 | "https://sekai-world.github.io/sekai-master-db-diff/outsideCharacters.json"
171 | )
172 | .body
173 | .to_s,
174 | symbolize_names: true
175 | )
176 | .to_h { |c| [c[:id], c] }
177 |
178 | def get_file_hash(path)
179 | $hash_cache[path] ||= Digest::SHA256.file(path).hexdigest
180 | end
181 | BANNER = {
182 | url: "/repo/banner.png",
183 | hash: get_file_hash("./public/repo/banner.png"),
184 | type: :ServerBanner
185 | }.freeze
186 |
187 | def format_artist(level, locale)
188 | if locale == "ja"
189 | "作詞:#{level[:lyricist]} 作曲:#{level[:composer]} 編曲:#{level[:arranger]}"
190 | else
191 | "Lyrics: #{level[:lyricist]} Music: #{level[:composer]} Arrangement: #{level[:arranger]}"
192 | end
193 | end
194 |
195 | def modify_level!(level, extra, server)
196 | name = level[:name]
197 | modifier = ""
198 | extra_name = nil
199 | if server == :official
200 | level[:name] = level[:name].sub("pjsekai-", "frptp-level-")
201 |
202 | bgm_id = level[:bgm][:url].split("/")[-1].split(".")[0]
203 | elsif level[:engine][:name] == "wbp-pjsekai"
204 | level[:name] = "frpt-#{level[:name]}"
205 |
206 | level[:useBackground] = {}
207 | level[:data][:url] = "/convert/#{level[:name]}"
208 | extra_name = " @ Converter"
209 |
210 | level[:data].delete(:hash)
211 | level[:data][:hash] = get_file_hash(
212 | "./convert/#{level[:name]}.gz"
213 | ) if File.exist?("./convert/#{level[:name]}.gz")
214 | elsif level[:engine][:name] == "psekai"
215 | level[:name] = "frpt-#{level[:name]}"
216 |
217 | level[:data][:url] = "/convert/l_#{level[:name]}"
218 | level[:name] = "l_#{level[:name]}"
219 | extra_name = " @ Old server"
220 | else
221 | level[:data][:url] = "/modify/#{level[:name]}-#{level[:data][:hash]}"
222 | if File.exist?("dist/modify/#{level[:data][:hash]}.gz")
223 | level[:data][:hash] = get_file_hash(
224 | "dist/modify/#{level[:data][:hash]}.gz"
225 | )
226 | else
227 | level[:data].delete(:hash)
228 | end
229 | end
230 | # img_name = level[:name].dup
231 | if extra && server != :official
232 | # img_name += ".extra"
233 | modifier += "e"
234 | level[:title] += " (Extra)"
235 | level[:name] += ".extra"
236 | end
237 | level[:engine] = load_engine
238 | level[:engine][:title] += extra_name if extra_name
239 |
240 | if Dir.exist?("./overrides/#{name}")
241 | if File.exist?("./overrides/#{name}/thumbnail.png")
242 | level[:cover][:url] = "/overrides/#{name}/thumbnail.png"
243 | level[:cover][:hash] = get_file_hash("./overrides/#{name}/thumbnail.png")
244 | end
245 | if File.exist?("./overrides/#{name}/bgm.mp3")
246 | level[:bgm][:url] = "/overrides/#{name}/bgm.mp3"
247 | level[:bgm][:hash] = get_file_hash("./overrides/#{name}/bgm.mp3")
248 | end
249 | if File.exist?("./overrides/#{name}/data.json")
250 | json_hash = get_file_hash("./overrides/#{name}/data.json")
251 | Zlib::GzipWriter.open("./dist/data-overrides/#{json_hash}.gz") do |gz|
252 | gz.write(File.read("./overrides/#{name}/data.json"))
253 | end
254 | level[:data][:url] = "/data-overrides/#{json_hash}.gz"
255 | level[:data][:hash] = get_file_hash(
256 | "./dist/data-overrides/#{json_hash}.gz"
257 | )
258 | end
259 | end
260 |
261 | level[:useBackground][:useDefault] = false
262 | level[:useBackground][:item] = {
263 | name: "frpt-bg-#{level[:name]}",
264 | version: 2,
265 | title: level[:title],
266 | subtitle: "#{level[:artists]} / #{level[:author]}",
267 | thumbnail: {
268 | type: :BackgroundThumbnail,
269 | hash: level[:cover][:hash],
270 | url: level[:cover][:url]
271 | },
272 | data: {
273 | type: :BackgroundData,
274 | hash: get_file_hash("./public/repo/data.gz"),
275 | url: "/repo/data.gz"
276 | },
277 | image: {
278 | type: :BackgroundImage,
279 | url:
280 | "/generate/#{level[:name].delete_suffix(".extra")}_#{level[:cover][:hash]}-#{modifier}"
281 | },
282 | configuration: {
283 | type: :BackgroundConfiguration,
284 | hash: get_file_hash("./public/repo/config.gz"),
285 | url: "/repo/config.gz"
286 | }
287 | }
288 |
289 | if server == :official
290 | name = level[:name].split("-")[2]
291 | level[:useBackground][:item][:image][:url] = level[:useBackground][:item][
292 | :image
293 | ][
294 | :url
295 | ].sub("frptp-level-", "official--").sub("_", "_#{name}")
296 | if File.exist?("dist/bg/#{name}-#{modifier}.png")
297 | level[:useBackground][:item][:image][:hash] = get_file_hash(
298 | "dist/bg/#{name}-#{modifier}.png"
299 | )
300 | end
301 | else
302 | if File.exist?("dist/bg/#{level[:cover][:hash]}-#{modifier}.png")
303 | level[:useBackground][:item][:image][:hash] = get_file_hash(
304 | "dist/bg/#{level[:cover][:hash]}-#{modifier}.png"
305 | )
306 | end
307 | level[:name] = "frpt-#{level[:name]}"
308 | end
309 | end
310 |
311 | SEARCH_OPTION = [
312 | {
313 | name: "#KEYWORDS",
314 | placeholder: "#KEYWORDS",
315 | query: "keywords",
316 | type: "text"
317 | }
318 | ].freeze
319 |
320 | set :bind, "0.0.0.0"
321 | set :public_folder, "#{File.dirname(__FILE__)}/public"
322 |
323 | if ENV["RACK_ENV"] == "production"
324 | set :port, ENV.fetch("PORT", nil)
325 | set :environment, :production
326 | else
327 | set :port, $config.port
328 | set :environment, :development
329 | end
330 | set :show_exceptions, development?
331 | $level_base = JSON.parse(File.read("base.json"), symbolize_names: true)
332 |
333 | get "/" do
334 | erb :index
335 | end
336 |
337 | get %r{/levels/frpt-(.+)} do |name|
338 | level_resp =
339 | HTTP.get("https://servers-legacy.purplepalette.net/levels/#{name}")
340 | next erb :level_404 if level_resp.status != 200
341 | level = JSON.parse(level_resp.body, symbolize_names: true)[:item]
342 | erb :level, locals: { level: level }
343 | end
344 |
345 | get %r{/(backgrounds|effects|engines|particles|skins)/(.+)} do |type, name|
346 | redirect "sonolus://#{request.host}/#{type}/#{name}"
347 | end
348 |
349 | get "/jacket/:name" do
350 | HTTP
351 | .get(
352 | "https://servers-legacy.purplepalette.net/repository/#{params[:name]}/cover.png"
353 | )
354 | .body
355 | .to_s
356 | end
357 |
358 | get "/info" do
359 | resp = {
360 | levels: {
361 | items: JSON.parse(File.read("./info_old.json"), symbolize_names: true),
362 | search: {
363 | options: SEARCH_OPTION
364 | }
365 | },
366 | skins: {
367 | items: [],
368 | search: {
369 | }
370 | },
371 | engines: {
372 | items: [],
373 | search: {
374 | }
375 | },
376 | backgrounds: {
377 | items: [],
378 | search: {
379 | }
380 | },
381 | effects: {
382 | items: [],
383 | search: {
384 | }
385 | },
386 | particles: {
387 | items: [],
388 | search: {
389 | }
390 | }
391 | }
392 | if params["localization"] != "ja"
393 | l = resp[:levels][:items][0]
394 | l[:title] = "Sorry, but we don't support Sonolus under 0.6.0."
395 | l[:artists] = "Please update your Sonolus."
396 | end
397 | json resp
398 | end
399 |
400 | namespace "/sonolus" do
401 | get "/info" do
402 | resp = {
403 | title: "FriedPotato",
404 | banner: BANNER,
405 | levels: {
406 | items: JSON.parse(File.read("./info.json"), symbolize_names: true),
407 | search: {
408 | options: SEARCH_OPTION
409 | }
410 | },
411 | skins: {
412 | items: [
413 | {
414 | name: "frpt-system",
415 | title: "統計:背景画像数",
416 | subtitle: "#{Dir.glob("./dist/bg/*.png").size}枚"
417 | },
418 | {
419 | name: "frpt-system",
420 | title: "統計:変換された譜面数",
421 | subtitle: "#{Dir.glob("./dist/conv/*.gz").size}個"
422 | }
423 | ],
424 | search: {
425 | }
426 | },
427 | backgrounds: {
428 | items: [],
429 | search: {
430 | options: SEARCH_OPTION
431 | }
432 | },
433 | effects: {
434 | items: [],
435 | search: {
436 | }
437 | },
438 | particles: {
439 | items: [],
440 | search: {
441 | }
442 | },
443 | engines: {
444 | items: [],
445 | search: {
446 | }
447 | }
448 | }
449 | if params["localization"] != "ja"
450 | l = resp[:levels][:items][0]
451 | l[:title] = "Welcome to FriedPotato!"
452 | l[
453 | :artists
454 | ] = "The source is open at https://github.com/sevenc-nanashi/FriedPotato"
455 | l = resp[:levels][:items][1]
456 | l[:title] = "Tap [More] button below to browse levels..."
457 | end
458 | json resp
459 | end
460 |
461 | get "/backgrounds/list" do
462 | levels =
463 | JSON.parse(
464 | HTTP.get(
465 | "https://servers-legacy.purplepalette.net/levels/list?#{
466 | URI.encode_www_form(
467 | { keywords: params[:keywords], page: params[:page] }
468 | )
469 | }"
470 | ).body,
471 | symbolize_names: true
472 | )
473 | res = {
474 | pageCount: levels[:pageCount],
475 | items:
476 | levels[:items].map do |level|
477 | {
478 | name: "frpt-bg-#{level[:name]}",
479 | version: 2,
480 | title: level[:title],
481 | subtitle: "#{level[:artists]} / #{level[:author]}",
482 | thumbnail: {
483 | type: :BackgroundThumbnail,
484 | url:
485 | "https://servers-legacy.purplepalette.net#{level[:cover][:url]}"
486 | },
487 | data: {
488 | type: :BackgroundData,
489 | url: "/repo/data.gz"
490 | },
491 | image: {
492 | type: :BackgroundImage,
493 | url: "/generate/#{level[:name]}_#{level[:cover][:hash]}-"
494 | },
495 | configuration: {
496 | type: :BackgroundConfiguration,
497 | url: "/repo/config.gz"
498 | }
499 | }
500 | end,
501 | search: {
502 | options: SEARCH_OPTION
503 | }
504 | }
505 | json res
506 | end
507 |
508 | get %r{/backgrounds/frpt-bg-([^.]+)} do |name|
509 | level =
510 | JSON.parse(
511 | HTTP.get(
512 | "https://servers-legacy.purplepalette.net/levels/#{name}"
513 | ).body,
514 | symbolize_names: true
515 | )[
516 | :item
517 | ]
518 | json(
519 | {
520 | description: level[:description],
521 | recommended: [],
522 | item: {
523 | name: "frpt-bg-#{level[:name]}",
524 | version: 2,
525 | title: level[:title],
526 | subtitle: "#{level[:artists]} / #{level[:author]}",
527 | thumbnail: {
528 | type: :BackgroundThumbnail,
529 | url:
530 | "https://servers-legacy.purplepalette.net#{level[:cover][:url]}"
531 | },
532 | data: {
533 | type: :BackgroundData,
534 | url: "/repo/data.gz"
535 | },
536 | image: {
537 | type: :BackgroundImage,
538 | url: "/generate/#{level[:name]}_#{level[:cover][:hash]}-"
539 | },
540 | configuration: {
541 | type: :BackgroundConfiguration,
542 | url: "/repo/config.gz"
543 | }
544 | }
545 | }
546 | )
547 | end
548 |
549 | get "/levels/list" do
550 | response =
551 | HTTP.get(
552 | "https://servers-legacy.purplepalette.net/levels/list?#{
553 | +URI.encode_www_form(
554 | { keywords: params[:keywords], page: params[:page].to_i }
555 | ).gsub("+", "%20")
556 | }"
557 | )
558 | if response.status >= 500
559 | item = JSON.parse(File.read("./info_down.json"), symbolize_names: true)
560 | if params["localization"] == "ja"
561 | item[:title] = "SweetPotatoが落ちています!"
562 | item[:artists] = "現在プレイできません。後でもう一度お試し下さい。"
563 | end
564 | next json({ items: [item], search: { options: [] }, pageCount: 0 })
565 | end
566 | ppdata =
567 | JSON.parse(
568 | response.body.to_s.gsub(
569 | '"/',
570 | '"https://servers-legacy.purplepalette.net/'
571 | ),
572 | symbolize_names: true
573 | )
574 | if params[:keywords].nil? || params[:keywords].empty?
575 | if ppdata[:items].length.zero?
576 | levels =
577 | JSON.parse(
578 | HTTP
579 | .get(
580 | "https://raw.githubusercontent.com/PurplePalette/PurplePalette.github.io/0f37a15a672c95daae92f78953d59d05c3f01b5d/sonolus/levels/list"
581 | )
582 | .body
583 | .to_s
584 | .gsub('"/', '"https://PurplePalette.github.io/sonolus/'),
585 | symbolize_names: true
586 | )[
587 | :items
588 | ].map do |data|
589 | data[:data][:url] = "/local/#{data[:name]}/data.gz"
590 | data[:name] = "l_#{data[:name]}"
591 |
592 | data
593 | end
594 | ppdata[:items] = levels
595 | end
596 | ppdata[:pageCount] += 1
597 | end
598 | ppdata[:items].each { modify_level!(_1, false, :purplepalette) }
599 | ppdata[:items].reject! { BLOCKED_AUTHORS.include?(_1[:author]) }
600 | ppdata[:search] = { options: SEARCH_OPTION }
601 | json ppdata
602 | end
603 |
604 | get %r{/(effects|particles|engines|skins)/list} do |type|
605 | json(
606 | {
607 | pageCount: 1,
608 | search: {
609 | options: []
610 | },
611 | items:
612 | Dir
613 | .glob("./data/#{type}/*.json")
614 | .map { |f| load_datafile(f.sub("./data/", "").sub(".json", "")) }
615 | }
616 | )
617 | end
618 | get %r{/(effects|particles|engines|skins)/(.+)} do |type, name|
619 | data = load_datafile("#{type}/#{name}")
620 | json({ item: data, description: data[:description], recommended: [] })
621 | end
622 | end
623 | get "/tests/:test_id/sonolus/info" do |test_id|
624 | resp = {
625 | title: "FriedPotato Test: #{test_id}",
626 | banner: BANNER,
627 | levels: {
628 | items:
629 | JSON.parse(
630 | File.read("./info_test.json").sub("{test_id}", test_id),
631 | symbolize_names: true
632 | ),
633 | search: {
634 | options: SEARCH_OPTION
635 | }
636 | },
637 | skins: {
638 | items: [],
639 | search: {
640 | }
641 | },
642 | engines: {
643 | items: [],
644 | search: {
645 | }
646 | },
647 | backgrounds: {
648 | items: [],
649 | search: {
650 | }
651 | },
652 | effects: {
653 | items: [],
654 | search: {
655 | }
656 | },
657 | particles: {
658 | items: [],
659 | search: {
660 | }
661 | }
662 | }
663 | if params["localization"] != "ja"
664 | l = resp[:levels][:items][0]
665 | l[:title] = "Welcome to FriedPotato!"
666 | l[
667 | :artists
668 | ] = "You're on test server [#{test_id}]. Tap [More] button below to browse levels..."
669 | end
670 | json resp
671 | end
672 |
673 | get %r{(?:/tests/[^/]+|/official)?/sonolus/levels/frpt-system} do
674 | item = JSON.load_file("./info_system.json", symbolize_names: true)
675 | if params["localization"] != "ja"
676 | item[:title] = "This level is not playable!"
677 | item[
678 | :artists
679 | ] = "This level is only used to show message. Please go back and do something else."
680 | end
681 | json({ item: item })
682 | end
683 |
684 | get %r{(?:/tests/[^/]+|/pjsekai|/official)?/generate/(.+)_(.+)} do |name, key|
685 | modifier = key.split("-")[1] || ""
686 | unless File.exist?("dist/bg/#{key}.png")
687 | case $config.background_engine
688 | when "dxruby"
689 | $current = name
690 | eval File.read("./bg_gen/main.rb") # rubocop:disable Security/Eval
691 | when "web"
692 | name += "_#{key}" if name == "l"
693 | HTTP
694 | .post(
695 | "https://image-gen.sevenc7c.com/generate/#{name}?extra=#{modifier.include?("e")}"
696 | )
697 | .then do |res|
698 | if res.status == 200
699 | File.write("dist/bg/#{key}.png", res.body, mode: "wb")
700 | elsif modifier.include?("e")
701 | redirect "/repo/background-base-extra.png"
702 | else
703 | redirect "/repo/background-base.png"
704 | end
705 | end
706 | when "none"
707 | if modifier.include?("e")
708 | redirect "/repo/background-base-extra.png"
709 | else
710 | redirect "/repo/background-base.png"
711 | end
712 | end
713 | end
714 | send_file "dist/bg/#{key}.png"
715 | end
716 |
717 | get "/tests/:test_id/sonolus/levels/list" do |test_id|
718 | response =
719 | HTTP.get(
720 | "https://servers-legacy.purplepalette.net/tests/#{test_id}/levels/list?#{
721 | +URI.encode_www_form(
722 | { keywords: params[:keywords], page: params[:page].to_i }
723 | ).gsub("+", "%20")
724 | }"
725 | )
726 | if response.status >= 500
727 | item = JSON.parse(File.read("./info_down.json"), symbolize_names: true)
728 | if params["localization"] == "ja"
729 | item[:title] = "SweetPotatoが落ちています!"
730 | item[:artists] = "現在プレイできません。後でもう一度お試し下さい。"
731 | end
732 | next json({ items: [item], search: { options: [] }, pageCount: 0 })
733 | end
734 | ppdata =
735 | JSON.parse(
736 | response.body.to_s.gsub(
737 | '"/',
738 | '"https://servers-legacy.purplepalette.net/'
739 | ),
740 | symbolize_names: true
741 | )
742 | ppdata[:items].each { modify_level!(_1, false, :purplepalette) }
743 | ppdata[:search] = { options: SEARCH_OPTION }
744 | json ppdata
745 | end
746 |
747 | get %r{(?:/tests/[^/]+)?/sonolus/levels/frpt-([^.]+)(?:\.(.+))?} do |name, suffix|
748 | level_raw =
749 | if name.start_with?("l_")
750 | HTTP
751 | .get(
752 | "https://PurplePalette.github.io/sonolus/levels/#{name[2..].gsub(" ", "%20")}"
753 | )
754 | .body
755 | .to_s
756 | .gsub('"/', '"https://PurplePalette.github.io/sonolus/')
757 | else
758 | HTTP
759 | .get("https://servers-legacy.purplepalette.net/levels/#{name}")
760 | .body
761 | .to_s
762 | .gsub('"/', '"https://servers-legacy.purplepalette.net/')
763 | end
764 |
765 | level_hash = JSON.parse(level_raw, symbolize_names: true)
766 | level = level_hash[:item]
767 | level_hash[:description] ||= ""
768 | extra = true if level_hash[:description].include?("#extra") ||
769 | suffix == "extra"
770 | modify_level!(level, extra, :purplepalette)
771 | level_hash[:recommended] = [
772 | {
773 | name: extra ? level[:name][..-7] : "#{level[:name]}.extra",
774 | version: 2,
775 | title: extra ? "ExtraモードOFF" : "ExtraモードON",
776 | subtitle: "-",
777 | cover: {
778 | type: :LevelCover,
779 | hash: get_file_hash("./public/repo/extra_#{extra ? "off" : "on"}.png"),
780 | url: "/repo/extra_#{extra ? "off" : "on"}.png"
781 | },
782 | data: {
783 | type: :LevelData,
784 | url: "/repo/data.gz"
785 | },
786 | engine: {
787 | }
788 | }
789 | ]
790 | if File.exist?("dist/bg/#{level[:name]}.png") && !$config.public
791 | level_hash[:recommended] << {
792 | name: "#{level[:name]}.delete-cache",
793 | version: 2,
794 | title: "背景キャッシュを削除",
795 | subtitle: "-",
796 | cover: {
797 | type: :LevelCover,
798 | hash: get_file_hash("./public/repo/delete.png"),
799 | url: "/repo/delete.png"
800 | },
801 | data: {
802 | type: :LevelData,
803 | url: "/repo/data.gz"
804 | },
805 | engine: {
806 | }
807 | }
808 | end
809 | json level_hash
810 | end
811 |
812 | namespace %r{/(?:official|pjsekai)} do
813 | namespace "/sonolus" do
814 | get "/info" do
815 | resp = {
816 | title: "FriedPotato: Official Charts",
817 | banner: BANNER,
818 | levels: {
819 | items:
820 | JSON.parse(
821 | File.read("./info_official.json"),
822 | symbolize_names: true
823 | ),
824 | search: {
825 | options: SEARCH_OPTION
826 | }
827 | },
828 | skins: {
829 | items: [],
830 | search: {
831 | }
832 | },
833 | engines: {
834 | items: [],
835 | search: {
836 | }
837 | },
838 | backgrounds: {
839 | items: [],
840 | search: {
841 | }
842 | },
843 | effects: {
844 | items: [],
845 | search: {
846 | }
847 | },
848 | particles: {
849 | items: [],
850 | search: {
851 | }
852 | }
853 | }
854 | if params["localization"] != "ja"
855 | l = resp[:levels][:items][0]
856 | l[:title] = "Welcome to FriedPotato!"
857 | l[
858 | :artists
859 | ] = "You're on Official Charts server (aka. /pjsekai). Tap [More] button below to browse levels..."
860 | end
861 | json resp
862 | end
863 |
864 | get "/levels/list" do
865 | levels =
866 | JSON
867 | .parse(
868 | HTTP.get(
869 | "https://sekai-world.github.io/sekai-master-db-diff/musics.json"
870 | ),
871 | symbolize_names: true
872 | )
873 | .filter { |l| l[:publishedAt] < Time.now.to_i * 1000 }
874 | .filter do |l|
875 | if params[:keywords]
876 | params[:keywords].split.all? do |k|
877 | l[:title].downcase.include?(k.downcase)
878 | end
879 | else
880 | true
881 | end
882 | end
883 | .sort_by { |l| -l[:publishedAt] }
884 | vocals =
885 | JSON.parse(
886 | HTTP.get(
887 | "https://sekai-world.github.io/sekai-master-db-diff/musicVocals.json"
888 | ),
889 | symbolize_names: true
890 | )
891 | json(
892 | {
893 | items:
894 | levels[20 * params[:page].to_i, 20]&.map do |level|
895 | level_vocals = vocals.filter { |v| v[:musicId] == level[:id] }
896 | preview_id = level_vocals.first[:assetbundleName]
897 | {
898 | name: "group-#{level[:id]}",
899 | version: 1,
900 | rating: level_vocals.length,
901 | title: level[:title],
902 | artists: format_artist(level, params[:localization]),
903 | cover: {
904 | type: :LevelCover,
905 | url:
906 | "https://storage.sekai.best/sekai-assets/music/jacket/jacket_s_#{level[:id].zfill(3)}_rip/jacket_s_#{level[:id].zfill(3)}.png"
907 | },
908 | engine: {
909 | name: "category",
910 | title: "-"
911 | },
912 | preview: {
913 | type: :LevelPreview,
914 | url:
915 | "https://storage.sekai.best/sekai-assets/music/short/#{preview_id}_rip/#{preview_id}_short.mp3"
916 | }
917 | }
918 | end || [],
919 | pageCount: (levels.length / 20.0).ceil,
920 | search: {
921 | options: SEARCH_OPTION
922 | }
923 | }
924 | )
925 | end
926 |
927 | get %r{/levels/frptp-level-([^.]+)(\.flick)?} do |name, flick|
928 | data =
929 | JSON.parse(
930 | HTTP
931 | .get(
932 | "https://servers.sonolus.com/pjsekai/sonolus/levels/pjsekai-#{name}"
933 | )
934 | .body
935 | .to_s
936 | .gsub('"/', '"https://servers.sonolus.com/pjsekai/'),
937 | symbolize_names: true
938 | )
939 | modify_level!(data[:item], !flick.nil?, :official)
940 | level = data[:item]
941 | if flick
942 | level[:title] += " (Flick)"
943 | level[:name] += ".flick"
944 | level[:useBackground][:item][:image][:url] += "e"
945 | level[:useBackground][:item][:image][:hash] = (
946 | if File.exist?("./dist/bg/#{name}.flick.png")
947 | get_file_hash("./dist/bg/#{name}.flick.png")
948 | else
949 | ""
950 | end
951 | )
952 | level[:data][:url] = "/flick/#{name}"
953 | level[:data][:hash] = (
954 | if File.exist?("./dist/modify/#{level[:data][:hash]}-f.gz")
955 | get_file_hash("./dist/modify/#{level[:data][:hash]}-f.gz")
956 | else
957 | ""
958 | end
959 | )
960 | end
961 | data[:recommended] = [
962 | {
963 | name:
964 | (flick ? level[:name][..-7] : "#{level[:name]}.flick").sub(
965 | "pjsekai-",
966 | ""
967 | ),
968 | version: 2,
969 | title:
970 | (
971 | if params[:localization] == "ja"
972 | (flick ? "FlickモードOFF" : "FlickモードON")
973 | else
974 | (flick ? "Disable Flick" : "Enable Flick")
975 | end
976 | ),
977 | subtitle: "-",
978 | cover: {
979 | type: :LevelCover,
980 | hash:
981 | get_file_hash("./public/repo/flick_#{flick ? "off" : "on"}.png"),
982 | url: "/repo/flick_#{flick ? "off" : "on"}.png"
983 | },
984 | engine: {
985 | }
986 | }
987 | ]
988 | json data
989 | end
990 | get %r{/levels/group-([^.]+)} do |name|
991 | level =
992 | JSON
993 | .parse(
994 | HTTP.get(
995 | "https://sekai-world.github.io/sekai-master-db-diff/musics.json"
996 | ),
997 | symbolize_names: true
998 | )
999 | .find { |l| l[:id] == name.to_i }
1000 | vocals =
1001 | JSON
1002 | .parse(
1003 | HTTP.get(
1004 | "https://sekai-world.github.io/sekai-master-db-diff/musicVocals.json"
1005 | ),
1006 | symbolize_names: true
1007 | )
1008 | .filter { |v| v[:musicId] == name.to_i }
1009 | difficulties =
1010 | JSON
1011 | .parse(
1012 | HTTP.get(
1013 | "https://sekai-world.github.io/sekai-master-db-diff/musicDifficulties.json"
1014 | ),
1015 | symbolize_names: true
1016 | )
1017 | .filter { |d| d[:musicId] == name.to_i }
1018 | preview_id = vocals.first[:assetbundleName]
1019 | engine = load_engine
1020 |
1021 | levels =
1022 | vocals
1023 | .map do |vocal|
1024 | difficulties.reverse.map do |difficulty|
1025 | {
1026 | name:
1027 | "frptp-level-#{level[:id]}-#{vocal[:id]}-#{difficulty[:musicDifficulty]}",
1028 | version: 1,
1029 | rating: difficulty[:playLevel],
1030 | engine: engine,
1031 | useSkin: {
1032 | useDefault: true
1033 | },
1034 | useBackground: {
1035 | useDefault: true
1036 | },
1037 | useEffect: {
1038 | useDefault: true
1039 | },
1040 | useParticle: {
1041 | useDefault: true
1042 | },
1043 | title:
1044 | "#{difficulty[:musicDifficulty].capitalize} - #{vocal[:caption]}",
1045 | artists:
1046 | vocal[:characters]
1047 | .map do |c|
1048 | if c[:characterType] == "game_character"
1049 | OFFICIAL_CHARACTERS[c[:characterId]]
1050 | else
1051 | OUTSIDE_CHARACTERS[c[:characterId]]
1052 | end
1053 | end
1054 | .map do |c|
1055 | c[:name] || "#{c[:firstName]} #{c[:givenName]}".strip
1056 | end
1057 | .join(" & ")
1058 | .then { |s| s.empty? ? "-" : s },
1059 | author: "",
1060 | cover: {
1061 | type: :LevelCover,
1062 | url:
1063 | "https://storage.sekai.best/sekai-assets/music/jacket/jacket_s_#{level[:id].zfill(3)}_rip/jacket_s_#{level[:id].zfill(3)}.png"
1064 | },
1065 | bgm: {
1066 | type: :LevelBgm,
1067 | url:
1068 | "https://storage.sekai.best/sekai-assets/music/long/#{preview_id}_rip/#{preview_id}.mp3"
1069 | },
1070 | preview: {
1071 | type: :LevelPreview,
1072 | url:
1073 | "https://storage.sekai.best/sekai-assets/music/short/#{preview_id}_rip/#{preview_id}_short.mp3"
1074 | },
1075 | data: {
1076 | type: :LevelData,
1077 | url:
1078 | "/levels/#{level[:id]}.#{vocal[:id]}.#{difficulty[:musicDifficulty]}/data?0.1.0-beta.11"
1079 | }
1080 | }.tap { |l| modify_level!(l, false, :official) }
1081 | end
1082 | end
1083 | .flatten
1084 | json(
1085 | {
1086 | item: {
1087 | name: "group-#{level[:id]}",
1088 | version: 1,
1089 | rating: vocals.length,
1090 | title: level[:title],
1091 | artists: format_artist(level, params[:localization]),
1092 | author: "-",
1093 | cover: {
1094 | type: :LevelCover,
1095 | hash: get_file_hash("./public/repo/folder.png"),
1096 | url: "/repo/folder.png"
1097 | },
1098 | engine: {
1099 | name: "category",
1100 | title: "-"
1101 | },
1102 | preview: {
1103 | type: :LevelPreview,
1104 | url:
1105 | "https://storage.sekai.best/sekai-assets/music/short/#{preview_id}_rip/#{preview_id}_short.mp3"
1106 | }
1107 | },
1108 | description: (params[:localization] == "ja" ? <<~DESC : <<~DESC),
1109 | 作詞:#{level[:lyricist]}
1110 | 作曲:#{level[:composer]}
1111 | 編曲:#{level[:arranger]}
1112 |
1113 | 追加日時:#{Time.at(level[:publishedAt] / 1000, in: "+09:00").strftime("%Y/%m/%d %H:%M:%S")}
1114 | DESC
1115 | Lyrics: #{level[:lyricist]}
1116 | Music: #{level[:composer]}
1117 | Arrangement: #{level[:arranger]}
1118 |
1119 | Published at: #{Time.at(level[:publishedAt] / 1000, in: "+00:00").strftime("%m/%d/%Y %H:%M:%S")} (UTC)
1120 | DESC
1121 | recommended: levels
1122 | }
1123 | )
1124 | end
1125 | end
1126 |
1127 | get "/flick/:name" do |name|
1128 | if File.exist?("./dist/modify/#{name}-f.gz")
1129 | next send_file("./dist/modify/#{name}-f.gz")
1130 | end
1131 | raw =
1132 | HTTP.get(
1133 | "https://servers.sonolus.com/pjsekai/sonolus/levels/pjsekai-#{name}/data"
1134 | ).body
1135 | gzreader = Zlib::GzipReader.new(StringIO.new(raw.to_s))
1136 | level_data = JSON.parse(gzreader.read, symbolize_names: true)
1137 | level_data[:entities].each do |entity|
1138 | next if entity[:archetype] < 3
1139 | val = entity[:data][:values]
1140 |
1141 | if [3, 7, 10, 14].include? entity[:archetype]
1142 | entity[:archetype] += 1
1143 | entity[:data][:values][3] = -1
1144 | end
1145 | end
1146 | Zlib::GzipWriter.open("./dist/modify/#{name}-f.gz") do |gz|
1147 | gz.write(JSON.dump(level_data))
1148 | end
1149 | send_file("./dist/modify/#{name}-f.gz")
1150 | end
1151 |
1152 | get %r{/generate/(.+?)(\.flick)?} do |name, flick|
1153 | unless File.exist?("dist/bg/#{name}#{flick}.png")
1154 | case $config.background_engine
1155 | when "dxruby"
1156 | eval File.read("./bg_gen/main.rb") # rubocop:disable Security/Eval
1157 | when "web"
1158 | res =
1159 | HTTP.post(
1160 | "https://image-gen.sevenc7c.com/generate/official-#{name}.png?extra=#{!flick.nil?}"
1161 | )
1162 | if res.status == 200
1163 | File.write("dist/bg/#{name}#{flick}.png", res.body, mode: "wb")
1164 | else
1165 | redirect "/repo/background-base.png"
1166 | end
1167 | when "none"
1168 | redirect "/repo/background-base.png"
1169 | end
1170 | end
1171 | send_file("./dist/bg/#{name}#{flick}.png")
1172 | end
1173 |
1174 | get %r{/(.+)} do |path|
1175 | call env.merge("PATH_INFO" => "/#{path}")
1176 | end
1177 | end
1178 |
1179 | get %r{(?:/tests/.+)?/convert/(.+)} do |name|
1180 | if File.exist?("./dist/conv/#{name}.gz")
1181 | next send_file "./dist/conv/#{name}.gz"
1182 | end
1183 | raw =
1184 | if name.start_with?("l_")
1185 | HTTP.get(
1186 | "https://PurplePalette.github.io/sonolus/repository/levels/#{name[2..]}/level"
1187 | )
1188 | else
1189 | HTTP.get(
1190 | "https://servers-legacy.purplepalette.net/repository/#{name}/data.gz"
1191 | ).body
1192 | end
1193 | gzreader = Zlib::GzipReader.new(StringIO.new(raw))
1194 | json = gzreader.read
1195 | gzreader.close
1196 | s = JSON.parse(json, symbolize_names: true)
1197 | base = { entities: [{ archetype: 0 }, { archetype: 1 }, { archetype: 2 }] }
1198 | slide_positions = {}
1199 | last_entities = []
1200 | s[:entities][2..]
1201 | .each
1202 | .with_index(2) do |e, i|
1203 | val = e[:data][:values]
1204 | case e[:archetype]
1205 | when 2
1206 | width = (val[2] + 1) / 2.0
1207 | base[:entities] << {
1208 | archetype: 3,
1209 | data: {
1210 | index: 0,
1211 | values: [val[0], val[1] + width, width, 0]
1212 | }
1213 | }
1214 | when 4
1215 | width = (val[3] + 1) / 2.0
1216 | base[:entities] << {
1217 | archetype: 4,
1218 | data: {
1219 | index: 0,
1220 | values: [val[1], val[2] + width, width, -1]
1221 | }
1222 | }
1223 | when 3
1224 | width = (val[2] + 1) / 2.0
1225 | slide_positions[i] = val
1226 | base[:entities] << {
1227 | archetype: 5,
1228 | data: {
1229 | index: 0,
1230 | values: [val[0], val[1] + width, width, 0]
1231 | }
1232 | }
1233 | when 5
1234 | slide_positions[i] = val
1235 | width = (val[3] + 1) / 2.0
1236 | base[:entities] << {
1237 | archetype: 6,
1238 | data: {
1239 | index: 0,
1240 | values: [val[1], val[2] + width, width]
1241 | }
1242 | }
1243 | when 6, 7
1244 | slide_positions[i] = val
1245 | width = (val[3] + 1) / 2.0
1246 | before = [val[1], val[2] + width, width]
1247 | cursor = val[0]
1248 | while data = slide_positions[cursor]
1249 | break if data.length == 3
1250 | cursor = data[0]
1251 | end
1252 | first_index = cursor
1253 | cursor = val[0]
1254 | while data = slide_positions[cursor]
1255 | data = [nil] + data if data.length == 3
1256 | width = (data[3] + 1) / 2.0
1257 | position = [data[1], data[2] + width, width]
1258 | last_entities << {
1259 | archetype: 9,
1260 | data: {
1261 | index: 0,
1262 | values: [position, before, -1, first_index + 1].flatten
1263 | }
1264 | }
1265 | before = position
1266 | cursor = data[0]
1267 | end
1268 | width = (val[3] + 1) / 2.0
1269 | base[:entities] << {
1270 | archetype: e[:archetype] + 1,
1271 | data: {
1272 | index: 0,
1273 | values: [val[1], val[2] + width, width, -1]
1274 | }
1275 | }
1276 | end
1277 | end
1278 | base[:entities] += last_entities
1279 | Zlib::GzipWriter.wrap(File.open("./dist/conv/#{name}.gz", "wb")) do |gz|
1280 | gz.write(base.to_json)
1281 | end
1282 | send_file "./dist/conv/#{name}.gz"
1283 | end
1284 |
1285 | get "overrides/:path" do |path|
1286 | send_file "./overrides/#{path}"
1287 | end
1288 |
1289 | namespace %r{(?:/tests/([^/]+)|/pjsekai|/official)?} do
1290 | get %r{/data-overrides/(.+)} do |_name, path|
1291 | send_file "./dist/data-overrides/#{path}"
1292 | end
1293 |
1294 | get %r{/skin/texture} do |_name|
1295 | send_file "./skin/texture.png"
1296 | end
1297 |
1298 | get %r{/skin/data} do |_name|
1299 | hash = get_file_hash("./skin/data.json")
1300 | unless File.exist?("./dist/skin/#{hash}.gz")
1301 | Zlib::GzipWriter.open("./dist/skin/#{hash}.gz") do |gz|
1302 | gz.write(File.read("./skin/data.json", mode: "rb"))
1303 | end
1304 | end
1305 | send_file "./dist/skin/#{hash}.gz"
1306 | end
1307 |
1308 | get %r{/engine/data} do |_name|
1309 | send_file "#{$config.engine_path}/dist/EngineData"
1310 | end
1311 |
1312 | get %r{/engine/configuration} do |_name|
1313 | send_file "#{$config.engine_path}/dist/EngineConfiguration"
1314 | end
1315 | end
1316 |
1317 | get %r{(?:/tests/([^/]+))?/modify/(.+)-(.+)} do |_name, level, hash|
1318 | cfg = [["t", $config.trace_enabled]].filter { |x| x[1] }.map { |x| x[0] }.join
1319 | key = "#{hash}-#{cfg}"
1320 | if File.exist?("./dist/modify/#{key}.gz")
1321 | next send_file "./dist/modify/#{key}.gz"
1322 | end
1323 | level_data = nil
1324 | entities = nil
1325 | loop do
1326 | raw =
1327 | HTTP.get(
1328 | "https://servers-legacy.purplepalette.net/repository/#{level}/data.gz"
1329 | ).body
1330 | gzreader = Zlib::GzipReader.new(StringIO.new(raw.to_s))
1331 | level_data = JSON.parse(gzreader.read, symbolize_names: true)
1332 | entities = level_data[:entities]
1333 | break if entities[3][:data][:values][0]
1334 | end
1335 | will_delete = []
1336 |
1337 | if $config.trace_enabled
1338 | entities
1339 | .filter { |e| e[:archetype] == 9 }
1340 | .each do |e|
1341 | unless (e[:data][:values][3] - e[:data][:values][0] - 0.0625 < 0.01) &&
1342 | (e[:data][:values][1..2] == e[:data][:values][4..5])
1343 | next
1344 | end
1345 | not_found = false
1346 | entities
1347 | .find do |e2|
1348 | e2[:archetype] == 5 and
1349 | e2[:data][:values] == e[:data][:values][0..2]
1350 | end
1351 | .tap do |e3|
1352 | index = entities.find_index(e3)
1353 | end_note =
1354 | entities.find do |e2|
1355 | [7, 8].include?(e2[:archetype]) and
1356 | e[:data][:values][4] == index
1357 | end
1358 | next not_found = true unless end_note
1359 | if end_note[:archetype] == 7
1360 | e3[:archetype] = 18
1361 | else
1362 | e3[:archetype] = 19
1363 | e3[:data][:values][3] = end_note[:data][:values][3]
1364 | end
1365 | will_delete << end_note
1366 | end
1367 |
1368 | will_delete << e unless not_found
1369 | end
1370 | wd_index = will_delete.filter_map { |e| entities.find_index(e) }
1371 | will_delete.each { |e| entities.delete(e) }
1372 | entities
1373 | .filter { |e| [7, 9].include?(e[:archetype]) }
1374 | .each do |e|
1375 | e[:data][:values][-1] -= wd_index
1376 | .filter { |i| i < e[:data][:values][-1] }
1377 | .length
1378 | end
1379 | end
1380 | Zlib::GzipWriter.wrap(File.open("./dist/modify/#{key}.gz", "wb")) do |gz|
1381 | gz.write(level_data.to_json)
1382 | end
1383 | send_file "./dist/modify/#{key}.gz"
1384 | end
1385 |
1386 | get %r{/(?:tests/(?:[^/]+)|official)/(.+)} do |path|
1387 | redirect "/#{path}", 301
1388 | end
1389 |
1390 | unless $config.public
1391 | ip = Socket.ip_address_list.find(&:ipv4_private?).ip_address
1392 | puts <<~MESSAGE.strip
1393 | \e[91m+---------------------------------------------+\e[m
1394 | \e[91m| FriedPotatoへようこそ! |\e[m
1395 | \e[91m+---------------------------------------------+\e[m
1396 |
1397 | Sonolusを開き、サーバーのURLに以下を入力して下さい:
1398 | \e[97mhttp://#{ip}:#{$config.port}\e[m
1399 | テストサーバーの場合は以下のURLを入力して下さい:
1400 | \e[97mhttp://#{ip}:#{$config.port}/tests/\e[m<テストサーバーID>
1401 |
1402 |
1403 | \e[97mCtrl+C\e[m を押すと終了します。
1404 |
1405 | Created by \e[96m名無し。(@sevenc-nanashi)\e[m
1406 | MESSAGE
1407 | puts
1408 | end
1409 |
--------------------------------------------------------------------------------
/overrides/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sevenc-nanashi/FriedPotato/05893682caa4ee83877710e56c9ab97925e4487b/overrides/.gitkeep
--------------------------------------------------------------------------------
/public/repo/EffectAudio.zip:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sevenc-nanashi/FriedPotato/05893682caa4ee83877710e56c9ab97925e4487b/public/repo/EffectAudio.zip
--------------------------------------------------------------------------------
/public/repo/EffectData.gz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sevenc-nanashi/FriedPotato/05893682caa4ee83877710e56c9ab97925e4487b/public/repo/EffectData.gz
--------------------------------------------------------------------------------
/public/repo/background-base-extra.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sevenc-nanashi/FriedPotato/05893682caa4ee83877710e56c9ab97925e4487b/public/repo/background-base-extra.png
--------------------------------------------------------------------------------
/public/repo/background-base.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sevenc-nanashi/FriedPotato/05893682caa4ee83877710e56c9ab97925e4487b/public/repo/background-base.png
--------------------------------------------------------------------------------
/public/repo/banner.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sevenc-nanashi/FriedPotato/05893682caa4ee83877710e56c9ab97925e4487b/public/repo/banner.png
--------------------------------------------------------------------------------
/public/repo/config.gz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sevenc-nanashi/FriedPotato/05893682caa4ee83877710e56c9ab97925e4487b/public/repo/config.gz
--------------------------------------------------------------------------------
/public/repo/data.gz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sevenc-nanashi/FriedPotato/05893682caa4ee83877710e56c9ab97925e4487b/public/repo/data.gz
--------------------------------------------------------------------------------
/public/repo/delete.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sevenc-nanashi/FriedPotato/05893682caa4ee83877710e56c9ab97925e4487b/public/repo/delete.png
--------------------------------------------------------------------------------
/public/repo/extra_off.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sevenc-nanashi/FriedPotato/05893682caa4ee83877710e56c9ab97925e4487b/public/repo/extra_off.png
--------------------------------------------------------------------------------
/public/repo/extra_on.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sevenc-nanashi/FriedPotato/05893682caa4ee83877710e56c9ab97925e4487b/public/repo/extra_on.png
--------------------------------------------------------------------------------
/public/repo/flick_off.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sevenc-nanashi/FriedPotato/05893682caa4ee83877710e56c9ab97925e4487b/public/repo/flick_off.png
--------------------------------------------------------------------------------
/public/repo/flick_on.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sevenc-nanashi/FriedPotato/05893682caa4ee83877710e56c9ab97925e4487b/public/repo/flick_on.png
--------------------------------------------------------------------------------
/public/repo/folder.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sevenc-nanashi/FriedPotato/05893682caa4ee83877710e56c9ab97925e4487b/public/repo/folder.png
--------------------------------------------------------------------------------
/public/repo/sounds/connect.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sevenc-nanashi/FriedPotato/05893682caa4ee83877710e56c9ab97925e4487b/public/repo/sounds/connect.mp3
--------------------------------------------------------------------------------
/public/repo/sounds/trace.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sevenc-nanashi/FriedPotato/05893682caa4ee83877710e56c9ab97925e4487b/public/repo/sounds/trace.mp3
--------------------------------------------------------------------------------
/public/repo/sounds/trace_crit.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sevenc-nanashi/FriedPotato/05893682caa4ee83877710e56c9ab97925e4487b/public/repo/sounds/trace_crit.mp3
--------------------------------------------------------------------------------
/public/repo/sounds/trace_flick.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sevenc-nanashi/FriedPotato/05893682caa4ee83877710e56c9ab97925e4487b/public/repo/sounds/trace_flick.mp3
--------------------------------------------------------------------------------
/public/repo/welcome.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sevenc-nanashi/FriedPotato/05893682caa4ee83877710e56c9ab97925e4487b/public/repo/welcome.mp3
--------------------------------------------------------------------------------
/setup.bat:
--------------------------------------------------------------------------------
1 | @echo off
2 | call bundle install
3 |
4 | pause
--------------------------------------------------------------------------------
/skin/texture.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sevenc-nanashi/FriedPotato/05893682caa4ee83877710e56c9ab97925e4487b/skin/texture.png
--------------------------------------------------------------------------------
/views/index.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | FriedPotato
8 |
9 |
10 |
11 |
12 |
13 |
173 |
174 |
175 |
176 |