Skip to content

Commit

Permalink
Merge pull request #7 from obstudio/develop
Browse files Browse the repository at this point in the history
Version 0.2.0
  • Loading branch information
NN708 authored Dec 4, 2019
2 parents 899e503 + 629fca1 commit 57cb682
Show file tree
Hide file tree
Showing 7 changed files with 185 additions and 56 deletions.
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
node_modules/
config.json
locale/zh-cn/custom.json
locale/en-us/custom.json
54 changes: 46 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,21 +10,43 @@ A chat relay between Minecraft server and IRC with no mods.
+ Support vanilla and paper servers
+ Can be run on Windows, Linux and macOS

### Chat bot support

+ [Koishi](https://koishijs.github.io/) (for QQ)

### Messages can be forwarded

+ Chat
+ `/say` message
+ Server start up / shutdown
+ Player join / leave
+ Player death
+ Achievement

## Usage

1. Install [NodeJS](https://nodejs.org/)
1. Clone this repository
2. Create `config.json` and write configurations in it
3. Run `index.js` using `node .`
1. Install [NodeJS](https://nodejs.org/).
2. Install MC-Repeater globally using `npm install -g mcrepeater`.
3. In your Minecraft server directory, create a bash / batch file (e.g. `start.sh` or `start.bat`), then write your Minecraft start command in it. For Java servers, it looks like:
```
java -Xmx1024M -Xms1024M -jar server.jar nogui
```
For Bedrock servers, it's usually:
```
LD_LIBRARY_PATH=. ./bedrock_server
```
4. Create `config.json` in the same directory and write [configurations](#configurations) in it.
5. Run MC-Repeater using the command `mcrepeater`. You don't need to start Minecraft server manually because it's automatically started by MC-Repeater.

## Configurations

Here's an example `config.json` for Koishi bot:
Here's a simple example `config.json` for Koishi:

```json
{
"logFile": "/path/to/mc-server/logs/latest.log",
"serverStart": "start.sh",
"serverType": "java",
"botType": "koishi",
"botHost": "bot.your-host.com",
"botPath": "/bot/request/path",
"key": "your-secret-key",
Expand All @@ -34,9 +56,25 @@ Here's an example `config.json` for Koishi bot:

### Parameters

+ **logFile:** Path to your Minecraft server latest log file.
#### Minecraft server configurations:

+ **serverStart:** Path to your Minecraft server starting bash / batch file.
+ **serverType:** Type of your server. Can be `java` or `paper`.
+ **autoRestart (optional):** A boolean value which determine whether the MC-Repeater will auto restart your server after your server crashed (default: `false`).

#### Chat bot configurations:

+ **botType:** Your bot type which determines how the message will be sent. Can be `koishi` or `local` (for debugging).
+ **botHost:** Hostname of your bot server.
+ **botPath:** Your request path to send information to. For Koishi, it's usually `/webhook/channel/your-channel`.
+ **key:** Your secret key to sign the information. Usually provided by your bot.
+ **language:** Your language. Currently support `en-us` and `zh-cn`.
+ **language:** Your language. Currently support `en-us` and `zh-cn`.

#### Network optimization:

+ **throttleInterval (optional):** The minimum interval at which messages are sent (default: `0`).
+ **offlineTimeout (optional):** The minimum time to determine a player is offline (default: `0`).

## License

Licensed under the [MIT](https://github.com/obstudio/MC-Repeater/blob/master/LICENSE) License.
95 changes: 73 additions & 22 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,35 +1,86 @@
const fs = require('fs')
#!/usr/bin/env node

const Iconv = require('iconv').Iconv
const config = require('./config')
const parse = require('./parse')
const send = require('./send')
const os = require('os')
const process = require('process')
const child_process = require('child_process')
const config = require(process.cwd() + '/config')

const OFFLINE_TIMEOUT = (config.offlineTimeout || 0) * 1000

const gbk2utf8 = new Iconv('GBK', 'UTF-8')
const offlinePlayers = new Set()

fs.watchFile(config.logFile, (curr, prev) => {
if (curr.size - prev.size > 0) {
fs.open(config.logFile, 'r', (err, fd) => {
if (err) throw err
const isWindows = os.type() === 'Windows_NT'

buffer = Buffer.alloc(curr.size - prev.size)
fs.read(fd, buffer, 0, curr.size - prev.size, prev.size, (err, bytesRead, buffer) => {
if (err) throw err
let serverProcess

let content
if (os.type() === 'Windows_NT') {
content = gbk2utf8.convert(buffer).toString().split('\r\n').filter(s => s)
} else {
content = buffer.toString().split('\n').filter(s => s)
let autoRestart = config.autoRestart
function newServerProcess() {
return child_process.execFile(config.serverStart, { encoding: 'buffer' }, (error) => {
if (error) {
throw error
}
serverProcessStopped()
})
}

function serverProcessInit() {
//when serverProcess have output message
serverProcess.stdout.on('data', (data) => {
const content = isWindows ? gbk2utf8.convert(data).toString().trim() : data.toString().trim()
if (content) {
console.log(content)
}
const info = parse(content)
if (info) {
if (info.type === 'leave' && OFFLINE_TIMEOUT) {
offlinePlayers.add(info.target)
setTimeout(() => {
if (offlinePlayers.delete(info.target)) {
try {
send(info.message)
} catch (error) {
console.log(error)
}
}
}, OFFLINE_TIMEOUT)
} else {
if (info.type === 'join') {
if (offlinePlayers.delete(info.target)) return
}
try {
send(info.message)
} catch (error) {
console.log(error)
}
}
}
})

info = content.map(parse).filter(s => s)
info.forEach(send)
})
//send the parent process input to serverProcess input
process.stdin.pipe(serverProcess.stdin)
}

fs.close(fd, (err) => {
if (err) throw err
})
})
function serverProcessStopped() {
if (autoRestart) {
console.log('Server is restarting.')
serverProcess = newServerProcess()
serverProcessInit()
} else {
console.log('MC-Repeater stopped.')
process.exit()
}
})
}

process.stdin.on('data', (data) => {
if (data.toString().trim() === 'stop') {
autoRestart = false
}
})

//create mc server child process
serverProcess = newServerProcess()
serverProcessInit()
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
{
"name": "mcrepeater",
"version": "0.1.1",
"version": "0.2.0",
"description": "A chat relay between Minecraft server and IRC with no mods.",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"bin": {
"mcrepeater": "index.js"
},
"repository": {
"type": "git",
"url": "git+https://github.com/obstudio/MC-Repeater.git"
Expand Down
37 changes: 27 additions & 10 deletions parse.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
const config = require('./config')
const process = require('process')
const config = require(process.cwd() + '/config')
const locale = require('./locale')

const lang = locale[config.language.trim().toLowerCase()]
Expand Down Expand Up @@ -309,45 +310,55 @@ function parseJava(text) {
const regJoin = new RegExp(`^(${vchar}+) joined the game$`)
const regLeave = new RegExp(`^(${vchar}+) left the game$`)
const regStart = /^Done \([0-9\.]+s\)! For help, type "help"$/
const regStop = /^Stopping the server$/
const regStop = /^Stopping server$/
const regMsg = new RegExp(`^<(${vchar}+)> (.+)$`)
const regServerMsg = new RegExp(`^\\[(${vchar}+)\\] (.+)$`)
const regAdvance = new RegExp(`^(${vchar}+) has (?:made the advancement|reached the goal|completed the challenge) \\[(.+)\\]$`)
info = undefined
if (res = regJoin.exec(text)) {
info = {
type: 'join',
target: res[1],
message: translate(lang.join, res)
}
} else if (res = regLeave.exec(text)) {
info = {
type: 'leave',
target: res[1],
message: translate(lang.leave, res)
}
} else if (regStart.exec(text)) {
info = {
type: 'start',
message: lang.start
}
} else if (regStop.exec(text)) {
info = {
type: 'stop',
message: lang.stop
}
} else if (res = regMsg.exec(text)) {
info = {
type: 'chat',
message: translate(lang.msg, res)
}
} else if (res = regServerMsg.exec(text)) {
if (res[1] === 'Server') {
res[1] = lang.server
}
info = {
type: 'server',
message: translate(lang.msg, res)
}
} else if (res = regAdvance.exec(text)) {
res[2] = lang.advancements[res[2]]
info = {
type: 'advance',
message: translate(lang.makeAdvance, res)
}
} else if (res = parseDeath(text)) {
info = {
type: 'death',
message: translate(lang.deathReasons[res[0]], res)
}
}
Expand All @@ -359,7 +370,7 @@ function parsePaper(text) {

let info

const serverInfo = /^\[\d{2}:\d{2}:\d{2}\] \[Server thread\/INFO\]: (.+)$/
const serverInfo = /^\[\d{2}:\d{2}:\d{2} INFO\]: (.+)$/
let serverInfoResult = serverInfo.exec(text)

const chatInfo = /^\[\d{2}:\d{2}:\d{2}\] \[Async Chat Thread - #\d+\/INFO\]: (.+)$/
Expand All @@ -373,47 +384,53 @@ function parsePaper(text) {
const regStop = /^Stopping server$/
const regServerMsg = new RegExp(`^\\[(${vchar}+)\\] (.+)$`)
const regAdvance = new RegExp(`^(${vchar}+) has (?:made the advancement|reached the goal|completed the challenge) \\[(.+)\\]$`)
const regMsg = new RegExp(`^<(${vchar}+)> (.+)$`)

if (serverInfoResult = regJoin.exec(text)) {
info = {
type: 'join',
target: serverInfoResult[1],
message: translate(lang.join, serverInfoResult)
}
} else if (serverInfoResult = regLeave.exec(text)) {
info = {
type: 'leave',
target: serverInfoResult[1],
message: translate(lang.leave, serverInfoResult)
}
} else if (regStart.exec(text)) {
info = {
type: 'start',
message: lang.start
}
} else if (regStop.exec(text)) {
info = {
type: 'stop',
message: lang.stop
}
} else if (serverInfoResult = regServerMsg.exec(text)) {
if (serverInfoResult[1] === 'Server') {
serverInfoResult[1] = lang.server
}
info = {
type: 'server',
message: translate(lang.msg, serverInfoResult)
}
} else if (serverInfoResult = regAdvance.exec(text)) {
serverInfoResult[2] = lang.advancements[serverInfoResult[2]]
info = {
type: 'advance',
message: translate(lang.makeAdvance, serverInfoResult)
}
} else if (serverInfoResult = parseDeath(text)) {
info = {
type: 'death',
message: translate(lang.deathReasons[serverInfoResult[0]], serverInfoResult)
}
}
} else if (chatInfoResult) {
text = chatInfoResult[1]
const regMsg = new RegExp(`^<(${vchar}+)> (.+)$`)

if (chatInfoResult = regMsg.exec(text)) {
} else if (serverInfoResult = regMsg.exec(text)) {
info = {
message: translate(lang.msg, chatInfoResult)
type: 'chat',
message: translate(lang.msg, serverInfoResult)
}
}
}
Expand Down
Loading

0 comments on commit 57cb682

Please sign in to comment.