This commit is contained in:
Andrey Kolosov
2019-08-01 18:02:20 +03:00
parent ca67365fe7
commit 1c7c0684c7
15 changed files with 3617 additions and 2717 deletions

View File

@@ -72,7 +72,9 @@ module.exports = {
appBuild: resolveApp('build'), appBuild: resolveApp('build'),
appPublic: resolveApp('public'), appPublic: resolveApp('public'),
appHtml: resolveApp('public/index.html'), appHtml: resolveApp('public/index.html'),
appHtmlStorage: resolveApp('public/storage.html'),
appIndexJs: resolveModule(resolveApp, 'src/index'), appIndexJs: resolveModule(resolveApp, 'src/index'),
appStorageJs: resolveModule(resolveApp, 'src/storage'),
appPackageJson: resolveApp('package.json'), appPackageJson: resolveApp('package.json'),
appSrc: resolveApp('src'), appSrc: resolveApp('src'),
appTsConfig: resolveApp('tsconfig.json'), appTsConfig: resolveApp('tsconfig.json'),

View File

@@ -128,7 +128,8 @@ module.exports = function(webpackEnv) {
: isEnvDevelopment && 'cheap-module-source-map', : isEnvDevelopment && 'cheap-module-source-map',
// These are the "entry points" to our application. // These are the "entry points" to our application.
// This means they will be the "root" imports that are included in JS bundle. // This means they will be the "root" imports that are included in JS bundle.
entry: [ entry: {
main: [
// Include an alternative client for WebpackDevServer. A client's job is to // Include an alternative client for WebpackDevServer. A client's job is to
// connect to WebpackDevServer by a socket and get notified about changes. // connect to WebpackDevServer by a socket and get notified about changes.
// When you save a file, the client will either apply hot updates (in case // When you save a file, the client will either apply hot updates (in case
@@ -147,6 +148,8 @@ module.exports = function(webpackEnv) {
// initialization, it doesn't blow up the WebpackDevServer client, and // initialization, it doesn't blow up the WebpackDevServer client, and
// changing JS code would still trigger a refresh. // changing JS code would still trigger a refresh.
].filter(Boolean), ].filter(Boolean),
storage: paths.appStorageJs,
},
output: { output: {
// The build folder. // The build folder.
path: isEnvProduction ? paths.appBuild : undefined, path: isEnvProduction ? paths.appBuild : undefined,
@@ -500,6 +503,7 @@ module.exports = function(webpackEnv) {
{}, {},
{ {
inject: true, inject: true,
chunks: ['main'],
template: paths.appHtml, template: paths.appHtml,
}, },
isEnvProduction isEnvProduction
@@ -520,6 +524,33 @@ module.exports = function(webpackEnv) {
: undefined : undefined
) )
), ),
new HtmlWebpackPlugin(
Object.assign(
{},
{
inject: true,
chunks: ['storage'],
filename: 'storage.html',
template: paths.appHtmlStorage,
},
isEnvProduction
? {
minify: {
removeComments: true,
collapseWhitespace: true,
removeRedundantAttributes: true,
useShortDoctype: true,
removeEmptyAttributes: true,
removeStyleLinkTypeAttributes: true,
keepClosingSlash: true,
minifyJS: true,
minifyCSS: true,
minifyURLs: true,
},
}
: undefined
)
),
// Inlines the webpack runtime script. This script is too small to warrant // Inlines the webpack runtime script. This script is too small to warrant
// a network request. // a network request.
isEnvProduction && isEnvProduction &&

5992
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -64,7 +64,8 @@
"scripts": { "scripts": {
"start": "node scripts/start.js", "start": "node scripts/start.js",
"build": "node scripts/build.js", "build": "node scripts/build.js",
"test": "node scripts/test.js" "test": "node scripts/test.js",
"deploy": "gh-pages -d build"
}, },
"eslintConfig": { "eslintConfig": {
"extends": "react-app" "extends": "react-app"
@@ -131,8 +132,10 @@
"react-app" "react-app"
] ]
}, },
"homepage": "https://d07riv.github.io/diabloweb",
"devDependencies": { "devDependencies": {
"exports-loader": "^0.7.0", "exports-loader": "^0.7.0",
"gh-pages": "^2.0.1",
"node-sass": "^4.12.0", "node-sass": "^4.12.0",
"worker-loader": "^2.0.0" "worker-loader": "^2.0.0"
} }

BIN
public/icon-192.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

BIN
public/icon-512.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

View File

@@ -6,10 +6,20 @@
"src": "favicon.ico", "src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16", "sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon" "type": "image/x-icon"
},
{
"src": "icon-192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "icon-512.png",
"sizes": "512x512",
"type": "image/png"
} }
], ],
"start_url": ".", "start_url": ".",
"display": "standalone", "display": "fullscreen",
"theme_color": "#000000", "theme_color": "#ffffff",
"background_color": "#ffffff" "background_color": "#000000"
} }

