mirror of
https://github.com/kichikuou/xsystem35-sdl2.git
synced 2026-10-08 22:49:49 +03:00
Move shell to separate repo
This commit is contained in:
+1
-4
@@ -2,9 +2,7 @@
|
||||
*.o
|
||||
*.lo
|
||||
*.la
|
||||
/docs/*.js
|
||||
/docs/*.js.mem
|
||||
/docs/*.wasm
|
||||
/out
|
||||
/config.h
|
||||
/config.log
|
||||
/config.status
|
||||
@@ -15,4 +13,3 @@
|
||||
Makefile
|
||||
.deps/
|
||||
.libs/
|
||||
node_modules/
|
||||
|
||||
+6
-3
@@ -14,12 +14,15 @@ LFLAGS += -s EXPORTED_FUNCTIONS="['_main','_ags_setAntialiasedStringMode', '_ald
|
||||
WASMFLAGS := -s WASM=1 -s ALLOW_MEMORY_GROWTH=1 -s "BINARYEN_TRAP_MODE='allow'"
|
||||
ASMJSFLAGS := -s TOTAL_MEMORY=100663296
|
||||
|
||||
all: docs/xsystem35.js docs/xsystem35.asm.js
|
||||
all: out/xsystem35.js out/xsystem35.asm.js
|
||||
|
||||
docs/xsystem35.js: emterpretify_whitelist src/xsystem35
|
||||
out:
|
||||
mkdir out
|
||||
|
||||
out/xsystem35.js: emterpretify_whitelist src/xsystem35 out
|
||||
$(CC) xsystem35.bc $(LFLAGS) $(WASMFLAGS) @emterpretify_whitelist -o $@
|
||||
|
||||
docs/xsystem35.asm.js: emterpretify_whitelist src/xsystem35
|
||||
out/xsystem35.asm.js: emterpretify_whitelist src/xsystem35 out
|
||||
$(CC) xsystem35.bc $(LFLAGS) $(ASMJSFLAGS) @emterpretify_whitelist -o $@
|
||||
|
||||
emterpretify_whitelist:
|
||||
|
||||
Binary file not shown.
@@ -1,190 +0,0 @@
|
||||
|
||||
Copyright (c) 2005-2008, The Android Open Source Project
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
-126
@@ -1,126 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>鬼畜王 on Web ヘルプ</title>
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/spectre.css/0.2.10/spectre.min.css">
|
||||
<style>
|
||||
|
||||
header {
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
top: 0;
|
||||
background-color: rgb(68,68,68);
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
.navbar {
|
||||
height: 42px;
|
||||
max-width: 640px;
|
||||
margin: 0 auto;
|
||||
padding: 0.5rem 0;
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
|
||||
.navbar-brand {
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
body > section {
|
||||
max-width: 640px;
|
||||
margin: 42px auto;
|
||||
padding: 2rem 1rem;
|
||||
}
|
||||
|
||||
details {
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
summary {
|
||||
font-size: 2.5rem;
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
|
||||
ol, ul {
|
||||
list-style-position: outside;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header id="toolbar">
|
||||
<div class="navbar">
|
||||
<section class="navbar-primary">
|
||||
<span class="navbar-brand text-ellipsis ml-5">鬼畜王 on Web ヘルプ</span>
|
||||
</section>
|
||||
</div>
|
||||
</header>
|
||||
<section>
|
||||
<details>
|
||||
<summary>このサイトについて</summary>
|
||||
<p>アリスソフトのゲームエンジン System3.5 の有志によるフリー実装である xsystem35 を JavaScript / WebAssembly に移植して、<a href="http://www.alicesoft.com/annai.html#free">配布フリー宣言</a> に基づいて配布されているゲームのいくつかをブラウザ上で遊べるようにしたものです。素晴らしいゲームがせっかくフリーで提供されているのに、遊ぶためのハードルが高い(仮想CDソフトが必要だったり)という状況を改善できれば、という試みです。</p>
|
||||
<p>以下の環境で動きます。</p>
|
||||
<ul>
|
||||
<li>Google Chrome (PC / Android)</li>
|
||||
<li>Mozilla Firefox</li>
|
||||
<li>Safari (<a href="#bug" onclick="bug.parentElement.open = true;">iOSでは一部動作に制限があります。</a>)</li>
|
||||
<li>Microsoft Edge</li>
|
||||
<li>Opera</li>
|
||||
</ul>
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>ゲームを起動する</summary>
|
||||
<p>このサイト自体は実行環境のみ提供するもので、ゲームのCDイメージを読み込んで動作します。</p>
|
||||
<ol>
|
||||
<li>以下のどちらかのサイトで、鬼畜王ランスのCDイメージをダウンロードします。
|
||||
<ul>
|
||||
<li><a href="http://retropc.net/alice/">アリスソフト アーカイブズ</a></li>
|
||||
<li><a href="http://www.geocities.jp/webmst_kitana/alicefree/index.html">アリスソフト『配布フリー宣言』対象ソフト配布</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>ダウンロードしたファイルはRAR形式の圧縮ファイルなので、展開してCD-ROMイメージを取り出します。Windowsなら <a href="http://www.forest.impress.co.jp/library/software/lhaplus/">LhaPlus</a>、Macなら <a href="https://itunes.apple.com/jp/app/the-unarchiver/id425424353">The Unarchiver</a> で展開できます。ChromeOSは標準機能でRARを展開できます。</li>
|
||||
<li>展開したフォルダにある <code>KICHIKUOU.img</code> と <code>KICHIKUOU.cue</code> (または <code>DISC.img</code> と <code>DISC.cue</code>) を、鬼畜王 on Web のページにドラッグ&ドロップするか、ページにある「ファイルを選択」ボタンを押して選択します。</li>
|
||||
<li>ゲームが始まります。セーブデータはブラウザ内に保存されるので、次回ページを開いたときは再度CDイメージを読み込ませればセーブしたところから再開できます。</li>
|
||||
</ol>
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>操作方法</summary>
|
||||
<p>スマートフォン / タブレットでは2本指でタッチすると右クリック扱いになります。</p>
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>設定</summary>
|
||||
<p>画面上部の歯車アイコンをクリックすると設定ウィンドウが表示されます。</p>
|
||||
<h5>表示</h5>
|
||||
<dl>
|
||||
<dt>拡大率</dt>
|
||||
<dd>ゲーム画面の拡大率を設定します。「自動」にするとウィンドウの大きさに合わせます。<dd>
|
||||
<dt>拡大時に補間しない</dt>
|
||||
<dd>Google ChromeとFirefoxでのみ有効です。通常は色を補間してなめらかに画面を拡大しますが、これをオンにすると単純に拡大します。ドット絵の感じを楽しみたいとき有効にするといいでしょう。</dd>
|
||||
<dt>文字をなめらかに表示する</dt>
|
||||
<dd>アンチエイリアスをかけて文字表示を綺麗にします。設定を変えた場合、すでに表示されている文字はそのままで次回の文字描画から反映されます。</dd>
|
||||
</dl>
|
||||
<h5>セーブデータ</h5>
|
||||
<p>ブラウザ内に保存されているセーブデータをZIPファイルに保存したり、ZIPファイルから復元できます。セーブデータをバックアップしたり、他のPCに移行する場合に使います。</p>
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary id="bug">バグ・制限</summary>
|
||||
<h5>iOS</h5>
|
||||
<p>iOSではiCloud Driveに展開済みのCDイメージを入れておけばSafariから読み込んで遊べますが、以下のような制限があります。</p>
|
||||
<ul>
|
||||
<li>しばらく遊んでいるとCDイメージの読み込みに失敗するようになり、BGMが再生できなくなります (おそらくSafariのバグ)。エラーが出たときは通知メッセージ上の「imgファイルの再読み込み」をタップして <code>KICHIKUOU.img</code> を再度選択してください。</li>
|
||||
<li>ボリューム調整は本体ボタンからしかできません。ページ上ではミュートのみ可能です。</li>
|
||||
</ul>
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>さいごに</summary>
|
||||
<p>ソースコードは以下にあります。
|
||||
<a href="https://github.com/kichikuou/xsystem35-sdl2/">https://github.com/kichikuou/xsystem35-sdl2/</a></p>
|
||||
<p>バグ報告、要望、質問、連絡、苦情などは以下のメールアドレスまでお願いします。
|
||||
<a href="mailto:KichikuouChrome@gmail.com">KichikuouChrome@gmail.com</a></p>
|
||||
</details>
|
||||
</section>
|
||||
</body>
|
||||
-208
@@ -1,208 +0,0 @@
|
||||
body {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
header {
|
||||
background-color: rgb(68,68,68);
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
@media all and (max-height: 480px) {
|
||||
#toolbar-handler {
|
||||
position: absolute;
|
||||
right: 0px;
|
||||
top: 0px;
|
||||
width: 40px;
|
||||
height: 10px;
|
||||
background-color: rgba(68,68,68,0.7);
|
||||
}
|
||||
|
||||
.toolbar-handler-icon {
|
||||
width: 5px;
|
||||
height: 5px;
|
||||
margin: auto;
|
||||
border: solid #ccc;
|
||||
border-width: 2px 2px 0 0;
|
||||
-webkit-transform: rotate(135deg);
|
||||
transform: rotate(135deg);
|
||||
}
|
||||
|
||||
header.closeable {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
transition: top 0.4s ease;
|
||||
background-color: rgba(68,68,68,0.85);
|
||||
}
|
||||
|
||||
header.closeable.closed {
|
||||
top: -42px;
|
||||
}
|
||||
}
|
||||
|
||||
@media not all and (max-height: 480px) {
|
||||
#toolbar-handler {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#toolbar-close-button {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.navbar {
|
||||
max-width: 640px;
|
||||
margin: 0 auto;
|
||||
padding: 0.5rem 0;
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
|
||||
.navbar .navbar-primary {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.navbar-brand {
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
.navbar .navbar-section {
|
||||
flex: 0;
|
||||
}
|
||||
|
||||
.navbar .btn-link {
|
||||
font-size: large;
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
.navbar .btn.btn-link:focus,
|
||||
.navbar .btn.btn-link:hover,
|
||||
.navbar .btn.btn-link:active,
|
||||
.navbar .btn.btn-link.active {
|
||||
color: #eee;
|
||||
}
|
||||
|
||||
.before-game-start .hidden-until-game-start {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.contents {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
#loader {
|
||||
max-width: 640px;
|
||||
margin: 2ex auto 0;
|
||||
}
|
||||
|
||||
#loader ul {
|
||||
list-style-type: none;
|
||||
}
|
||||
#loader li {
|
||||
color: #5764c6;
|
||||
}
|
||||
#loader li:before {
|
||||
font-family: FontAwesome;
|
||||
content: "\f15b";
|
||||
padding-right: 0.5em;
|
||||
}
|
||||
#loader li.notready {
|
||||
color: #808080;
|
||||
}
|
||||
#loader li.notready:after {
|
||||
content: " (未選択)";
|
||||
}
|
||||
|
||||
#unsupported {
|
||||
max-width: 640px;
|
||||
margin: 2ex auto 0;
|
||||
color: red;
|
||||
}
|
||||
|
||||
.bgblack-fade {
|
||||
background-color: black;
|
||||
transition-property: background-color;
|
||||
transition-duration: 1s;
|
||||
transition-timing-function: linear;
|
||||
}
|
||||
|
||||
#xsystem35 {
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
#xsystem35.fit {
|
||||
margin: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
/* the canvas *must not* have any border or padding, or mouse coords will be wrong */
|
||||
canvas.emscripten {
|
||||
border: 0px none;
|
||||
}
|
||||
|
||||
@media all and (min-aspect-ratio: 640/400) {
|
||||
.fit canvas.emscripten {
|
||||
height: 100%;
|
||||
max-height: 100vh;
|
||||
margin: auto;
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
@media not all and (min-aspect-ratio: 640/400) {
|
||||
#xsystem35.fit {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.fit canvas.emscripten {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.pixelated {
|
||||
image-rendering: pixelated;
|
||||
image-rendering: -moz-crisp-edges;
|
||||
}
|
||||
|
||||
#volume-control {
|
||||
float: right;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
#volume-control-icon {
|
||||
font-size: 20px;
|
||||
width: 26px;
|
||||
padding: 5px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
#volume-control-slider {
|
||||
width: 60px;
|
||||
height: 30px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.settings section > div {
|
||||
padding-left: 2rem;
|
||||
}
|
||||
|
||||
.toast-container {
|
||||
position: fixed;
|
||||
bottom: 30px;
|
||||
left: 30px;
|
||||
width: 400px;
|
||||
z-index: 500;
|
||||
}
|
||||
|
||||
.toast {
|
||||
-webkit-animation: slide-down .2s ease 1;
|
||||
animation: slide-down .2s ease 1;
|
||||
margin: 5px;
|
||||
}
|
||||
@@ -1,128 +0,0 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<title>鬼畜王 on Web</title>
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/spectre.css/0.2.10/spectre.min.css">
|
||||
<link rel="stylesheet" href="http://maxcdn.bootstrapcdn.com/font-awesome/4.3.0/css/font-awesome.min.css">
|
||||
<link rel="stylesheet" href="style.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div id="toolbar-handler" class="tooltip tooltip-bottom" data-tooltip="メニュー" hidden>
|
||||
<div class="toolbar-handler-icon"></div>
|
||||
</div>
|
||||
<header id="toolbar" class="before-game-start">
|
||||
<div class="navbar">
|
||||
<section class="navbar-primary">
|
||||
<span class="navbar-brand text-ellipsis ml-5">鬼畜王 on Web</span>
|
||||
</section>
|
||||
<section class="navbar-section">
|
||||
<div id="volume-control" class="mr-10 hidden-until-game-start">
|
||||
<i id="volume-control-icon" class="fa fa-volume-up tooltip tooltip-bottom" data-tooltip="ミュート"></i>
|
||||
<input id="volume-control-slider" type="range" class="tooltip tooltip-bottom" data-tooltip="音量">
|
||||
</div>
|
||||
|
||||
<button id="screenshot-button" class="btn btn-link tooltip tooltip-bottom mr-10 hidden-until-game-start" data-tooltip="スクリーンショット"><i class="fa fa-camera"></i></button>
|
||||
<button id="settings-button" class="btn btn-link tooltip tooltip-bottom mr-10" data-tooltip="設定"><i class="fa fa-cog"></i></button>
|
||||
<a class="btn btn-link tooltip tooltip-bottom" target="_blank" href="help.html" data-tooltip="ヘルプ"><i class="fa fa-question-circle mr-10"></i></a>
|
||||
<button id="toolbar-close-button" class="btn btn-link"><i class="fa fa-angle-up"></i></button>
|
||||
</section>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="contents">
|
||||
|
||||
<div id="loader">
|
||||
<p>アリスソフトの <a href="http://www.alicesoft.com/annai.html#free">配布フリー宣言</a> に基づいて配布されている18禁ゲーム「鬼畜王ランス」をWebブラウザ上で遊べるようにしたものです。
|
||||
<a target="_blank" href="help.html">遊び方はこちら。</a></p>
|
||||
<p><strong>(注意) 18歳未満の人は利用できません。</strong></p>
|
||||
<p>以下のファイルをドラッグ&ドロップするか、「ファイルを選択」ボタンを押して選択してください。</p>
|
||||
<ul>
|
||||
<li id="imgReady" class="notready">KICHIKUOU.img</li>
|
||||
<li id="cueReady" class="notready">KICHIKUOU.cue</li>
|
||||
</ul>
|
||||
<input type="file" id="fileselect" class="form-input" multiple/>
|
||||
</div>
|
||||
|
||||
<div id="xsystem35" hidden>
|
||||
<audio id="audio"></audio>
|
||||
<div class="emscripten" id="status"></div>
|
||||
<canvas class="emscripten" id="canvas" oncontextmenu="event.preventDefault()"></canvas>
|
||||
</div>
|
||||
|
||||
<div id="unsupported" hidden>
|
||||
このブラウザでは利用できません。
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Settings -->
|
||||
<div id="settings-modal" class="modal">
|
||||
<div class="modal-overlay"></div>
|
||||
<div class="modal-container">
|
||||
<div class="modal-header">
|
||||
<div class="modal-title">設定</div>
|
||||
</div>
|
||||
<div class="modal-body settings">
|
||||
<section>
|
||||
<h5>表示</h5>
|
||||
<div class="form-group">
|
||||
<span class="mr-5">拡大率:</span>
|
||||
<select id="zoom" class="form-select">
|
||||
<option value="1">100%</option>
|
||||
<option value="1.25">125%</option>
|
||||
<option value="1.5">150%</option>
|
||||
<option value="2">200%</option>
|
||||
<option value="3">300%</option>
|
||||
<option value="fit">自動</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label for="pixelate" class="form-checkbox">
|
||||
<input type="checkbox" id="pixelate">
|
||||
<i class="form-icon"></i>拡大時に補間しない
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<label for="antialias" class="form-checkbox">
|
||||
<input type="checkbox" id="antialias" checked>
|
||||
<i class="form-icon"></i>文字をなめらかに表示する
|
||||
</label>
|
||||
</div>
|
||||
</section>
|
||||
<div class="divider"></div>
|
||||
<section>
|
||||
<h5>セーブデータ</h5>
|
||||
<div>
|
||||
<button id="downloadSaveData" class="btn mr-10" disabled>ZIPに保存</button>
|
||||
<button id="uploadSaveData" class="btn">ZIPから復元</button>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button id="settings-close" class="btn btn-primary">OK</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="toast-container"></div>
|
||||
<template id="cdda-error">
|
||||
BGMの再生に失敗しました。
|
||||
<button class="btn btn-link cdda-reload-button">imgファイルを再読み込み</button>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
var Module = {};
|
||||
</script>
|
||||
<script src="shell.js"></script>
|
||||
<script src="//cdnjs.cloudflare.com/ajax/libs/jszip/3.1.3/jszip.min.js" async></script>
|
||||
<script>
|
||||
if (xsystem35.shell === undefined) {
|
||||
loader.hidden = true;
|
||||
unsupported.hidden = false;
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
-271
@@ -1,271 +0,0 @@
|
||||
/// <reference path="util.ts" />
|
||||
/// <reference path="volume.ts" />
|
||||
|
||||
namespace xsystem35 {
|
||||
declare var webkitAudioContext: any;
|
||||
export class AudioManager {
|
||||
private context: AudioContext;
|
||||
private masterGain: GainNode;
|
||||
private slots: PCMSound[];
|
||||
private bufCache: AudioBuffer[];
|
||||
private isSafari: boolean;
|
||||
|
||||
constructor(volumeControl: VolumeControl) {
|
||||
if (typeof (AudioContext) !== 'undefined') {
|
||||
this.context = new AudioContext();
|
||||
} else if (typeof (webkitAudioContext) !== 'undefined') {
|
||||
this.context = new webkitAudioContext();
|
||||
this.isSafari = true;
|
||||
this.removeUserGestureRestriction();
|
||||
}
|
||||
this.masterGain = this.context.createGain();
|
||||
this.masterGain.connect(this.context.destination);
|
||||
this.slots = [];
|
||||
this.bufCache = [];
|
||||
volumeControl.addEventListener(this.onVolumeChanged.bind(this));
|
||||
this.masterGain.gain.value = volumeControl.volume();
|
||||
document.addEventListener('visibilitychange', this.onVisibilityChange.bind(this));
|
||||
}
|
||||
|
||||
private removeUserGestureRestriction() {
|
||||
let hanlder = () => {
|
||||
let src = this.context.createBufferSource();
|
||||
src.buffer = this.context.createBuffer(1, 1, 22050);
|
||||
src.connect(this.context.destination);
|
||||
src.start();
|
||||
console.log('AudioContext unlocked');
|
||||
window.removeEventListener('touchend', hanlder);
|
||||
};
|
||||
window.addEventListener('touchend', hanlder);
|
||||
}
|
||||
|
||||
private load(no: number): Promise<AudioBuffer> {
|
||||
let buf = this.getWave(no);
|
||||
if (!buf)
|
||||
return Promise.reject('Failed to open wave ' + no);
|
||||
|
||||
let decoded: Promise<AudioBuffer>;
|
||||
if (this.isSafari) {
|
||||
decoded = new Promise((resolve, reject) => {
|
||||
this.context.decodeAudioData(buf, resolve, reject);
|
||||
});
|
||||
} else {
|
||||
decoded = this.context.decodeAudioData(buf);
|
||||
}
|
||||
return decoded.then((audioBuf) => {
|
||||
this.bufCache[no] = audioBuf;
|
||||
return audioBuf;
|
||||
});
|
||||
}
|
||||
|
||||
private getWave(no: number): ArrayBuffer {
|
||||
let dfile = _ald_getdata(2 /* DRIFILE_WAVE */, no - 1);
|
||||
if (!dfile)
|
||||
return null;
|
||||
let ptr = Module.getValue(dfile + 8, '*');
|
||||
let size = Module.getValue(dfile, 'i32');
|
||||
let buf = Module.HEAPU8.buffer.slice(ptr, ptr + size);
|
||||
_ald_freedata(dfile);
|
||||
return buf;
|
||||
}
|
||||
|
||||
pcm_load(slot: number, no: number) {
|
||||
EmterpreterAsync.handle((resume) => {
|
||||
this.pcm_stop(slot);
|
||||
if (this.bufCache[no]) {
|
||||
this.slots[slot] = new PCMSoundSimple(this.masterGain, this.bufCache[no]);
|
||||
return resume();
|
||||
}
|
||||
this.load(no).then((audioBuf) => {
|
||||
this.slots[slot] = new PCMSoundSimple(this.masterGain, audioBuf);
|
||||
resume();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
pcm_load_mixlr(slot: number, noL: number, noR: number) {
|
||||
EmterpreterAsync.handle((resume) => {
|
||||
this.pcm_stop(slot);
|
||||
if (this.bufCache[noL] && this.bufCache[noR]) {
|
||||
this.slots[slot] = new PCMSoundMixLR(this.masterGain, this.bufCache[noL], this.bufCache[noR]);
|
||||
return resume();
|
||||
}
|
||||
let ps: [Promise<AudioBuffer>, Promise<AudioBuffer>] = [
|
||||
this.bufCache[noL] ? Promise.resolve(this.bufCache[noL]) : this.load(noL),
|
||||
this.bufCache[noR] ? Promise.resolve(this.bufCache[noR]) : this.load(noR),
|
||||
];
|
||||
Promise.all(ps).then((bufs) => {
|
||||
this.slots[slot] = new PCMSoundMixLR(this.masterGain, bufs[0], bufs[1]);
|
||||
resume();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
pcm_start(slot: number, loop: number): number {
|
||||
if (this.slots[slot]) {
|
||||
this.slots[slot].start(loop);
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
pcm_stop(slot: number): number {
|
||||
if (!this.slots[slot])
|
||||
return 0;
|
||||
this.slots[slot].stop();
|
||||
this.slots[slot] = null;
|
||||
return 1;
|
||||
}
|
||||
|
||||
pcm_fadeout(slot: number, msec: number): number {
|
||||
if (!this.slots[slot])
|
||||
return 0;
|
||||
this.slots[slot].fadeout(msec);
|
||||
return 1;
|
||||
}
|
||||
|
||||
pcm_getpos(slot: number): number {
|
||||
if (!this.slots[slot])
|
||||
return 0;
|
||||
return this.slots[slot].getPosition() * 1000;
|
||||
}
|
||||
|
||||
pcm_setvol(slot: number, vol: number): number {
|
||||
if (!this.slots[slot])
|
||||
return 0;
|
||||
this.slots[slot].setGain(vol / 100);
|
||||
return 1;
|
||||
}
|
||||
|
||||
pcm_getwavelen(slot: number): number {
|
||||
if (!this.slots[slot])
|
||||
return 0;
|
||||
return this.slots[slot].duration * 1000;
|
||||
}
|
||||
|
||||
pcm_isplaying(slot: number): number {
|
||||
if (!this.slots[slot])
|
||||
return 0;
|
||||
return this.slots[slot].isPlaying() ? 1 : 0;
|
||||
}
|
||||
|
||||
private onVisibilityChange() {
|
||||
if (document.hidden)
|
||||
this.bufCache = [];
|
||||
}
|
||||
|
||||
private onVolumeChanged(evt: CustomEvent) {
|
||||
this.masterGain.gain.value = evt.detail;
|
||||
}
|
||||
}
|
||||
|
||||
abstract class PCMSound {
|
||||
protected context: AudioContext;
|
||||
protected gain: GainNode;
|
||||
protected startTime: number;
|
||||
|
||||
constructor(protected dst: AudioNode) {
|
||||
this.context = dst.context;
|
||||
this.gain = this.context.createGain();
|
||||
this.gain.connect(dst);
|
||||
}
|
||||
abstract start(loop: number): void;
|
||||
abstract stop(): void;
|
||||
setGain(gain: number) {
|
||||
this.gain.gain.value = gain;
|
||||
}
|
||||
fadeout(msec: number) {
|
||||
this.gain.gain.linearRampToValueAtTime(0, this.context.currentTime + msec / 1000);
|
||||
}
|
||||
getPosition(): number {
|
||||
if (!this.startTime)
|
||||
return 0;
|
||||
return this.context.currentTime - this.startTime;
|
||||
}
|
||||
isPlaying(): boolean {
|
||||
return !!this.startTime;
|
||||
}
|
||||
abstract get duration(): number;
|
||||
}
|
||||
|
||||
class PCMSoundSimple extends PCMSound {
|
||||
private node: AudioBufferSourceNode;
|
||||
|
||||
constructor(dst: AudioNode, buf: AudioBuffer) {
|
||||
super(dst);
|
||||
this.node = this.context.createBufferSource();
|
||||
this.node.buffer = buf;
|
||||
this.node.connect(this.gain);
|
||||
this.node.onended = this.onended.bind(this);
|
||||
}
|
||||
|
||||
start(loop: number) {
|
||||
if (loop === 0)
|
||||
this.node.loop = true;
|
||||
else if (loop !== 1)
|
||||
console.warn('Unsupported PCM loop count ' + loop);
|
||||
this.node.start();
|
||||
this.startTime = this.context.currentTime;
|
||||
}
|
||||
|
||||
stop() {
|
||||
if (this.startTime) {
|
||||
this.node.stop();
|
||||
this.startTime = null;
|
||||
}
|
||||
}
|
||||
|
||||
get duration() {
|
||||
return this.node.buffer.duration;
|
||||
}
|
||||
|
||||
private onended() {
|
||||
this.startTime = null;
|
||||
}
|
||||
}
|
||||
|
||||
class PCMSoundMixLR extends PCMSound {
|
||||
private lsrc: AudioBufferSourceNode;
|
||||
private rsrc: AudioBufferSourceNode;
|
||||
private endCount = 0;
|
||||
|
||||
constructor(dst: AudioNode, lbuf: AudioBuffer, rbuf: AudioBuffer) {
|
||||
super(dst);
|
||||
this.lsrc = this.context.createBufferSource();
|
||||
this.rsrc = this.context.createBufferSource();
|
||||
this.lsrc.buffer = lbuf;
|
||||
this.rsrc.buffer = rbuf;
|
||||
let merger = this.context.createChannelMerger(2);
|
||||
merger.connect(this.gain);
|
||||
this.lsrc.connect(merger, 0, 0);
|
||||
this.rsrc.connect(merger, 0, 1);
|
||||
this.lsrc.onended = this.rsrc.onended = this.onended.bind(this);
|
||||
}
|
||||
|
||||
start(loop: number) {
|
||||
if (loop !== 1)
|
||||
console.warn('PCMSoundMixLR: loop is not supported ' + loop);
|
||||
this.lsrc.start();
|
||||
this.rsrc.start();
|
||||
this.startTime = this.context.currentTime;
|
||||
}
|
||||
|
||||
stop() {
|
||||
if (this.startTime) {
|
||||
this.lsrc.stop();
|
||||
this.rsrc.stop();
|
||||
this.startTime = null;
|
||||
}
|
||||
}
|
||||
|
||||
get duration() {
|
||||
return Math.max(this.lsrc.buffer.duration, this.rsrc.buffer.duration);
|
||||
}
|
||||
|
||||
private onended() {
|
||||
this.endCount++;
|
||||
if (this.endCount === 2)
|
||||
this.startTime = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
-115
@@ -1,115 +0,0 @@
|
||||
/// <reference path="util.ts" />
|
||||
/// <reference path="volume.ts" />
|
||||
|
||||
namespace xsystem35 {
|
||||
export class CDPlayer {
|
||||
private audio = <HTMLAudioElement>$('audio');
|
||||
private blobCache: Blob[];
|
||||
private currentTrack: number;
|
||||
private isVolumeSupported: boolean;
|
||||
private unmute: () => void; // Non-null if emulating mute by pause
|
||||
|
||||
constructor(private imageLoader: ImageLoader, volumeControl: VolumeControl) {
|
||||
// Volume control of <audio> is not supported in iOS
|
||||
this.audio.volume = 0.5;
|
||||
this.isVolumeSupported = this.audio.volume !== 1;
|
||||
|
||||
this.blobCache = [];
|
||||
volumeControl.addEventListener(this.onVolumeChanged.bind(this));
|
||||
this.audio.volume = volumeControl.volume();
|
||||
this.audio.addEventListener('error', this.onAudioError.bind(this));
|
||||
this.removeUserGestureRestriction();
|
||||
if (!this.isVolumeSupported) {
|
||||
volumeControl.hideSlider();
|
||||
if (this.audio.volume === 0)
|
||||
this.unmute = () => {};
|
||||
}
|
||||
document.addEventListener('visibilitychange', this.onVisibilityChange.bind(this));
|
||||
}
|
||||
|
||||
play(track: number, loop: number) {
|
||||
this.currentTrack = track;
|
||||
if (this.unmute) {
|
||||
this.unmute = () => { this.play(track, loop); };
|
||||
return;
|
||||
}
|
||||
if (this.blobCache[track]) {
|
||||
this.startPlayback(this.blobCache[track], loop);
|
||||
return;
|
||||
}
|
||||
this.audio.currentTime = 0;
|
||||
this.imageLoader.getCDDA(track).then((blob) => {
|
||||
this.blobCache[track] = blob;
|
||||
this.startPlayback(blob, loop);
|
||||
});
|
||||
}
|
||||
|
||||
stop() {
|
||||
this.audio.pause();
|
||||
this.currentTrack = null;
|
||||
if (this.unmute)
|
||||
this.unmute = () => {};
|
||||
}
|
||||
|
||||
getPosition(): number {
|
||||
if (!this.currentTrack)
|
||||
return 0;
|
||||
let time = Math.round(this.audio.currentTime * 75);
|
||||
if (this.unmute || this.audio.error)
|
||||
time += 750; // unblock Kichikuou OP
|
||||
return this.currentTrack | time << 8;
|
||||
}
|
||||
|
||||
private startPlayback(blob: Blob, loop: number) {
|
||||
this.audio.setAttribute('src', URL.createObjectURL(blob));
|
||||
this.audio.loop = (loop !== 0);
|
||||
this.audio.load();
|
||||
this.audio.play();
|
||||
}
|
||||
|
||||
private onVisibilityChange() {
|
||||
if (document.hidden)
|
||||
this.blobCache = [];
|
||||
}
|
||||
|
||||
private onVolumeChanged(evt: CustomEvent) {
|
||||
if (this.isVolumeSupported) {
|
||||
this.audio.volume = evt.detail;
|
||||
return;
|
||||
}
|
||||
let muted = evt.detail === 0;
|
||||
if (!!this.unmute === muted)
|
||||
return;
|
||||
if (muted) {
|
||||
this.audio.pause();
|
||||
this.unmute = () => { this.audio.play(); };
|
||||
} else {
|
||||
let unmute = this.unmute;
|
||||
this.unmute = null;
|
||||
unmute();
|
||||
}
|
||||
}
|
||||
|
||||
private onAudioError(err: ErrorEvent) {
|
||||
let clone = document.importNode((<HTMLTemplateElement>$('#cdda-error')).content, true);
|
||||
let toast = xsystem35.shell.addToast(clone, 'danger');
|
||||
toast.querySelector('.cdda-reload-button').addEventListener('click', () => {
|
||||
this.imageLoader.reloadImage().then(() => {
|
||||
this.play(this.currentTrack, this.audio.loop ? 1 : 0);
|
||||
(<HTMLElement>toast.querySelector('.btn-clear')).click();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private removeUserGestureRestriction() {
|
||||
let hanlder = () => {
|
||||
if (!this.currentTrack) {
|
||||
this.audio.load();
|
||||
console.log('CDDA unlocked');
|
||||
}
|
||||
window.removeEventListener('touchend', hanlder);
|
||||
};
|
||||
window.addEventListener('touchend', hanlder);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,292 +0,0 @@
|
||||
/// <reference path="util.ts" />
|
||||
|
||||
namespace CDImage {
|
||||
export class ISO9660FileSystem {
|
||||
static async create(sectorReader: Reader): Promise<ISO9660FileSystem> {
|
||||
let pvd = new PVD(await sectorReader.readSector(0x10));
|
||||
return new ISO9660FileSystem(sectorReader, pvd);
|
||||
}
|
||||
|
||||
private constructor(private sectorReader: Reader, private pvd: PVD) {
|
||||
if (this.pvd.type !== 1)
|
||||
throw new Error('PVD not found');
|
||||
}
|
||||
|
||||
volumeLabel(): string {
|
||||
return this.pvd.volumeLabel();
|
||||
}
|
||||
|
||||
rootDir(): DirEnt {
|
||||
return this.pvd.rootDirEnt();
|
||||
}
|
||||
|
||||
async getDirEnt(name: string, parent: DirEnt): Promise<DirEnt> {
|
||||
name = name.toLowerCase();
|
||||
for (let e of await this.readDir(parent)) {
|
||||
if (e.name.toLowerCase() === name)
|
||||
return e;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async readDir(dirent: DirEnt): Promise<DirEnt[]> {
|
||||
let sector = dirent.sector;
|
||||
let position = 0;
|
||||
let length = dirent.size;
|
||||
let entries: DirEnt[] = [];
|
||||
let buf: ArrayBuffer;
|
||||
while (position < length) {
|
||||
if (position === 0)
|
||||
buf = await this.sectorReader.readSector(sector);
|
||||
let child = new DirEnt(buf, position);
|
||||
if (child.length === 0) {
|
||||
// Padded end of sector
|
||||
position = 2048;
|
||||
} else {
|
||||
entries.push(child);
|
||||
position += child.length;
|
||||
}
|
||||
if (position > 2048)
|
||||
throw new Error('dirent across sector boundary');
|
||||
if (position === 2048) {
|
||||
sector++;
|
||||
position = 0;
|
||||
length -= 2048;
|
||||
}
|
||||
}
|
||||
return entries;
|
||||
}
|
||||
|
||||
readFile(dirent: DirEnt): Promise<Uint8Array[]> {
|
||||
return this.sectorReader.readSequentialSectors(dirent.sector, dirent.size);
|
||||
}
|
||||
}
|
||||
|
||||
class PVD {
|
||||
private view: DataView;
|
||||
constructor(private buf: ArrayBuffer) {
|
||||
this.view = new DataView(buf);
|
||||
}
|
||||
get type(): number {
|
||||
return this.view.getUint8(0);
|
||||
}
|
||||
volumeLabel(): string {
|
||||
return SJISArrayToString(new DataView(this.buf, 40, 32)).trim();
|
||||
}
|
||||
rootDirEnt(): DirEnt {
|
||||
return new DirEnt(this.buf, 156);
|
||||
}
|
||||
}
|
||||
|
||||
export class DirEnt {
|
||||
private view: DataView;
|
||||
constructor(private buf: ArrayBuffer, private offset: number) {
|
||||
this.view = new DataView(buf, offset);
|
||||
}
|
||||
get length(): number {
|
||||
return this.view.getUint8(0);
|
||||
}
|
||||
get sector(): number {
|
||||
return this.view.getUint32(2, true);
|
||||
}
|
||||
get size(): number {
|
||||
return this.view.getUint32(10, true);
|
||||
}
|
||||
get isDirectory(): boolean {
|
||||
return (this.view.getUint8(25) & 2) !== 0;
|
||||
}
|
||||
get name(): string {
|
||||
let len = this.view.getUint8(32);
|
||||
return SJISArrayToString(new DataView(this.buf, this.offset + 33, len)).split(';')[0];
|
||||
}
|
||||
}
|
||||
|
||||
export interface Reader {
|
||||
readSector(sector: number): Promise<ArrayBuffer>;
|
||||
readSequentialSectors(startSector: number, length: number): Promise<Uint8Array[]>;
|
||||
maxTrack(): number;
|
||||
extractTrack(track: number): Promise<Blob>;
|
||||
resetImage(image: File): void;
|
||||
}
|
||||
|
||||
export async function createReader(img: File, cue: File) {
|
||||
if (cue.name.endsWith('.cue')) {
|
||||
let reader = new ImgCueReader(img);
|
||||
await reader.parseCue(cue);
|
||||
return reader;
|
||||
} else {
|
||||
let reader = new MdfMdsReader(img);
|
||||
await reader.parseMds(cue);
|
||||
return reader;
|
||||
}
|
||||
}
|
||||
|
||||
class ImageReaderBase {
|
||||
constructor(public image: File) { }
|
||||
|
||||
async readSequential(startOffset: number,
|
||||
bytesToRead: number,
|
||||
blockSize: number,
|
||||
sectorSize: number,
|
||||
sectorOffset: number): Promise<Uint8Array[]> {
|
||||
let sectors = Math.ceil(bytesToRead / sectorSize);
|
||||
let blob = this.image.slice(startOffset, startOffset + sectors * blockSize);
|
||||
let buf = await readFileAsArrayBuffer(blob);
|
||||
let bufs: Uint8Array[] = [];
|
||||
for (let i = 0; i < sectors; i++) {
|
||||
bufs.push(new Uint8Array(buf, i * blockSize + sectorOffset, Math.min(bytesToRead, sectorSize)));
|
||||
bytesToRead -= sectorSize;
|
||||
}
|
||||
return bufs;
|
||||
}
|
||||
|
||||
resetImage(image: File) {
|
||||
this.image = image;
|
||||
}
|
||||
}
|
||||
|
||||
class ImgCueReader extends ImageReaderBase implements Reader {
|
||||
private tracks: Array<{ type: string; index: string[]; }>;
|
||||
|
||||
constructor(img: File) {
|
||||
super(img);
|
||||
}
|
||||
|
||||
readSector(sector: number): Promise<ArrayBuffer> {
|
||||
let start = sector * 2352 + 16;
|
||||
let end = start + 2048;
|
||||
return readFileAsArrayBuffer(this.image.slice(start, end));
|
||||
}
|
||||
|
||||
readSequentialSectors(startSector: number, length: number): Promise<Uint8Array[]> {
|
||||
return this.readSequential(startSector * 2352, length, 2352, 2048, 16);
|
||||
}
|
||||
|
||||
async parseCue(cueFile: File) {
|
||||
let lines = (await readFileAsText(cueFile)).split('\n');
|
||||
this.tracks = [];
|
||||
let currentTrack: number = null;
|
||||
for (let line of lines) {
|
||||
let fields = line.trim().split(/\s+/);
|
||||
switch (fields[0]) {
|
||||
case 'TRACK':
|
||||
currentTrack = Number(fields[1]);
|
||||
this.tracks[currentTrack] = { type: fields[2], index: [] };
|
||||
break;
|
||||
case 'INDEX':
|
||||
if (currentTrack)
|
||||
this.tracks[currentTrack].index[Number(fields[1])] = fields[2];
|
||||
break;
|
||||
default:
|
||||
// Do nothing
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
maxTrack(): number {
|
||||
return this.tracks.length - 1;
|
||||
}
|
||||
|
||||
async extractTrack(track: number): Promise<Blob> {
|
||||
if (!this.tracks[track] || this.tracks[track].type !== 'AUDIO')
|
||||
return;
|
||||
|
||||
let start = this.indexToSector(this.tracks[track].index[1]) * 2352;
|
||||
let end: number;
|
||||
if (this.tracks[track + 1]) {
|
||||
let index = this.tracks[track + 1].index[0] || this.tracks[track + 1].index[1];
|
||||
end = this.indexToSector(index) * 2352;
|
||||
} else {
|
||||
end = this.image.size;
|
||||
}
|
||||
let size = end - start;
|
||||
return new Blob([createWaveHeader(size), this.image.slice(start, start + size)], { type: 'audio/wav' });
|
||||
}
|
||||
|
||||
private indexToSector(index: string): number {
|
||||
let msf = index.split(':').map(Number);
|
||||
return msf[0] * 60 * 75 + msf[1] * 75 + msf[2];
|
||||
}
|
||||
}
|
||||
|
||||
enum MdsTrackMode { Audio = 0xa9, Mode1 = 0xaa }
|
||||
|
||||
class MdfMdsReader extends ImageReaderBase implements Reader {
|
||||
private tracks: Array<{ mode: number; sectorSize: number; offset: number; sectors: number; }>;
|
||||
|
||||
constructor(mdf: File) {
|
||||
super(mdf);
|
||||
}
|
||||
|
||||
async parseMds(mdsFile: File) {
|
||||
let buf = await readFileAsArrayBuffer(mdsFile);
|
||||
|
||||
let signature = ASCIIArrayToString(new Uint8Array(buf, 0, 16));
|
||||
if (signature !== 'MEDIA DESCRIPTOR')
|
||||
throw new Error(mdsFile.name + ': not a mds file');
|
||||
|
||||
let header = new DataView(buf, 0, 0x70);
|
||||
let entries = header.getUint8(0x62);
|
||||
|
||||
this.tracks = [];
|
||||
for (let i = 0; i < entries; i++) {
|
||||
let trackData = new DataView(buf, 0x70 + i * 0x50, 0x50);
|
||||
let extraData = new DataView(buf, 0x70 + entries * 0x50 + i * 8, 8);
|
||||
let mode = trackData.getUint8(0x00);
|
||||
let track = trackData.getUint8(0x04);
|
||||
let sectorSize = trackData.getUint16(0x10, true);
|
||||
let offset = trackData.getUint32(0x28, true); // >4GB offset is not supported.
|
||||
let sectors = extraData.getUint32(0x4, true);
|
||||
if (track < 100)
|
||||
this.tracks[track] = { mode, sectorSize, offset, sectors };
|
||||
}
|
||||
if (this.tracks[1].mode !== MdsTrackMode.Mode1)
|
||||
throw new Error('track 1 is not mode1');
|
||||
}
|
||||
|
||||
readSector(sector: number): Promise<ArrayBuffer> {
|
||||
let start = sector * this.tracks[1].sectorSize + 16;
|
||||
let end = start + 2048;
|
||||
return readFileAsArrayBuffer(this.image.slice(start, end));
|
||||
}
|
||||
|
||||
readSequentialSectors(startSector: number, length: number): Promise<Uint8Array[]> {
|
||||
let track = this.tracks[1];
|
||||
return this.readSequential(track.offset + startSector * track.sectorSize,
|
||||
length, track.sectorSize, 2048, 16);
|
||||
}
|
||||
|
||||
maxTrack(): number {
|
||||
return this.tracks.length - 1;
|
||||
}
|
||||
|
||||
async extractTrack(track: number): Promise<Blob> {
|
||||
if (!this.tracks[track] || this.tracks[track].mode !== MdsTrackMode.Audio)
|
||||
return;
|
||||
|
||||
let size = this.tracks[track].sectors * 2352;
|
||||
let chunks = await this.readSequential(this.tracks[track].offset, size,
|
||||
this.tracks[track].sectorSize, 2352, 0);
|
||||
return new Blob([<any>createWaveHeader(size)].concat(chunks), { type: 'audio/wav' });
|
||||
}
|
||||
}
|
||||
|
||||
function createWaveHeader(size: number): ArrayBuffer {
|
||||
let buf = new ArrayBuffer(44);
|
||||
let view = new DataView(buf);
|
||||
view.setUint32(0, 0x52494646, false); // 'RIFF'
|
||||
view.setUint32(4, size + 36, true); // filesize - 8
|
||||
view.setUint32(8, 0x57415645, false); // 'WAVE'
|
||||
view.setUint32(12, 0x666D7420, false); // 'fmt '
|
||||
view.setUint32(16, 16, true); // size of fmt chunk
|
||||
view.setUint16(20, 1, true); // PCM format
|
||||
view.setUint16(22, 2, true); // stereo
|
||||
view.setUint32(24, 44100, true); // sampling rate
|
||||
view.setUint32(28, 176400, true); // bytes/sec
|
||||
view.setUint16(32, 4, true); // block size
|
||||
view.setUint16(34, 16, true); // bit/sample
|
||||
view.setUint32(36, 0x64617461, false); // 'data'
|
||||
view.setUint32(40, size, true); // data size
|
||||
return buf;
|
||||
}
|
||||
}
|
||||
-136
@@ -1,136 +0,0 @@
|
||||
/// <reference path="util.ts" />
|
||||
/// <reference path="cdimage.ts" />
|
||||
|
||||
namespace xsystem35 {
|
||||
export class ImageLoader {
|
||||
private imgFile: File;
|
||||
private cueFile: File;
|
||||
private imageReader: CDImage.Reader;
|
||||
|
||||
constructor(private shell: System35Shell) {
|
||||
$('#fileselect').addEventListener('change', this.handleFileSelect.bind(this), false);
|
||||
document.body.ondragover = this.handleDragOver.bind(this);
|
||||
document.body.ondrop = this.handleDrop.bind(this);
|
||||
}
|
||||
|
||||
getCDDA(track: number): Promise<Blob> {
|
||||
return this.imageReader.extractTrack(track);
|
||||
}
|
||||
|
||||
reloadImage(): Promise<any> {
|
||||
return openFileInput().then((file) => {
|
||||
this.imageReader.resetImage(file);
|
||||
});
|
||||
}
|
||||
|
||||
private handleFileSelect(evt: Event) {
|
||||
let input = <HTMLInputElement>evt.target;
|
||||
let files = input.files;
|
||||
for (let i = 0; i < files.length; i++)
|
||||
this.setFile(files[i]);
|
||||
input.value = '';
|
||||
}
|
||||
|
||||
private handleDragOver(evt: DragEvent) {
|
||||
evt.stopPropagation();
|
||||
evt.preventDefault();
|
||||
evt.dataTransfer.dropEffect = 'copy';
|
||||
}
|
||||
|
||||
private handleDrop(evt: DragEvent) {
|
||||
evt.stopPropagation();
|
||||
evt.preventDefault();
|
||||
let files = evt.dataTransfer.files;
|
||||
for (let i = 0; i < files.length; i++)
|
||||
this.setFile(files[i]);
|
||||
}
|
||||
|
||||
private async setFile(file: File) {
|
||||
let name = file.name.toLowerCase();
|
||||
if (name.endsWith('.img') || name.endsWith('.mdf')) {
|
||||
this.imgFile = file;
|
||||
$('#imgReady').classList.remove('notready');
|
||||
$('#imgReady').textContent = file.name;
|
||||
} else if (name.endsWith('.cue') || name.endsWith('.mds')) {
|
||||
this.cueFile = file;
|
||||
$('#cueReady').classList.remove('notready');
|
||||
$('#cueReady').textContent = file.name;
|
||||
}
|
||||
if (this.imgFile && this.cueFile) {
|
||||
this.imageReader = await CDImage.createReader(this.imgFile, this.cueFile);
|
||||
await xsystem35.fileSystemReady;
|
||||
this.startLoad();
|
||||
}
|
||||
}
|
||||
|
||||
private async extractFile(isofs: CDImage.ISO9660FileSystem, entry: CDImage.DirEnt,
|
||||
buf: Uint8Array, offset: number) {
|
||||
let ptr = 0;
|
||||
for (let chunk of await isofs.readFile(entry)) {
|
||||
buf.set(chunk, ptr + offset);
|
||||
ptr += chunk.byteLength;
|
||||
}
|
||||
if (ptr !== entry.size)
|
||||
throw new Error('expected ' + entry.size + ' bytes, but read ' + ptr + 'bytes');
|
||||
}
|
||||
|
||||
private async startLoad() {
|
||||
let isofs = await CDImage.ISO9660FileSystem.create(this.imageReader);
|
||||
// this.walk(isofs, isofs.rootDir(), '/');
|
||||
let gamedata = await isofs.getDirEnt('gamedata', isofs.rootDir());
|
||||
if (!gamedata) {
|
||||
this.setError('インストールできません。GAMEDATAフォルダが見つかりません。');
|
||||
return;
|
||||
}
|
||||
this.shell.loadStarted();
|
||||
|
||||
let aldFiles = [];
|
||||
for (let e of await isofs.readDir(gamedata)) {
|
||||
if (!e.name.toLowerCase().endsWith('.ald'))
|
||||
continue;
|
||||
// Store contents in the emscripten heap, so that it can be mmap-ed without copying
|
||||
let ptr = Module.getMemory(e.size);
|
||||
await this.extractFile(isofs, e, Module.HEAPU8, ptr);
|
||||
FS.writeFile(e.name, Module.HEAPU8.subarray(ptr, ptr + e.size), { encoding: 'binary', canOwn: true });
|
||||
aldFiles.push(e.name);
|
||||
}
|
||||
FS.writeFile('xsystem35.gr', this.createGr(aldFiles));
|
||||
FS.writeFile('.xsys35rc', xsystem35.xsys35rc);
|
||||
this.shell.loaded();
|
||||
}
|
||||
|
||||
private setError(msg: string) {
|
||||
console.log(msg);
|
||||
}
|
||||
|
||||
private createGr(files: string[]): string {
|
||||
const resourceType: { [ch: string]: string } = {
|
||||
d: 'Data', g: 'Graphics', m: 'Midi', r: 'Resource', s: 'Scenario', w: 'Wave',
|
||||
};
|
||||
let basename: string;
|
||||
let lines: string[] = [];
|
||||
for (let name of files) {
|
||||
let type = name.charAt(name.length - 6).toLowerCase();
|
||||
let id = name.charAt(name.length - 5);
|
||||
basename = name.slice(0, -6);
|
||||
lines.push(resourceType[type] + id.toUpperCase() + ' ' + name);
|
||||
}
|
||||
for (let i = 0; i < 26; i++) {
|
||||
let id = String.fromCharCode(65 + i);
|
||||
lines.push('Save' + id + ' save/' + basename + 's' + id.toLowerCase() + '.asd');
|
||||
}
|
||||
return lines.join('\n') + '\n';
|
||||
}
|
||||
|
||||
// For debug
|
||||
private async walk(isofs: CDImage.ISO9660FileSystem, dir: CDImage.DirEnt, dirname: string) {
|
||||
for (let e of await isofs.readDir(dir)) {
|
||||
if (e.name !== '\0' && e.name !== '\x01') {
|
||||
console.log(dirname + e.name);
|
||||
if (e.isDirectory)
|
||||
this.walk(isofs, e, dirname + e.name + '/');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
{
|
||||
"name": "xsystem35-shell",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"dependencies": {
|
||||
"@types/emscripten": "0.0.29",
|
||||
"@types/jszip": "0.0.32",
|
||||
"@types/text-encoding": "0.0.30"
|
||||
}
|
||||
}
|
||||
@@ -1,92 +0,0 @@
|
||||
/// <reference path="util.ts" />
|
||||
|
||||
namespace xsystem35 {
|
||||
export class Settings {
|
||||
private keyDownHandler: (ev: KeyboardEvent) => void;
|
||||
constructor() {
|
||||
$('#settings-button').addEventListener('click', this.openModal.bind(this));
|
||||
$('#settings-close').addEventListener('click', this.closeModal.bind(this));
|
||||
this.keyDownHandler = (ev: KeyboardEvent) => {
|
||||
if (ev.keyCode === 27) // escape
|
||||
this.closeModal();
|
||||
};
|
||||
$('.modal-overlay').addEventListener('click', this.closeModal.bind(this));
|
||||
|
||||
$('#downloadSaveData').addEventListener('click', this.downloadSaveData.bind(this));
|
||||
$('#uploadSaveData').addEventListener('click', this.uploadSaveData.bind(this));
|
||||
this.checkSaveData();
|
||||
}
|
||||
|
||||
private openModal() {
|
||||
$('#settings-modal').classList.add('active');
|
||||
document.addEventListener('keydown', this.keyDownHandler);
|
||||
}
|
||||
|
||||
private closeModal() {
|
||||
$('#settings-modal').classList.remove('active');
|
||||
document.removeEventListener('keydown', this.keyDownHandler);
|
||||
}
|
||||
|
||||
private checkSaveData() {
|
||||
xsystem35.saveDirReady.then(() => {
|
||||
if ((<string[]>FS.readdir('/save')).some((name) => name.toLowerCase().endsWith('.asd')))
|
||||
$('#downloadSaveData').removeAttribute('disabled');
|
||||
});
|
||||
}
|
||||
|
||||
private async downloadSaveData() {
|
||||
let zip = new JSZip();
|
||||
let folder = zip.folder('save');
|
||||
for (let name of FS.readdir('/save')) {
|
||||
if (!name.toLowerCase().endsWith('.asd'))
|
||||
continue;
|
||||
let content = FS.readFile('/save/' + name, { encoding: 'binary' });
|
||||
folder.file(name, content);
|
||||
}
|
||||
let blob = await zip.generateAsync({type: 'blob', compression: 'DEFLATE'});
|
||||
if (navigator.msSaveBlob) { // Edge
|
||||
navigator.msSaveBlob(blob, 'savedata.zip');
|
||||
} else {
|
||||
let elem = document.createElement('a');
|
||||
elem.setAttribute('download', 'savedata.zip');
|
||||
elem.setAttribute('href', URL.createObjectURL(blob));
|
||||
document.body.appendChild(elem);
|
||||
elem.click();
|
||||
setTimeout(() => { document.body.removeChild(elem); }, 5000);
|
||||
}
|
||||
}
|
||||
|
||||
private uploadSaveData() {
|
||||
openFileInput().then((file) => {
|
||||
this.extractSaveData(file);
|
||||
});
|
||||
}
|
||||
|
||||
private async extractSaveData(file: File) {
|
||||
function basename(path: string) {
|
||||
return path.slice(path.lastIndexOf('/') + 1);
|
||||
}
|
||||
function addSaveFile(name: string, content: ArrayBuffer) {
|
||||
console.log(name);
|
||||
FS.writeFile('/save/' + name, new Uint8Array(content), { encoding: 'binary' });
|
||||
}
|
||||
try {
|
||||
await xsystem35.saveDirReady;
|
||||
if (file.name.toLowerCase().endsWith('.asd')) {
|
||||
addSaveFile(file.name, await readFileAsArrayBuffer(file));
|
||||
} else {
|
||||
let zip = new JSZip();
|
||||
await zip.loadAsync(await readFileAsArrayBuffer(file));
|
||||
for (let f of zip.file(/\.asd$/i))
|
||||
addSaveFile(basename(f.name), await f.async('arraybuffer'));
|
||||
}
|
||||
xsystem35.shell.syncfs(0);
|
||||
xsystem35.shell.addToast('セーブデータの復元に成功しました。', 'success');
|
||||
this.checkSaveData();
|
||||
} catch (err) {
|
||||
xsystem35.shell.addToast('セーブデータを復元できませんでした。', 'danger');
|
||||
console.warn(err);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
-184
@@ -1,184 +0,0 @@
|
||||
/// <reference path="util.ts" />
|
||||
/// <reference path="loader.ts" />
|
||||
/// <reference path="settings.ts" />
|
||||
/// <reference path="zoom.ts" />
|
||||
/// <reference path="volume.ts" />
|
||||
/// <reference path="cdda.ts" />
|
||||
/// <reference path="audio.ts" />
|
||||
/// <reference path="toolbar.ts" />
|
||||
|
||||
namespace xsystem35 {
|
||||
const Font = { url: 'fonts/MTLc3m.ttf', fname: 'MTLc3m.ttf'};
|
||||
export const xsys35rc = [
|
||||
'font_device: ttf',
|
||||
'ttfont_mincho: ' + Font.fname,
|
||||
'ttfont_gothic: ' + Font.fname, '',
|
||||
].join('\n');
|
||||
export let fileSystemReady: Promise<any>;
|
||||
export let saveDirReady: Promise<any>;
|
||||
export let cdPlayer: CDPlayer;
|
||||
export let audio: AudioManager;
|
||||
export let settings: Settings;
|
||||
|
||||
export class System35Shell {
|
||||
private params: URLSearchParams & Map<string, string>;
|
||||
private imageLoader: ImageLoader;
|
||||
status: HTMLElement = document.getElementById('status');
|
||||
private zoom: ZoomManager;
|
||||
private volumeControl: VolumeControl;
|
||||
private toolbar: ToolBar;
|
||||
private antialiasCheckbox: HTMLInputElement;
|
||||
|
||||
constructor() {
|
||||
this.parseParams(location.search.slice(1));
|
||||
this.initModule();
|
||||
|
||||
this.setStatus('Downloading...');
|
||||
window.onerror = () => {
|
||||
this.setStatus('Exception thrown, see JavaScript console');
|
||||
this.setStatus = (text: string) => {
|
||||
if (text) Module.printErr('[post-exception status] ' + text);
|
||||
};
|
||||
};
|
||||
|
||||
this.imageLoader = new ImageLoader(this);
|
||||
this.volumeControl = new VolumeControl();
|
||||
xsystem35.cdPlayer = new CDPlayer(this.imageLoader, this.volumeControl);
|
||||
this.zoom = new ZoomManager();
|
||||
this.toolbar = new ToolBar();
|
||||
this.antialiasCheckbox = <HTMLInputElement>$('#antialias');
|
||||
this.antialiasCheckbox.addEventListener('change', this.antialiasChanged.bind(this));
|
||||
this.antialiasCheckbox.checked = localStorage.getItem('antialias') !== 'false';
|
||||
xsystem35.audio = new AudioManager(this.volumeControl);
|
||||
xsystem35.settings = new Settings();
|
||||
}
|
||||
|
||||
private parseParams(searchParams: string) {
|
||||
if (typeof URLSearchParams !== 'undefined') {
|
||||
this.params = <URLSearchParams & Map<string, string>>new URLSearchParams(searchParams);
|
||||
return;
|
||||
}
|
||||
// For Edge
|
||||
this.params = <URLSearchParams & Map<string, string>>new Map();
|
||||
if (window.location.search.length > 1) {
|
||||
for (let item of searchParams.split('&')) {
|
||||
let [key, value] = item.split('=');
|
||||
this.params.set(key, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private initModule() {
|
||||
let fsReady: () => void;
|
||||
fileSystemReady = new Promise((resolve) => { fsReady = resolve; });
|
||||
let idbfsReady: () => void;
|
||||
saveDirReady = new Promise((resolve) => { idbfsReady = resolve; });
|
||||
|
||||
Module.arguments = [];
|
||||
for (let [name, val] of this.params) {
|
||||
if (name.startsWith('-')) {
|
||||
Module.arguments.push(name);
|
||||
if (val)
|
||||
Module.arguments.push(val);
|
||||
}
|
||||
}
|
||||
Module.print = Module.printErr = console.log.bind(console);
|
||||
Module.setWindowTitle = (title) => {
|
||||
let colon = title.indexOf(':');
|
||||
if (colon !== -1)
|
||||
$('.navbar-brand').textContent = title.slice(colon + 1);
|
||||
};
|
||||
Module.canvas = <HTMLCanvasElement>document.getElementById('canvas');
|
||||
Module.setStatus = this.setStatus.bind(this);
|
||||
Module.preRun = [
|
||||
() => { Module.addRunDependency('gameFiles'); },
|
||||
fsReady,
|
||||
function loadFont() {
|
||||
FS.createPreloadedFile('/', Font.fname, Font.url, true, false);
|
||||
},
|
||||
function prepareSaveDir() {
|
||||
FS.mkdir('/save');
|
||||
FS.mount(IDBFS, {}, '/save');
|
||||
Module.addRunDependency('syncfs');
|
||||
FS.syncfs(true, (err) => {
|
||||
Module.removeRunDependency('syncfs');
|
||||
idbfsReady();
|
||||
});
|
||||
},
|
||||
];
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
let useWasm = typeof WebAssembly === 'object' && this.params.get('wasm') !== '0';
|
||||
let src = useWasm ? 'xsystem35.js' : 'xsystem35.asm.js';
|
||||
let script = document.createElement('script');
|
||||
script.src = src;
|
||||
script.onerror = () => { this.addToast('xsystem35の読み込みに失敗しました。リロードしてください。', 'danger'); };
|
||||
document.body.appendChild(script);
|
||||
});
|
||||
}
|
||||
|
||||
loadStarted() {
|
||||
$('#loader').hidden = true;
|
||||
document.body.classList.add('bgblack-fade');
|
||||
this.toolbar.setCloseable();
|
||||
}
|
||||
|
||||
loaded() {
|
||||
$('#xsystem35').hidden = false;
|
||||
$('#toolbar').classList.remove('before-game-start');
|
||||
setTimeout(() => {
|
||||
if (this.antialiasCheckbox.checked)
|
||||
Module.arguments.push('-antialias');
|
||||
Module.removeRunDependency('gameFiles');
|
||||
}, 0);
|
||||
}
|
||||
|
||||
setStatus(text: string) {
|
||||
console.log(text);
|
||||
this.status.innerHTML = text;
|
||||
}
|
||||
|
||||
windowSizeChanged() {
|
||||
this.zoom.handleZoom();
|
||||
}
|
||||
|
||||
addToast(msg: string | Node, type?: 'success' | 'danger'): HTMLElement {
|
||||
let container = $('.toast-container');
|
||||
let div = document.createElement('div');
|
||||
div.classList.add('toast');
|
||||
if (type)
|
||||
div.classList.add('toast-' + type);
|
||||
if (typeof msg === 'string')
|
||||
div.innerText = msg;
|
||||
else
|
||||
div.appendChild(msg);
|
||||
let btn = document.createElement('button');
|
||||
btn.setAttribute('class', 'btn btn-clear float-right');
|
||||
function dismiss() { container.removeChild(div); }
|
||||
btn.addEventListener('click', dismiss);
|
||||
if (type !== 'danger')
|
||||
setTimeout(dismiss, 5000);
|
||||
div.insertBefore(btn, div.firstChild);
|
||||
container.insertBefore(div, container.firstChild);
|
||||
return div;
|
||||
}
|
||||
|
||||
private fsyncTimer: number;
|
||||
syncfs(timeout = 100) {
|
||||
window.clearTimeout(this.fsyncTimer);
|
||||
this.fsyncTimer = window.setTimeout(() => {
|
||||
FS.syncfs(false, (err) => {
|
||||
if (err)
|
||||
console.log('FS.syncfs error: ', err);
|
||||
});
|
||||
}, timeout);
|
||||
}
|
||||
|
||||
private antialiasChanged() {
|
||||
localStorage.setItem('antialias', String(this.antialiasCheckbox.checked));
|
||||
_ags_setAntialiasedStringMode(this.antialiasCheckbox.checked ? 1 : 0);
|
||||
}
|
||||
}
|
||||
|
||||
export let shell = new System35Shell();
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
/// <reference path="util.ts" />
|
||||
|
||||
namespace xsystem35 {
|
||||
export class ToolBar {
|
||||
private toolbar = $('#toolbar');
|
||||
private handler = $('#toolbar-handler');
|
||||
|
||||
constructor() {
|
||||
$('#screenshot-button').addEventListener('click', this.saveScreenshot.bind(this));
|
||||
}
|
||||
|
||||
setCloseable() {
|
||||
this.handler.hidden = false;
|
||||
this.handler.addEventListener('click', this.open.bind(this));
|
||||
$('#toolbar-close-button').addEventListener('click', this.close.bind(this));
|
||||
this.toolbar.classList.add('closeable');
|
||||
this.close();
|
||||
}
|
||||
|
||||
private open() {
|
||||
this.toolbar.classList.remove('closed');
|
||||
this.handler.hidden = true;
|
||||
}
|
||||
|
||||
private close() {
|
||||
this.toolbar.classList.add('closed');
|
||||
this.handler.hidden = false;
|
||||
}
|
||||
|
||||
private async saveScreenshot() {
|
||||
let pixels = _sdl_getDisplaySurface();
|
||||
let canvas = document.createElement('canvas');
|
||||
canvas.width = Module.canvas.width;
|
||||
canvas.height = Module.canvas.height;
|
||||
let ctx = canvas.getContext('2d');
|
||||
let image = ctx.createImageData(canvas.width, canvas.height);
|
||||
let buffer = image.data;
|
||||
let num = image.data.length;
|
||||
for (let dst = 0; dst < num; dst += 4) {
|
||||
buffer[dst] = Module.HEAPU8[pixels + 2];
|
||||
buffer[dst + 1] = Module.HEAPU8[pixels + 1];
|
||||
buffer[dst + 2] = Module.HEAPU8[pixels];
|
||||
buffer[dst + 3] = 0xff;
|
||||
pixels += 4;
|
||||
}
|
||||
ctx.putImageData(image, 0, 0);
|
||||
|
||||
let url;
|
||||
if (canvas.toBlob) {
|
||||
let blob = await new Promise((resolve) => canvas.toBlob(resolve));
|
||||
url = URL.createObjectURL(blob);
|
||||
} else if (canvas.msToBlob) { // Edge
|
||||
let blob = canvas.msToBlob();
|
||||
navigator.msSaveBlob(blob, 'screenshot.png');
|
||||
return;
|
||||
} else { // Safari
|
||||
url = canvas.toDataURL();
|
||||
}
|
||||
let elem = document.createElement('a');
|
||||
elem.setAttribute('download', 'screenshot.png');
|
||||
elem.setAttribute('href', url);
|
||||
elem.setAttribute('target', '_blank'); // Unless this, iOS safari replaces current page
|
||||
document.body.appendChild(elem);
|
||||
elem.click();
|
||||
setTimeout(() => { document.body.removeChild(elem); }, 5000);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es6",
|
||||
"outFile": "../docs/shell.js",
|
||||
"noImplicitAny": true,
|
||||
"sourceMap": false
|
||||
},
|
||||
"files": [
|
||||
"shell.ts"
|
||||
]
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
{
|
||||
"extends": "tslint:recommended",
|
||||
"rules": {
|
||||
"curly": false,
|
||||
"interface-name": false,
|
||||
"max-classes-per-file": false,
|
||||
"member-access": false,
|
||||
"member-ordering": false,
|
||||
"no-angle-bracket-type-assertion": false,
|
||||
"no-bitwise": false,
|
||||
"no-empty": false,
|
||||
"no-namespace": false,
|
||||
"no-reference": false,
|
||||
"only-arrow-functions": ["allow-declarations", "allow-named-functions"],
|
||||
"prefer-const": false,
|
||||
"prefer-for-of": false,
|
||||
"quotemark": [true, "single", "avoid-escape"],
|
||||
"whitespace": [
|
||||
true,
|
||||
"check-branch",
|
||||
"check-decl",
|
||||
"check-operator",
|
||||
"check-separator",
|
||||
"check-type"
|
||||
],
|
||||
|
||||
"no-console": false
|
||||
}
|
||||
}
|
||||
@@ -1,82 +0,0 @@
|
||||
let $: (selector: string) => HTMLElement = document.querySelector.bind(document);
|
||||
|
||||
function readFileAsArrayBuffer(blob: Blob): Promise<ArrayBuffer> {
|
||||
return new Promise((resolve, reject) => {
|
||||
let reader = new FileReader();
|
||||
reader.onload = () => { resolve(reader.result); };
|
||||
reader.onerror = () => { reject(reader.error); };
|
||||
reader.readAsArrayBuffer(blob);
|
||||
});
|
||||
}
|
||||
|
||||
function readFileAsText(blob: Blob): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
let reader = new FileReader();
|
||||
reader.onload = () => { resolve(reader.result); };
|
||||
reader.onerror = () => { reject(reader.error); };
|
||||
reader.readAsText(blob);
|
||||
});
|
||||
}
|
||||
|
||||
function ASCIIArrayToString(buffer: Uint8Array): string {
|
||||
return String.fromCharCode.apply(null, buffer);
|
||||
}
|
||||
|
||||
function SJISArrayToString(buffer: DataView): string {
|
||||
if (typeof TextDecoder !== 'undefined')
|
||||
return new TextDecoder('shift_jis').decode(buffer);
|
||||
|
||||
let out = [];
|
||||
for (let i = 0; i < buffer.byteLength; i++) {
|
||||
let c = buffer.getUint8(i);
|
||||
if (c >= 0xa0 && c <= 0xdf)
|
||||
out.push(0xff60 + c - 0xa0);
|
||||
else if (c < 0x80)
|
||||
out.push(c);
|
||||
else
|
||||
out.push(_sjis2unicode(c, buffer.getUint8(++i)));
|
||||
}
|
||||
return String.fromCharCode.apply(null, out);
|
||||
}
|
||||
|
||||
function openFileInput(): Promise<File> {
|
||||
return new Promise((resolve) => {
|
||||
let input = document.createElement('input');
|
||||
input.type = 'file';
|
||||
input.addEventListener('change', (evt: Event) => {
|
||||
document.body.removeChild(input);
|
||||
resolve(input.files[0]);
|
||||
});
|
||||
input.style.display = 'none';
|
||||
document.body.appendChild(input);
|
||||
input.click();
|
||||
});
|
||||
}
|
||||
|
||||
declare var WebAssembly: any;
|
||||
|
||||
// xsystem35 exported functions
|
||||
declare function _musfade_setvolval_all(vol: number): void;
|
||||
declare function _ags_setAntialiasedStringMode(on: number): void;
|
||||
declare function _ald_getdata(type: number, no: number): number;
|
||||
declare function _ald_freedata(data: number): void;
|
||||
declare function _sjis2unicode(byte1: number, byte2: number): void;
|
||||
declare function _sdl_getDisplaySurface(): number;
|
||||
|
||||
declare namespace Module {
|
||||
// Undocumented methods / attributes
|
||||
let canvas: HTMLCanvasElement;
|
||||
function getMemory(size: number): number;
|
||||
function setStatus(status: string): void;
|
||||
function setWindowTitle(title: string): void;
|
||||
}
|
||||
|
||||
declare namespace FS {
|
||||
function readFile(path: string, opts?: {encoding?: string; flags?: string}): any;
|
||||
function writeFile(path: string, data: ArrayBufferView | string,
|
||||
opts?: {encoding?: string; flags?: string; canOwn?: boolean}): void;
|
||||
}
|
||||
|
||||
declare namespace EmterpreterAsync {
|
||||
function handle(asyncOp: (resume: () => void) => void): void;
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
/// <reference path="util.ts" />
|
||||
|
||||
namespace xsystem35 {
|
||||
export class VolumeControl {
|
||||
private vol: number; // 0.0 - 1.0
|
||||
private muted: boolean;
|
||||
private elem: HTMLElement;
|
||||
private icon: HTMLElement;
|
||||
private slider: HTMLInputElement;
|
||||
|
||||
constructor() {
|
||||
this.vol = Number(localStorage.getItem('volume') || 1);
|
||||
this.muted = false;
|
||||
|
||||
this.elem = $('#volume-control');
|
||||
this.icon = $('#volume-control-icon');
|
||||
this.slider = <HTMLInputElement>$('#volume-control-slider');
|
||||
this.slider.value = String(Math.round(this.vol * 100));
|
||||
|
||||
this.icon.addEventListener('click', this.onIconClicked.bind(this));
|
||||
this.slider.addEventListener('input', this.onSliderValueChanged.bind(this));
|
||||
this.slider.addEventListener('change', this.onSliderValueSettled.bind(this));
|
||||
}
|
||||
|
||||
volume(): number {
|
||||
return this.muted ? 0 : parseInt(this.slider.value, 10) / 100;
|
||||
}
|
||||
|
||||
addEventListener(handler: (evt: CustomEvent) => any) {
|
||||
this.elem.addEventListener('volumechange', handler);
|
||||
}
|
||||
|
||||
hideSlider() {
|
||||
this.slider.hidden = true;
|
||||
}
|
||||
|
||||
private onIconClicked(e: Event) {
|
||||
this.muted = !this.muted;
|
||||
if (this.muted) {
|
||||
this.icon.classList.remove('fa-volume-up');
|
||||
this.icon.classList.add('fa-volume-off');
|
||||
this.slider.value = '0';
|
||||
} else {
|
||||
this.icon.classList.remove('fa-volume-off');
|
||||
this.icon.classList.add('fa-volume-up');
|
||||
this.slider.value = String(Math.round(this.vol * 100));
|
||||
}
|
||||
this.dispatchEvent();
|
||||
}
|
||||
|
||||
private onSliderValueChanged(e: Event) {
|
||||
this.vol = parseInt(this.slider.value, 10) / 100;
|
||||
if (this.vol > 0 && this.muted) {
|
||||
this.muted = false;
|
||||
this.icon.classList.remove('fa-volume-off');
|
||||
this.icon.classList.add('fa-volume-up');
|
||||
}
|
||||
this.dispatchEvent();
|
||||
}
|
||||
|
||||
private onSliderValueSettled(e: Event) {
|
||||
localStorage.setItem('volume', this.vol + '');
|
||||
}
|
||||
|
||||
private dispatchEvent() {
|
||||
let event = new CustomEvent('volumechange', { detail: this.volume() });
|
||||
this.elem.dispatchEvent(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
/// <reference path="util.ts" />
|
||||
|
||||
namespace xsystem35 {
|
||||
export class ZoomManager {
|
||||
private canvas = <HTMLCanvasElement>$('#canvas');
|
||||
private zoomSelect = <HTMLInputElement>$('#zoom');
|
||||
private pixelateCheckbox = <HTMLInputElement>$('#pixelate');
|
||||
|
||||
constructor() {
|
||||
this.zoomSelect.addEventListener('change', this.handleZoom.bind(this));
|
||||
this.zoomSelect.value = localStorage.getItem('zoom') || 'fit';
|
||||
if (CSS.supports('image-rendering', 'pixelated') || CSS.supports('image-rendering', '-moz-crisp-edges')) {
|
||||
this.pixelateCheckbox.addEventListener('change', this.handlePixelate.bind(this));
|
||||
if (localStorage.getItem('pixelate') === 'true') {
|
||||
this.pixelateCheckbox.checked = true;
|
||||
this.handlePixelate();
|
||||
}
|
||||
} else {
|
||||
this.pixelateCheckbox.setAttribute('disabled', 'true');
|
||||
}
|
||||
}
|
||||
|
||||
handleZoom() {
|
||||
let value = this.zoomSelect.value;
|
||||
localStorage.setItem('zoom', value);
|
||||
let navbarStyle = $('.navbar').style;
|
||||
if (value === 'fit') {
|
||||
$('#xsystem35').classList.add('fit');
|
||||
navbarStyle.maxWidth = 'none';
|
||||
this.canvas.style.width = null;
|
||||
} else {
|
||||
$('#xsystem35').classList.remove('fit');
|
||||
let ratio = Number(value);
|
||||
navbarStyle.maxWidth = this.canvas.style.width = this.canvas.width * ratio + 'px';
|
||||
}
|
||||
}
|
||||
|
||||
private handlePixelate() {
|
||||
localStorage.setItem('pixelate', String(this.pixelateCheckbox.checked));
|
||||
if (this.pixelateCheckbox.checked)
|
||||
this.canvas.classList.add('pixelated');
|
||||
else
|
||||
this.canvas.classList.remove('pixelated');
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user