Initial (dev)
This commit is contained in:
commit
d0d8c5f08e
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
node_modules/
|
||||
*.ini
|
26
README
Normal file
26
README
Normal file
@ -0,0 +1,26 @@
|
||||
CHUNICHUNIMatch
|
||||
===============
|
||||
|
||||
Remote LAN multiplayer matching by virtualizing members into the local LAN.
|
||||
|
||||
*No VPN required*
|
||||
|
||||
Instructions
|
||||
------------
|
||||
|
||||
***Project is currently in experimentation***
|
||||
|
||||
* 配置IP别名。提权cmd运行以下指令在主网卡上新增192.168.139.11-14(如果必要,替换Ethernet为网卡名)
|
||||
```
|
||||
netsh interface ipv4 set interface interface="Ethernet" dhcpstaticipcoexistence=enabled
|
||||
netsh interface ipv4 add address "Ethernet" 192.168.139.11 255.255.255.0 store=active
|
||||
netsh interface ipv4 add address "Ethernet" 192.168.139.12 255.255.255.0 store=active
|
||||
netsh interface ipv4 add address "Ethernet" 192.168.139.13 255.255.255.0 store=active
|
||||
netsh interface ipv4 add address "Ethernet" 192.168.139.14 255.255.255.0 store=active
|
||||
```
|
||||
* 确保使用的segatools更新到至少forcebind
|
||||
* 修改segatools.ini添加下面的新section以使用CCM在连接时自动生成的INI
|
||||
```
|
||||
[override]
|
||||
netenv="D:\path\to\ccm\segatools-override.ini"
|
||||
```
|
78
ccencap.js
Normal file
78
ccencap.js
Normal file
@ -0,0 +1,78 @@
|
||||
var CCEncap = {};
|
||||
|
||||
const CCM_NET_SENDTCP = 0x01;
|
||||
const CCM_NET_SENDUDP = 0x02;
|
||||
const CCM_PARTY_CREATE = 0x30;
|
||||
const CCM_PARTY_HELLO = 0x31;
|
||||
const CCM_PARTY_UPDATE = 0x32;
|
||||
|
||||
CCEncap.encodePartyCreate = function(req) {
|
||||
var msg = Buffer.from(JSON.stringify(req));
|
||||
msg[0] = CCM_PARTY_CREATE;
|
||||
return msg;
|
||||
}
|
||||
|
||||
CCEncap.encodePartyHello = function(req) {
|
||||
var msg = Buffer.from(JSON.stringify(req));
|
||||
msg[0] = CCM_PARTY_HELLO;
|
||||
return msg;
|
||||
}
|
||||
|
||||
CCEncap.encodePartyUpdate = function(req) {
|
||||
var msg = Buffer.from(JSON.stringify(req));
|
||||
msg[0] = CCM_PARTY_UPDATE;
|
||||
return msg;
|
||||
}
|
||||
|
||||
CCEncap.encodePacket = function(src, dst, sport, dport, type, data) {
|
||||
if (type == 'tcp') {
|
||||
var header = CCM_NET_SENDTCP;
|
||||
} else if (type == 'udp') {
|
||||
var header = CCM_NET_SENDUDP;
|
||||
} else {
|
||||
throw new Error('Invalid network protocol: ' + type);
|
||||
}
|
||||
var msg = Buffer.alloc(data.length + 7);
|
||||
msg[0] = header;
|
||||
msg[1] = dst;
|
||||
msg.writeUInt16LE(sport, 2);
|
||||
msg.writeUInt16LE(dport, 4);
|
||||
msg[6] = src;
|
||||
data.copy(msg, 7);
|
||||
console.log(type + "_send "+src+':'+sport+'->'+dst+':'+dport);
|
||||
return msg;
|
||||
}
|
||||
|
||||
CCEncap.decode = function(msg) {
|
||||
var opcode = msg[0], data = msg;
|
||||
var ret = {op: '?'};
|
||||
switch(opcode) {
|
||||
case CCM_PARTY_CREATE: ret.op = 'party_create'; break;
|
||||
case CCM_PARTY_HELLO: ret.op = 'party_hello'; break;
|
||||
case CCM_PARTY_UPDATE: ret.op = 'party_update'; break;
|
||||
case CCM_NET_SENDTCP: ret.op = 'net_tcp'; break;
|
||||
case CCM_NET_SENDUDP: ret.op = 'net_udp'; break;
|
||||
}
|
||||
switch(opcode) {
|
||||
case CCM_PARTY_CREATE:
|
||||
case CCM_PARTY_HELLO:
|
||||
case CCM_PARTY_UPDATE:
|
||||
msg[0] = 0x7b; // '{'
|
||||
Object.assign(ret, JSON.parse(msg));
|
||||
break;
|
||||
case CCM_NET_SENDTCP:
|
||||
case CCM_NET_SENDUDP:
|
||||
ret.dst = msg[1];
|
||||
ret.src = msg[6];
|
||||
ret.sport = msg.readUInt16LE(2);
|
||||
ret.dport = msg.readUInt16LE(4);
|
||||
// ret.msg = msg.slice(7);
|
||||
console.log(ret.op+" "+ret.src+':'+ret.sport+'->'+ret.dst+':'+ret.dport);
|
||||
break;
|
||||
default:
|
||||
console.warn('Received unknown opcode: ' + opcode.toString(16));
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
module.exports = CCEncap;
|
72
cchub.js
Normal file
72
cchub.js
Normal file
@ -0,0 +1,72 @@
|
||||
const ws = require('ws');
|
||||
const enc = require('./ccencap');
|
||||
|
||||
// Note: src and dst addresses in this file are numerical suffixes only.
|
||||
|
||||
function CCHub(url) {
|
||||
var conn = new ws.WebSocket(url);
|
||||
this.ready = new Promise((resolve) => {
|
||||
conn.on('open', () => {
|
||||
this.conn = conn;
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
this.conn = this.ready;
|
||||
|
||||
this.onNetForward = function(src, dst, sport, dport, type, msg) {};
|
||||
this.onPartyJoin = function(subnet, suffix) {};
|
||||
this.onPartyUpdate = function(members) {};
|
||||
|
||||
conn.on('message', (data) => {
|
||||
msg = enc.decode(data);
|
||||
switch (msg.op) {
|
||||
case 'party_update':
|
||||
this._updateParty(msg);
|
||||
break;
|
||||
case 'net_tcp':
|
||||
case 'net_udp':
|
||||
var type = msg.op.slice(4);
|
||||
var payload = data.slice(7);
|
||||
this.onNetForward(msg.src, msg.dst, msg.sport, msg.dport, type, payload);
|
||||
break;
|
||||
default:
|
||||
console.warn('Unhandled packed: ', msg)
|
||||
}
|
||||
});
|
||||
|
||||
this.subnet = null;
|
||||
this.mysuffix = 0;
|
||||
this.members = [];
|
||||
}
|
||||
|
||||
CCHub.prototype.send = function(msg) {
|
||||
if (this.conn instanceof Promise) {
|
||||
this.conn.then(() => {
|
||||
this.send(msg);
|
||||
});
|
||||
return;
|
||||
}
|
||||
this.conn.send(msg);
|
||||
};
|
||||
|
||||
CCHub.prototype.hello = function(req) {
|
||||
this.send(enc.encodePartyHello(req));
|
||||
};
|
||||
|
||||
CCHub.prototype._updateParty = function(msg) {
|
||||
if (!this.subnet) {
|
||||
this.subnet = msg.subnet;
|
||||
this.mysuffix = msg.suffix;
|
||||
this.onPartyJoin(this.subnet, this.mysuffix);
|
||||
}
|
||||
var diff = msg.members.filter(suffix => this.members.indexOf(suffix) == -1);
|
||||
this.members = this.members.concat(diff);
|
||||
this.onPartyUpdate(diff);
|
||||
};
|
||||
|
||||
CCHub.prototype.sendNetForward = function(src, dst, sport, dport, type, data) {
|
||||
var msg = enc.encodePacket(src, dst, sport, dport, type, data);
|
||||
this.send(msg);
|
||||
};
|
||||
|
||||
module.exports = CCHub;
|
120
ccrelay.js
Normal file
120
ccrelay.js
Normal file
@ -0,0 +1,120 @@
|
||||
const net = require('net');
|
||||
const dgram = require('dgram');
|
||||
|
||||
// Note: src and dst addresses in this file are full IP strings only.
|
||||
|
||||
function CCRelay(host) {
|
||||
console.log("Relay started for " + host);
|
||||
this.host = host;
|
||||
this.sockets = {};
|
||||
this.servers = {};
|
||||
this.onReceive = function(sport, dport, type, msg) {}
|
||||
}
|
||||
|
||||
CCRelay.prototype.bindUDP = function(port) {
|
||||
if (this.sockets['udp'+port]) {
|
||||
console.warn('Attempted to bind more than once on '+this.host+':'+this.port);
|
||||
return this.sockets['udp'+port];
|
||||
}
|
||||
var socket = dgram.createSocket('udp4');
|
||||
socket.on('error', (e) => {
|
||||
console.error('udp '+this.host+':'+port+' error', e);
|
||||
socket.close();
|
||||
this.sockets['udp'+port] = null;
|
||||
this.bindUDP(port);
|
||||
});
|
||||
socket.on('message', (msg, rinfo) => {
|
||||
this.onReceive(rinfo.port, port, 'udp', msg);
|
||||
});
|
||||
var bindpromise = new Promise((resolve) => {
|
||||
socket.bind({address: this.host, port: port}, () => {
|
||||
this.sockets['udp'+port] = socket;
|
||||
resolve(socket);
|
||||
});
|
||||
});
|
||||
this.sockets['udp'+port] = bindpromise;
|
||||
return bindpromise;
|
||||
};
|
||||
|
||||
CCRelay.prototype.sendUDP = function(dst, sport, dport, msg) {
|
||||
var socket = this.sockets['udp'+sport];
|
||||
if (!socket) {
|
||||
socket = this.bindUDP(sport);
|
||||
}
|
||||
if (socket instanceof Promise) {
|
||||
return socket.then(() => {
|
||||
return this.sendUDP(dst, sport, dport, msg);
|
||||
});
|
||||
}
|
||||
return new Promise((resolve) => {
|
||||
socket.send(msg, dport, dst, resolve);
|
||||
});
|
||||
};
|
||||
|
||||
CCRelay.prototype._setupTCPSocket = function(socket, sport, dport) {
|
||||
socket.on('data', (msg) => {
|
||||
this.onReceive(sport, dport, 'tcp', msg);
|
||||
});
|
||||
socket.on('end', () => {
|
||||
this.sockets['tcp'+sport+'-'+dport] = null;
|
||||
});
|
||||
socket.on('close', () => {
|
||||
this.sockets['tcp'+sport+'-'+dport] = null;
|
||||
});
|
||||
socket.on('error', e => {
|
||||
this.sockets['tcp'+sport+'-'+dport] = null;
|
||||
});
|
||||
}
|
||||
|
||||
CCRelay.prototype.listenTCP = function(port) {
|
||||
if (this.servers['tcp'+port]) {
|
||||
console.warn('Attempted to listen more than once on '+this.host+':'+this.port);
|
||||
return this.servers['tcp'+port];
|
||||
}
|
||||
var server = net.createServer();
|
||||
server.on('error', (e) => {
|
||||
console.error('tcpserver '+this.host+':'+port+' error', e);
|
||||
server.close();
|
||||
this.servers['tcp'+port] = null;
|
||||
this.listenTCP(port);
|
||||
});
|
||||
server.on('connection', (socket) => {
|
||||
this._setupTCPSocket(socket, socket.remotePort, port);
|
||||
console.log("Adding passive TCP socket ", 'tcp'+socket.remotePort+'-'+port)
|
||||
this.sockets['tcp'+socket.remotePort+'-'+port] = socket;
|
||||
});
|
||||
var listenpromise = new Promise((resolve) => {
|
||||
server.listen({host: this.host, port: port}, () => {
|
||||
this.servers['tcp'+port] = server;
|
||||
resolve(server);
|
||||
});
|
||||
});
|
||||
this.servers['tcp'+port] = listenpromise;
|
||||
return listenpromise;
|
||||
};
|
||||
|
||||
CCRelay.prototype.sendTCP = function(dst, sport, dport, msg) {
|
||||
var socket = this.sockets['tcp'+sport+'-'+dport];
|
||||
if (!socket) {
|
||||
socket = new Promise((resolve) => {
|
||||
console.log("Adding active TCP socket", 'tcp'+sport+'-'+dport);
|
||||
var newsocket = new net.Socket();
|
||||
this._setupTCPSocket(newsocket, sport, dport);
|
||||
newsocket.connect(dport, dst, () => {
|
||||
this.sockets['tcp'+sport+'-'+dport] = newsocket;
|
||||
resolve(newsocket);
|
||||
});
|
||||
});
|
||||
this.sockets['tcp'+sport+'-'+dport] = socket;
|
||||
}
|
||||
if (socket instanceof Promise) {
|
||||
return socket.then(() => {
|
||||
return this.sendTCP(dst, sport, dport, msg);
|
||||
});
|
||||
}
|
||||
return new Promise((resolve) => {
|
||||
socket.write(msg, null, resolve);
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = CCRelay;
|
6
config.js
Normal file
6
config.js
Normal file
@ -0,0 +1,6 @@
|
||||
module.exports = {
|
||||
"server": "gcp.naominet.com",
|
||||
"server_port": 61820,
|
||||
"login": {},
|
||||
"write_override_ini": "segatools-override.ini"
|
||||
};
|
60
index.js
Normal file
60
index.js
Normal file
@ -0,0 +1,60 @@
|
||||
const cfg = require('./config');
|
||||
|
||||
const fs = require('fs');
|
||||
|
||||
const CCRelay = require('./ccrelay');
|
||||
const CCHub = require('./cchub');
|
||||
|
||||
|
||||
var hub = new CCHub('ws://' + cfg.server + ':' + cfg.server_port);
|
||||
var relays = {};
|
||||
var suffix = 0;
|
||||
var subnet = "";
|
||||
|
||||
hub.ready.then(() => {
|
||||
console.log('Connecting to CCM Hub ' + cfg.server + '...');
|
||||
hub.hello(cfg.login || {});
|
||||
});
|
||||
|
||||
hub.onPartyJoin = function(mysubnet, mysuffix) {
|
||||
suffix = mysuffix;
|
||||
subnet = mysubnet;
|
||||
console.log('Join successful. Game IP should be ' + subnet+suffix);
|
||||
console.log('Please wait until all match members have joined before starting game');
|
||||
if (cfg.write_override_ini) {
|
||||
var override =
|
||||
'[netenv]\n' +
|
||||
'enable=1\n' +
|
||||
'forcebind=1\n' +
|
||||
'addrSuffix=' + suffix + '\n' +
|
||||
'[keychip]\n' +
|
||||
'subnet=' + subnet + '0\n';
|
||||
fs.writeFileSync(cfg.write_override_ini, override);
|
||||
}
|
||||
}
|
||||
|
||||
hub.onPartyUpdate = function(members) {
|
||||
members.forEach(dst => {
|
||||
console.log(subnet+dst + ' joined virtual party.');
|
||||
var relay = new CCRelay(subnet+dst);
|
||||
relay.bindUDP(50200);
|
||||
relay.listenTCP(50200);
|
||||
relay.onReceive = function(sport, dport, type, msg) {
|
||||
hub.sendNetForward(suffix, dst, sport, dport, type, msg);
|
||||
}
|
||||
relays[dst] = relay;
|
||||
});
|
||||
}
|
||||
|
||||
hub.onNetForward = function(src, dst, sport, dport, type, msg) {
|
||||
var relay = relays[src];
|
||||
if (!relay) {
|
||||
console.warn('Received fwd from unknown host ' + src);
|
||||
return;
|
||||
}
|
||||
if (type == 'tcp') {
|
||||
relay.sendTCP(subnet+dst, sport, dport, msg)
|
||||
} else if (type == 'udp') {
|
||||
relay.sendUDP(subnet+dst, sport, dport, msg);
|
||||
}
|
||||
};
|
7
netif.bat
Normal file
7
netif.bat
Normal file
@ -0,0 +1,7 @@
|
||||
set NETIF="Ethernet"
|
||||
netsh interface ipv4 set interface interface=%NETIF% dhcpstaticipcoexistence=enabled
|
||||
netsh interface ipv4 add address %NETIF% 192.168.139.11 255.255.255.0 store=active
|
||||
netsh interface ipv4 add address %NETIF% 192.168.139.12 255.255.255.0 store=active
|
||||
netsh interface ipv4 add address %NETIF% 192.168.139.13 255.255.255.0 store=active
|
||||
netsh interface ipv4 add address %NETIF% 192.168.139.14 255.255.255.0 store=active
|
||||
pause
|
40
package-lock.json
generated
Normal file
40
package-lock.json
generated
Normal file
@ -0,0 +1,40 @@
|
||||
{
|
||||
"name": "chunichunimatch",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"dependencies": {
|
||||
"ws": "^8.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ws": {
|
||||
"version": "8.5.0",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.5.0.tgz",
|
||||
"integrity": "sha512-BWX0SWVgLPzYwF8lTzEy1egjhS4S4OEAHfsO8o65WOVsrnSRGaSiUaa9e0ggGlkMTtBlmOpEXiie9RUcBO86qg==",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"bufferutil": "^4.0.1",
|
||||
"utf-8-validate": "^5.0.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"bufferutil": {
|
||||
"optional": true
|
||||
},
|
||||
"utf-8-validate": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"ws": {
|
||||
"version": "8.5.0",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.5.0.tgz",
|
||||
"integrity": "sha512-BWX0SWVgLPzYwF8lTzEy1egjhS4S4OEAHfsO8o65WOVsrnSRGaSiUaa9e0ggGlkMTtBlmOpEXiie9RUcBO86qg==",
|
||||
"requires": {}
|
||||
}
|
||||
}
|
||||
}
|
5
package.json
Normal file
5
package.json
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"ws": "^8.5.0"
|
||||
}
|
||||
}
|
80
server.js
Normal file
80
server.js
Normal file
@ -0,0 +1,80 @@
|
||||
const cfg = require('./config');
|
||||
const ws = require('ws');
|
||||
const enc = require('./ccencap');
|
||||
|
||||
var server = new ws.WebSocketServer({
|
||||
port: cfg.server_port,
|
||||
perMessageDeflate: false
|
||||
});
|
||||
|
||||
var subnet = '192.168.139.';
|
||||
var occupancy = [0, 0, 0, 0];
|
||||
var suffixes = [11, 12, 13, 14];
|
||||
var clients = {};
|
||||
|
||||
function allocateClient() {
|
||||
var slot = -1;
|
||||
for (var i = 0; i < occupancy.length; i++) {
|
||||
if (!occupancy[i]) {
|
||||
slot = i;
|
||||
occupancy[slot] = 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return slot;
|
||||
}
|
||||
|
||||
server.on('connection', function(conn, req) {
|
||||
var slot = allocateClient();
|
||||
var realip = req.socket.remoteAddress;
|
||||
if (slot < 0) {
|
||||
console.log('Dropping ' + realip + ' due to no party vacancy');
|
||||
conn.close();
|
||||
return;
|
||||
}
|
||||
var suffix = suffixes[slot];
|
||||
clients[suffix] = conn;
|
||||
console.log("[Client #" + slot + "] Allocated. Welcome " + realip);
|
||||
conn.on('message', function(data) {
|
||||
var msg = enc.decode(data);
|
||||
switch(msg.op) {
|
||||
case 'party_hello':
|
||||
sendMemberChange();
|
||||
break;
|
||||
case 'net_tcp':
|
||||
case 'net_udp':
|
||||
var dstconn = clients[msg.dst];
|
||||
if (!dstconn) {
|
||||
console.warn('Invalid dst: ' + msg.src + '->' + msg.dst);
|
||||
} else {
|
||||
dstconn.send(data);
|
||||
}
|
||||
break;
|
||||
}
|
||||
});
|
||||
conn.on('close', function() {
|
||||
console.log("[Client #" + slot + "] Dropped.");
|
||||
delete clients[suffix];
|
||||
occupancy[slot] = 0;
|
||||
});
|
||||
conn.on('error', function(e) {
|
||||
console.log("[Client #" + slot + "] Error ", e);
|
||||
conn.close();
|
||||
delete clients[suffix];
|
||||
occupancy[slot] = 0;
|
||||
});
|
||||
});
|
||||
|
||||
function sendMemberChange() {
|
||||
// var members = suffixes.filter((suffix, slot) => occupancy[slot]);
|
||||
// Pre-allocate all members on the client to prevent late joiner crash
|
||||
var members = suffixes;
|
||||
for (var i = 0; i < occupancy.length; i++) {
|
||||
if (!occupancy[i]) continue;
|
||||
clients[suffixes[i]].send(enc.encodePartyUpdate({
|
||||
subnet: subnet,
|
||||
suffix: suffixes[i],
|
||||
members: members.filter(suffix => suffix != suffixes[i])
|
||||
}));
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user