9
public/storage.html Normal file
View File

@@ -0,0 +1,9 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>DIABLO Connector</title>
</head>
<body>
</body>
</html>

View File

@@ -46,7 +46,7 @@ class App extends React.Component {
touchMods = [false, false, false, false, false, false]; touchMods = [false, false, false, false, false, false];
touchBelt = [-1, -1, -1, -1, -1, -1]; touchBelt = [-1, -1, -1, -1, -1, -1];
fs = create_fs(this); fs = create_fs(true);
constructor(props) { constructor(props) {
super(props); super(props);
@@ -344,6 +344,11 @@ class App extends React.Component {
this.touchButton = touchOther; this.touchButton = touchOther;
if (touchOther) { if (touchOther) {
this.setTouchMod(touchOther.index, true); this.setTouchMod(touchOther.index, true);
if (touchOther.index === TOUCH_MOVE) {
this.setTouchMod(TOUCH_RMB, false);
} else if (touchOther.index === TOUCH_RMB) {
this.setTouchMod(TOUCH_MOVE, false);
}
delete this.panPos; delete this.panPos;
} else if (touches.length === 2) { } else if (touches.length === 2) {
const x = (touches[1].clientX + touches[0].clientX) / 2, y = (touches[1].clientY + touches[0].clientY) / 2; const x = (touches[1].clientX + touches[0].clientX) / 2, y = (touches[1].clientY + touches[0].clientY) / 2;
@@ -406,6 +411,10 @@ class App extends React.Component {
const {x, y} = this.mousePos(prevTc); const {x, y} = this.mousePos(prevTc);
this.game("DApi_Mouse", 2, 1, this.eventMods(e), x, y); this.game("DApi_Mouse", 2, 1, this.eventMods(e), x, y);
this.game("DApi_Mouse", 2, 2, this.eventMods(e), x, y); this.game("DApi_Mouse", 2, 2, this.eventMods(e), x, y);
if (this.touchMods[TOUCH_RMB] && (!this.touchButton || this.touchButton.index !== TOUCH_RMB)) {
this.setTouchButton(TOUCH_RMB, false);
}
} }
if (!document.fullscreenElement) { if (!document.fullscreenElement) {
this.element.requestFullscreen(); this.element.requestFullscreen();
@@ -469,22 +478,20 @@ class App extends React.Component {
This is a web port of the original Diablo game, based on source code reconstructed by This is a web port of the original Diablo game, based on source code reconstructed by
GalaXyHaXz and devilution team: <Link href="https://github.com/diasurgical/devilution">https://github.com/diasurgical/devilution</Link> GalaXyHaXz and devilution team: <Link href="https://github.com/diasurgical/devilution">https://github.com/diasurgical/devilution</Link>
</p> </p>
<form>
<p> <p>
If you own the original game, you can drop the original DIABDAT.MPQ onto this page (or <label htmlFor="loadFile" className="link" onClick={this.download}>click here</label>) If you own the original game, you can drop the original DIABDAT.MPQ onto this page or click the button below to start playing.
to start playing. The game can be purchased from <Link href="https://www.gog.com/game/diablo">GoG</Link>. The game can be purchased from <Link href="https://www.gog.com/game/diablo">GoG</Link>.
</p> </p>
<input accept=".mpq" type="file" id="loadFile" style={{display: "none"}} onChange={this.parseFile}/> {!has_spawn && (
</form>
{has_spawn ? (
<span className="startButton" onClick={() => this.start()}>Play Shareware</span>
) : (
<p> <p>
Or you can download and play the shareware version instead (50MB download). <i>The site has lately been under too much stress due to users downloading the shareware Or you can play the shareware version for free (50MB download).
package, so instead you will need to <a href="https://d07riv.github.io/diabloweb/public/spawn.mpq">download it from GitHub</a>, then drop it on the page (or upload
by <label htmlFor="loadFile" className="link" onClick={this.download}>clicking here</label>).</i>
</p> </p>
)} )}
<form>
<label htmlFor="loadFile" className="startButton">Select MPQ</label>
<input accept=".mpq" type="file" id="loadFile" style={{display: "none"}} onChange={this.parseFile}/>
</form>
<span className="startButton" onClick={() => this.start()}>Play Shareware</span>
</div> </div>
)} )}
</div> </div>

View File

@@ -79,7 +79,7 @@ body, #root, .App {
text-align: center; text-align: center;
background: #000; background: #000;
p { p {
margin: 12px 0; margin: 10px 0;
} }
.startButton { .startButton {
display: inline-block; display: inline-block;
@@ -88,6 +88,8 @@ body, #root, .App {
font-size: 2em; font-size: 2em;
padding: 4px 18px; padding: 4px 18px;
cursor: pointer; cursor: pointer;
margin-top: 6px;
width: 90%;
&:hover { &:hover {
background-color: #111; background-color: #111;
} }

View File

@@ -1,4 +1,4 @@
//import axios from 'axios'; import axios from 'axios';
const SpawnSize = 50274091; const SpawnSize = 50274091;
@@ -12,13 +12,12 @@ export default async function load_spawn(api, fs) {
file = null; file = null;
} }
if (!file) { if (!file) {
throw Error("Invalid spawn.mpq size."); const spawn = await axios.request({
/*const spawn = await axios.request({
url: '/spawn.mpq', url: '/spawn.mpq',
responseType: 'arraybuffer', responseType: 'arraybuffer',
onDownloadProgress: e => { onDownloadProgress: e => {
if (api.onProgress) { if (api.onProgress) {
api.onProgress({type: 'spawn', loaded: e.loaded, total: e.total || SpawnSize}); api.onProgress({text: 'Downloading...', loaded: e.loaded, total: e.total || SpawnSize});
} }
}, },
headers: { headers: {
@@ -30,7 +29,7 @@ export default async function load_spawn(api, fs) {
} }
const data = new Uint8Array(spawn.data); const data = new Uint8Array(spawn.data);
fs.files.set('spawn.mpq', data); fs.files.set('spawn.mpq', data);
fs.update('spawn.mpq', data);*/ fs.update('spawn.mpq', data.slice());
} }
return fs; return fs;
} }

View File

@@ -1,22 +1,63 @@
import IdbKvStore from 'idb-kv-store'; import IdbKvStore from 'idb-kv-store';
export default async function create_fs() { const importStorage = () => new Promise((resolve, reject) => {
let done = false;
const frame = document.createElement('iframe');
window.addEventListener('message', ({data}) => {
if (data.method === 'storage' && !done) {
done = true;
resolve(data.files);
frame.contentWindow.postMessage({method: 'clear'}, '*');
}
});
frame.addEventListener('load', () => {
frame.contentWindow.postMessage({method: 'transfer'}, '*');
});
frame.addEventListener('error', () => {
if (!done) {
done = true;
resolve(null);
}
});
frame.src = "https://diablo.rivsoft.net/storage.html";
frame.style.display = "none";
document.body.appendChild(frame);
setTimeout(() => {
if (!done) {
done = true;
resolve(null);
}
}, 10000);
});
export default async function create_fs(load) {
try { try {
const store = new IdbKvStore('diablo_fs'); const store = new IdbKvStore('diablo_fs');
const files = new Map(); const files = new Map();
for (let [name, data] of Object.entries(await store.json())) { for (let [name, data] of Object.entries(await store.json())) {
files.set(name, data); files.set(name, data);
} }
if (load) {
const files = await importStorage();
if (files) {
for (let [name, data] of files) {
files.set(name, data);
store.set(name, data);
}
}
}
return { return {
files, files,
update: (name, data) => store.set(name, data), update: (name, data) => store.set(name, data),
delete: name => store.remove(name), delete: name => store.remove(name),
clear: () => store.clear(),
}; };
} catch (e) { } catch (e) {
return { return {
files: new Map(), files: new Map(),
update: () => Promise.resolve(), update: () => Promise.resolve(),
delete: () => Promise.resolve(), delete: () => Promise.resolve(),
clear: () => Promise.resolve(),
}; };
} }
} }

View File

@@ -1,7 +1,10 @@
import React from 'react'; import React from 'react';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';
import './reset.css'; import './reset.css';
import * as serviceWorker from './serviceWorker';
import App from './App'; import App from './App';
ReactDOM.render(<App />, document.getElementById('root')); ReactDOM.render(<App />, document.getElementById('root'));
serviceWorker.register();

135
src/serviceWorker.js Normal file
View File

@@ -0,0 +1,135 @@
// This optional code is used to register a service worker.
// register() is not called by default.
// This lets the app load faster on subsequent visits in production, and gives
// it offline capabilities. However, it also means that developers (and users)
// will only see deployed updates on subsequent visits to a page, after all the
// existing tabs open on the page have been closed, since previously cached
// resources are updated in the background.
// To learn more about the benefits of this model and instructions on how to
// opt-in, read https://bit.ly/CRA-PWA
const isLocalhost = Boolean(
window.location.hostname === 'localhost' ||
// [::1] is the IPv6 localhost address.
window.location.hostname === '[::1]' ||
// 127.0.0.1/8 is considered localhost for IPv4.
window.location.hostname.match(
/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
)
);
export function register(config) {
if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
// The URL constructor is available in all browsers that support SW.
const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href);
if (publicUrl.origin !== window.location.origin) {
// Our service worker won't work if PUBLIC_URL is on a different origin
// from what our page is served on. This might happen if a CDN is used to
// serve assets; see https://github.com/facebook/create-react-app/issues/2374
return;
}
window.addEventListener('load', () => {
const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
if (isLocalhost) {
// This is running on localhost. Let's check if a service worker still exists or not.
checkValidServiceWorker(swUrl, config);
// Add some additional logging to localhost, pointing developers to the
// service worker/PWA documentation.
navigator.serviceWorker.ready.then(() => {
console.log(
'This web app is being served cache-first by a service ' +
'worker. To learn more, visit https://bit.ly/CRA-PWA'
);
});
} else {
// Is not localhost. Just register service worker
registerValidSW(swUrl, config);
}
});
}
}
function registerValidSW(swUrl, config) {
navigator.serviceWorker
.register(swUrl)
.then(registration => {
registration.onupdatefound = () => {
const installingWorker = registration.installing;
if (installingWorker == null) {
return;
}
installingWorker.onstatechange = () => {
if (installingWorker.state === 'installed') {
if (navigator.serviceWorker.controller) {
// At this point, the updated precached content has been fetched,
// but the previous service worker will still serve the older
// content until all client tabs are closed.
console.log(
'New content is available and will be used when all ' +
'tabs for this page are closed. See https://bit.ly/CRA-PWA.'
);
// Execute callback
if (config && config.onUpdate) {
config.onUpdate(registration);
}
} else {
// At this point, everything has been precached.
// It's the perfect time to display a
// "Content is cached for offline use." message.
console.log('Content is cached for offline use.');
// Execute callback
if (config && config.onSuccess) {
config.onSuccess(registration);
}
}
}
};
};
})
.catch(error => {
console.error('Error during service worker registration:', error);
});
}
function checkValidServiceWorker(swUrl, config) {
// Check if the service worker can be found. If it can't reload the page.
fetch(swUrl)
.then(response => {
// Ensure service worker exists, and that we really are getting a JS file.
const contentType = response.headers.get('content-type');
if (
response.status === 404 ||
(contentType != null && contentType.indexOf('javascript') === -1)
) {
// No service worker found. Probably a different app. Reload the page.
navigator.serviceWorker.ready.then(registration => {
registration.unregister().then(() => {
window.location.reload();
});
});
} else {
// Service worker found. Proceed as normal.
registerValidSW(swUrl, config);
}
})
.catch(() => {
console.log(
'No internet connection found. App is running in offline mode.'
);
});
}
export function unregister() {
if ('serviceWorker' in navigator) {
navigator.serviceWorker.ready.then(registration => {
registration.unregister();
});
}
}

12
src/storage.js Normal file
View File

@@ -0,0 +1,12 @@
import create_fs from './fs';
const fs = create_fs();
window.addEventListener('message', ({data, source}) => {
if (data.method === 'transfer') {
fs.then(({files}) => {
source.postMessage({method: 'storage', files}, '*');
});
} else if (data.method === 'clear') {
fs.then(({clear}) => clear());
}
});