Move shell to separate repo

This commit is contained in:
kichikuou
2017-05-02 11:21:05 +09:00
parent e53e2b40c1
commit 8ace19af34
20 changed files with 7 additions and 2065 deletions
+1 -4
View File
@@ -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
View File
@@ -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.
-190
View File
@@ -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
View File
@@ -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
View File
@@ -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;
}
-128
View File
@@ -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
View File
@@ -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
View File
@@ -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);
}
}
}
-292
View File
@@ -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
View File
@@ -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 + '/');
}
}
}
}
}
-10
View File
@@ -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"
}
}
-92
View File
@@ -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
View File
@@ -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();
}
-68
View File
@@ -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);
}
}
}
-11
View File
@@ -1,11 +0,0 @@
{
"compilerOptions": {
"target": "es6",
"outFile": "../docs/shell.js",
"noImplicitAny": true,
"sourceMap": false
},
"files": [
"shell.ts"
]
}
-29
View File
@@ -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
}
}
-82
View File
@@ -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;
}
-70
View File
@@ -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);
}
}
}
-46
View File
@@ -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');
}
}
}