Compare commits

...

6 Commits

12 changed files with 417 additions and 384 deletions

4
.gitignore vendored
View File

@ -1,3 +1,7 @@
# Minecraft-specific
server.jar
world/
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]

View File

@ -7,13 +7,14 @@ WORKDIR /usr/src/app
COPY requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt
# Prepare MC server
COPY server.jar ./
RUN java -jar server.jar --nogui
# # Prepare MC server
# COPY server.jar ./
# RUN java -jar server.jar --nogui
COPY . .
COPY src/ ./src/
COPY main.py ./
# Buffer Python's stdout for debugging during runtime
ENV PYTHONUNBUFFERED=1
CMD ["python", "main.py", "java", "-Xmx1024M", "-Xms1024M", "-jar", "server.jar", "nogui"]
CMD ["python", "main.py"]

View File

@ -6,25 +6,36 @@ This way, communication is enabled between a Minecraft server and a Matrix room.
## Setup
To create a Docker image, go to this folder and run the following command:
1. Create a Docker image using the following command:
```
docker build -t matrix-mc-server ./
```
Then, once a Docker image has been created, you can run the following command to run a Minecraft server. In `config.py`, you can find additional environment variables you can insert to alter settings on the Minecraft server.
2. Download the [latest Minecraft server jar file](https://www.minecraft.net/en-us/download/server).
```
docker run --name mc-server \
-p 25565:25565 \
-v "<folder in which your store your Minecraft world>":/usr/src/app/world \
-e EULA=true \
-e MATRIX_HOMESERVER='<your matrix homeserver>' \
-e MATRIX_USERNAME='<matrix bridge client username>' \
-e MATRIX_PASSWORD='<matrix bridge client password>' \
-e MC_CHANNEL='<channel in which you communicate>' \
-e SERVER_ADDRESS='<ip address where players can connect to the server>' \
mc-bridge
```
3. Open `config.yaml` and adjust it to however you like.
This should successfully launch your bridged Minecraft server.
**NOTE:** Make sure to set EULA to `true` otherwise the server will refuse to start.
4. Start the Docker container. You will need a few volumes:
- `/usr/src/app/config.yaml` - Location of your custom `config.yaml`
- `/usr/src/app/whitelist.json` - Location of your whitelist, if any.
- `/usr/src/app/world/` - Folder of your custom Minecraft world to load
- `/usr/src/app/server.jar` - (Default) location of the server jar file. Can be changed in `config.yaml`.
For example, your Docker compose file could look like the following:
```docker-compose
version: '3'
services:
matrix-mc-server:
image: matrix-mc-server:latest
volumes:
- <your config>:/usr/src/app/config.yaml
- <your whitelist>:/usr/src/app/whitelist.json
- <your world folder>:/usr/src/app/world
- <your server jar file>:/usr/src/app/server.jar
```

View File

@ -1,48 +0,0 @@
import os
import config
def rewriter(file_name):
"""Create a decorator that rewrites a file based on given settings."""
def exec(func):
"""Rewrite a file"""
new_lines = []
info_to_remember = {}
line_no = 0
with open(file_name, 'r', encoding='utf-8') as open_file:
for line in open_file:
line_no += 1
new_line = func(line, line_no, data=info_to_remember)
new_lines.append(new_line)
with open(file_name, 'w', encoding='utf-8') as write_file:
for line in new_lines:
write_file.write(line)
return exec
@rewriter('eula.txt')
def confirm_eula(line : str, line_no : int, data):
"""Confirm the Minecraft EULA"""
if not config.EULA:
return line
else:
return line.replace('eula=false', 'eula=true')
@rewriter('server.properties')
def fill_in_server_settings(line : str, line_no : int, data):
"""Set up the server based on our chosen properties"""
if line.strip().startswith('#'):
return line
key = line.split('=')[0]
server_settings = config.at(['minecraft']) or {}
try:
value = server_settings[key]
except IndexError:
return line
else:
return key + '=' + str(value) + '\n'

View File

@ -1,84 +0,0 @@
import os
import yaml
from typing import Any, List, Optional
with open('config.yaml', 'r') as open_file:
SETTINGS = yaml.load(open_file)
def at(keys : List[str]) -> Optional[Any]:
"""
Potentially get a value. If it doesn't exist, return None.
"""
return at_value(keys, SETTINGS)
def at_value(keys : List[str], value : Any) -> Optional[Any]:
try:
head, tail = keys[0], keys[1:]
except IndexError:
return value
else:
try:
new_value = value[head]
except TypeError:
return None
except KeyError:
return None
else:
return at_value(tail, new_value)
# EULA
EULA = at(['minecraft', 'eula']) or False
# Minecraft bridge credentials
MATRIX_HOMESERVER = at(['matrix', 'homeserver']) or "https://matrix.example.org/"
MATRIX_USERNAME = at(['matrix', 'username']) or "@alice:example.org"
MATRIX_PASSWORD = at(['matrix', 'password']) or "bridge_password"
# Matrix bridge room
MATRIX_ROOM = at(['matrix', 'room_id']) or "!channel_id:example.org"
SERVER_IP = os.getenv('SERVER_ADDRESS') or 'unknown ip'
# Matrix users who are allowed to run OP commands in Minecraft through Matrix
MC_ADMINS = [
"@bramvdnheuvel:nltrix.net", # Bram on Matrix (example, feel free to remove)
"@_discord_625632515314548736:t2bot.io" # Bram on Discord (example, feel free to remove)
# Your username on Matrix
]
if os.getenv('MATRIX_ADMINS') is not None:
MC_ADMINS = os.getenv('MATRIX_ADMINS').split(',')
make_bool = lambda os_value, default_value : default_value if not os_value else (
False if os_value.lower() == 'false' else True
)
SERVER_SETTINGS = {
'level-name': os.getenv('WORLD') or 'world',
# Server settings
'port' : 25565 if os.getenv('PORT') == None else int(os.getenv('PORT')),
'query.port' : 25565 if os.getenv('PORT') == None else int(os.getenv('PORT')),
'max-players' : 7 if os.getenv('MAX_PLAYERS') == None else int(os.getenv('MAX_PLAYERS')),
# Server temperature >:3
'view-distance' : 10 if os.getenv('RENDER_DISTANCE') == None else int(os.getenv('RENDER_DISTANCE')),
'enable-command-block' : make_bool(os.getenv('COMMAND_BLOCKS'), True),
# Environment
'allow-nether' : make_bool(os.getenv('NETHER'), True),
'spawn-npcs' : make_bool(os.getenv('NPCS'), True),
'spawn-animals' : make_bool(os.getenv('ANIMALS'), True),
'spawn-monsters' : make_bool(os.getenv('MONSTERS'), True),
# Gamemode
'pvp' : make_bool(os.getenv('PVP'), True),
'gamemode' : os.getenv('GAMEMODE') or 'survival',
'difficulty': os.getenv('DIFFICULTY') or 'medium',
'hardcore' : make_bool(os.getenv('HARDCORE'), False),
# Grief protection
'online-mode' : make_bool(os.getenv('VERIFY_ACCOUNTS'), True),
'white-list' : make_bool(os.getenv('WHITELIST'), True),
'enforce-whitelist' : make_bool(os.getenv('WHITELIST'), True),
'spawn-protection' : 16 if os.getenv('SPAWN_PROTECTION') == None else os.getenv('SPAWN_PROTECTION'),
}

View File

@ -1,3 +1,16 @@
config:
# How much memory the Minecraft server is allowed to take up
# Number in megabytes. Defaults to 1024. (= 1 GB)
ram: 1024
# File location of the server jar.
# To be downloaded at: https://www.minecraft.net/en-us/download/server
server_jar: server.jar
# Confirm the Minecraft EULA. https://account.mojang.com/documents/minecraft_eula
# Defaults to false, but is required to run the server.
# eula: true
# Matrix bridge configurations
matrix:
# Homeserver URL
@ -14,14 +27,16 @@ matrix:
server_address: unknown ip
# List of Matrix users that can send commands to the bridge.
# When a message starts with a slash, (/) the bridge will interpret it as a
# Minecraft command and will put that as a command into the console.
# When a message starts with an exclamation mark, (!) the bridge will
# interpret it as a Minecraft command and will put that as a command
# into the console.
mc-admins:
- "@bram:matrix.directory"
# - "@alice:example.org"
# -
# When users have bridged from other platforms, you can indicate accordingly.
# When multiple RegEx strings apply, all are included.
alternative_platforms:
Discord:
match: "@_?discord_\d+:.+"
@ -34,8 +49,6 @@ matrix:
# Settings that directly affect running the Minecraft server.
minecraft:
# Confirm the Minecraft EULA. Defaults to false.
# eula: true
# --------------------
# MINECRAFT SERVER

74
main.py
View File

@ -1,67 +1,7 @@
import asyncio
import time
import re
from nio import AsyncClient, MatrixRoom, RoomMessageText
import mc_wrapper
import config
import build_server
STARTUP_TIME = time.time()
client = AsyncClient(config.MATRIX_HOMESERVER, config.MATRIX_USERNAME)
async def message_callback(room: MatrixRoom, event: RoomMessageText) -> None:
if room.machine_name != config.MATRIX_ROOM:
return
if event.sender == client.user_id:
return
if int(event.server_timestamp) < STARTUP_TIME:
return
# Determine platform
platform = 'Matrix'
if re.fullmatch(r"@_discord_\d+:t2bot\.io", event.sender):
platform = 'Discord'
# Determine how to display username
name = room.users[event.sender].display_name
for user in room.users:
if user == event.sender:
continue
if room.users[user].display_name == name:
name = room.users[event.sender].disambiguated_name
break
mc_wrapper.reply_to_mc(
event.body, name,
admin=(event.sender in config.MC_ADMINS),
platform=platform
)
client.add_event_callback(message_callback, RoomMessageText)
async def activate_client() -> None:
print(await client.login(config.MATRIX_PASSWORD))
await client.room_send(
room_id=config.MC_CHANNEL,
message_type="m.room.message",
content = {
"msgtype": "m.text",
"body": "Starting Minecraft-Matrix bridge...",
"format": "org.matrix.custom.html",
"formatted_body": "<strong>Starting Minecraft-Matrix bridge...</strong>"
}
)
await client.sync_forever(timeout=30000) # milliseconds
async def start():
await asyncio.gather(
activate_client(),
mc_wrapper.start(client, config.MC_CHANNEL)
)
asyncio.run(start())
import src.mc_wrapper as mc_wrapper
import src.mxclient as matrix_client
import asyncio
# Start the Minecraft process
asyncio.run(matrix_client.start())

25
src/build_server.py Normal file
View File

@ -0,0 +1,25 @@
"""
This module prepares the necessary files for running the server in the
correct configuration.
"""
import src.config as config
def write_eula():
"""
Write whether the user accepts to Minecraft's EULA.
The server refuses to run unless explicitly accepted.
"""
with open("eula.txt", 'w') as fp:
if config.EULA == True:
fp.write("eula=true")
else:
fp.write("eula=false")
def write_server_properties():
"""
Write the configuration for the Minecraft world.
"""
with open("server.properties", 'w') as fp:
for key, value in config.at(['minecraft']).items():
fp.write(f"{key}={value}\n")

62
src/config.py Normal file
View File

@ -0,0 +1,62 @@
"""
This module loads and parses the config.yaml file.
"""
from typing import Any, List, Optional
import yaml
with open('config.yaml', 'r') as open_file:
SETTINGS = yaml.load(open_file)
def at(keys : List[str]) -> Optional[Any]:
"""
Potentially get a value. If it doesn't exist, return None.
"""
return at_value(keys, SETTINGS)
def at_value(keys : List[str], value : Any) -> Optional[Any]:
try:
head, tail = keys[0], keys[1:]
except IndexError:
return value
else:
try:
new_value = value[head]
except TypeError:
return None
except KeyError:
return None
else:
return at_value(tail, new_value)
# EULA
EULA = at(['config', 'eula']) or False
# Minecraft bridge credentials
MATRIX_HOMESERVER = at(['matrix', 'homeserver']) or "https://matrix.example.org/"
MATRIX_USERNAME = at(['matrix', 'username']) or "@alice:example.org"
MATRIX_PASSWORD = at(['matrix', 'password']) or "bridge_password"
# Matrix bridge room
MATRIX_ROOM = at(['matrix', 'room_id']) or "!channel_id:example.org"
SERVER_IP = at(['matrix', 'server_address']) or 'unknown ip'
MATRIX_ADMINS = at(['matrix', 'mc-admins']) or []
try:
RAM_SIZE = int(at(['config', 'ram']))
except TypeError:
RAM_SIZE = 1024
except ValueError:
RAM_SIZE = 1024
SERVER_JAR_LOCATION = at(['config', 'server_jar']) or 'server.jar'
RUN_COMMAND = [
'java',
f'-Xmx{RAM_SIZE}M',
f'-Xms{RAM_SIZE}M',
'-jar', SERVER_JAR_LOCATION,
'nogui'
]

View File

@ -1,124 +1,160 @@
from subprocess import Popen, PIPE
from typing import Union
import asyncio
import json
import sys
import re
from nbsr import NonBlockingStreamReader as NBSR
import config
# run the shell as a subprocess:
p = Popen(sys.argv[1:],
stdin = PIPE, stdout = PIPE, stderr = PIPE, shell = False)
# wrap p.stdout with a NonBlockingStreamReader object:
nbsr = NBSR(p.stdout)
async def start(client, mc_channel):
await asyncio.sleep(3)
while True:
output = nbsr.readline(0.1)
# 0.1 secs to let the shell output the result
if not output:
await asyncio.sleep(1)
else:
try:
sentence = output.decode("utf-8").strip()
except UnicodeDecodeError:
print("Could not decode sentence:")
print(output)
else:
print(sentence)
plain_text, sentence = process_message(sentence)
if sentence is not None:
print("[Matrix] " + plain_text)
# Send terminal message to Matrix
await client.room_send(
room_id=mc_channel,
message_type="m.room.message",
content = {
"msgtype": "m.text",
"body": plain_text,
"format": "org.matrix.custom.html",
"formatted_body": sentence
}
)
server_live = False
def process_message(sentence : str) -> Union[str, None]:
global server_live
if (match := re.fullmatch(
r"\[[\d:]+\] \[Server thread\/INFO\]: Preparing level \"(.+)\""
, sentence)):
level, = match.groups()
return f"Preparing level {level}...", "<strong>Preparing level \"" + level + "\"...</strong>"
if re.fullmatch(
r"\[[\d:]+\] \[Server thread\/INFO\]: Done \(\d+.?\d*s\)! For help, type \"help\"",
sentence):
server_live = True
return f"The Minecraft server is live. The server is reacable at <code>{config.SERVER_IP}</code>.", f"The minecraft server is live. The server is reacable at <code>{config.SERVER_IP}</code>."
if re.fullmatch(
r"\[[\d:]+\] \[Server thread\/INFO\]: Stopping server",
sentence):
return "The server has stopped.", "<strong>The server has stopped.</strong>"
if not server_live:
return None, None
if (match := re.fullmatch(
r"\[[\d:]+\] \[Server thread\/INFO\]: ([A-Za-z0-9_]{3,16}) joined the game",
sentence)):
username, = match.groups()
return username + " joined the Minecraft server", (
"<strong>" + username + " joined the Minecraft server</strong>")
if (match := re.fullmatch(
r"\[[\d:]+\] \[Server thread\/INFO\]: ([A-Za-z0-9_]{3,16}) left the game",
sentence)):
username, = match.groups()
return username + " left the Minecraft server", (
"<strong>" + username + " left the Minecraft server</strong>")
if (match := re.fullmatch(
r"\[[\d:]+\] \[Server thread\/INFO\]: <([A-Za-z0-9_]{3,16})> (.+)",
sentence)):
username, message = match.groups()
return username + ": " + message, (
"<strong>" + username + "</strong>: " + message
)
if (match := re.fullmatch(
r"\[[\d:]+\] \[Server thread\/INFO\]: ([A-Za-z0-9_]{3,16}) ([\w\[\]\-\. !?,]+)",
sentence)):
message = " ".join(match.groups())
return message, "<strong>" + message + "</strong>"
return None, None
def reply_to_mc(message : str, author : str,
admin : bool = False, platform : str = 'Matrix'):
"""
Send something back to the Minecraft terminal.
"""
if admin and message.startswith('!'):
p.stdin.write((message[1:] + "\r\n").encode())
else:
msg = [
"",
dict(text="M", color="red"),
dict(text="D", color="aqua") if platform == 'Discord' else None,
dict(text=f" <{author}> {message}")
]
p.stdin.write(
("execute as @a run tellraw @s " + json.dumps([m for m in msg if m is not None]) + "\r\n").encode()
)
p.stdin.flush()
if __name__ == '__main__':
"""
The mc_wrapper modules handles the process that runs the Minecraft server.
The server is run in a subprocess, and live communication between the
game and the Python script is facilitated.
"""
from subprocess import Popen, PIPE
from typing import Union
import asyncio
import json
import re
from src.nbsr import NonBlockingStreamReader as NBSR
import src.config as config
import src.build_server as build
# Write the appropriate files
build.write_eula()
build.write_server_properties()
# run the shell as a subprocess:
p = Popen(config.RUN_COMMAND,
stdin = PIPE, stdout = PIPE, stderr = PIPE, shell = False)
# wrap p.stdout with a NonBlockingStreamReader object:
nbsr = NBSR(p.stdout)
async def start(client, mc_channel):
"""
Start reading from the Minecraft subprocess.
"""
await asyncio.sleep(3)
while True:
output = nbsr.readline(0.1)
# 0.1 secs to let the shell output the result
if not output:
await asyncio.sleep(1)
else:
try:
sentence = output.decode("utf-8").strip()
except UnicodeDecodeError:
print("Could not decode sentence:")
print(output)
else:
print(sentence)
plain_text, sentence = process_message(sentence)
if sentence is not None:
print("[Matrix] " + plain_text)
# Send terminal message to Matrix
await client.room_send(
room_id=mc_channel,
message_type="m.room.message",
content = {
"msgtype": "m.text",
"body": plain_text,
"format": "org.matrix.custom.html",
"formatted_body": sentence
}
)
server_live = False
def process_message(sentence : str) -> Union[str, None]:
"""
Process a message that is sent to stdout in the Minecraft terminal.
If this function deems it relevant, it returns a string that can be
sent to Matrix.
"""
global server_live
if (match := re.fullmatch(
r"\[[\d:]+\] \[Server thread\/INFO\]: Preparing level \"(.+)\""
, sentence)):
level, = match.groups()
return f"Preparing level {level}...", "<strong>Preparing level \"" + level + "\"...</strong>"
if re.fullmatch(
r"\[[\d:]+\] \[Server thread\/INFO\]: Done \(\d+.?\d*s\)! For help, type \"help\"",
sentence):
server_live = True
return f"The Minecraft server is live. The server is reacable at <code>{config.SERVER_IP}</code>.", f"The minecraft server is live. The server is reacable at <code>{config.SERVER_IP}</code>."
if re.fullmatch(
r"\[[\d:]+\] \[Server thread\/INFO\]: Stopping server",
sentence):
return "The server has stopped.", "<strong>The server has stopped.</strong>"
if not server_live:
return None, None
if (match := re.fullmatch(
r"\[[\d:]+\] \[Server thread\/INFO\]: ([A-Za-z0-9_]{3,16}) joined the game",
sentence)):
username, = match.groups()
return username + " joined the Minecraft server", (
"<strong>" + username + " joined the Minecraft server</strong>")
if (match := re.fullmatch(
r"\[[\d:]+\] \[Server thread\/INFO\]: ([A-Za-z0-9_]{3,16}) left the game",
sentence)):
username, = match.groups()
return username + " left the Minecraft server", (
"<strong>" + username + " left the Minecraft server</strong>"
)
if (match := re.fullmatch(
r"\[[\d:]+\] \[Server thread\/INFO\]: <([A-Za-z0-9_]{3,16})> (.+)",
sentence)):
username, message = match.groups()
return username + ": " + message, (
"<strong>" + username + "</strong>: " + message
)
if (match := re.fullmatch(
r"\[[\d:]+\] \[Server thread\/INFO\]: ([A-Za-z0-9_]{3,16}) ([\w\[\]\-\. !?,]+)",
sentence)):
message = " ".join(match.groups())
return message, "<strong>" + message + "</strong>"
return None, None
def reply_to_mc(message : str, display_name : str, sender : str):
"""
Send something back to the Minecraft terminal.
"""
if sender in config.MATRIX_ADMINS and message.startswith('!'):
p.stdin.write((message[1:] + "\r\n").encode())
else:
p.stdin.write(
("execute as @a run tellraw @s " + format(sender, display_name, message) + "\r\n").encode()
)
p.stdin.flush()
def format(sender : str, display_name : str, message : str) -> str:
"""
Create a string used to format the user's message.
"""
start = [ "", dict(text="M", color="red" ) ]
end = [ dict(text=f" <{display_name}> {message}") ]
options = config.at(['matrix', 'alternative_platforms']) or {}
for platform, details in options.items():
try:
regex = details['match']
text = details['text']
color = details['color']
except KeyError:
print("WARNING: Platform `" + platform + "` is missing some configurations.")
else:
if re.fullmatch(regex, sender):
start.append(dict(text=text, color=color))
return json.dumps(start + end)
if __name__ == '__main__':
asyncio.run(start())

61
src/mxclient.py Normal file
View File

@ -0,0 +1,61 @@
import asyncio
import time
import re
from nio import AsyncClient, MatrixRoom, RoomMessageText
import src.mc_wrapper as mc_wrapper
import src.config as config
STARTUP_TIME = time.time()
client = AsyncClient(config.MATRIX_HOMESERVER, config.MATRIX_USERNAME)
async def message_callback(room: MatrixRoom, event: RoomMessageText) -> None:
if room.machine_name != config.MATRIX_ROOM:
return
if event.sender == client.user_id:
return
if int(event.server_timestamp) < STARTUP_TIME:
return
# Determine how to display username
name = room.users[event.sender].display_name
for user in room.users:
if user == event.sender:
continue
if room.users[user].display_name == name:
name = room.users[event.sender].disambiguated_name
break
mc_wrapper.reply_to_mc(event.body, name, event.sender)
client.add_event_callback(message_callback, RoomMessageText)
async def activate_client() -> None:
print(await client.login(config.MATRIX_PASSWORD))
await client.room_send(
room_id=config.MATRIX_ROOM,
message_type="m.room.message",
content = {
"msgtype": "m.text",
"body": "Starting Minecraft-Matrix bridge...",
"format": "org.matrix.custom.html",
"formatted_body": "<strong>Starting Minecraft-Matrix bridge...</strong>"
}
)
await client.sync_forever(timeout=30000) # milliseconds
async def start():
await asyncio.gather(
# Start the Matrix client
activate_client(),
# Start the Minecraft subprocess
mc_wrapper.start(client, config.MATRIX_ROOM)
)
if __name__ == '__main__':
asyncio.run(start())

View File

@ -1,39 +1,51 @@
from threading import Thread
from queue import Queue, Empty
class NonBlockingStreamReader:
def __init__(self, stream):
'''
stream: the stream to read from.
Usually a process' stdout or stderr.
'''
self._s = stream
self._q = Queue()
def _populateQueue(stream, queue):
'''
Collect lines from 'stream' and put them in 'quque'.
'''
while True:
line = stream.readline()
if line:
queue.put(line)
else:
raise UnexpectedEndOfStream
self._t = Thread(target = _populateQueue,
args = (self._s, self._q))
self._t.daemon = True
self._t.start() #start collecting lines from the stream
def readline(self, timeout = None):
try:
return self._q.get(block = timeout is not None,
timeout = timeout)
except Empty:
return None
"""
The NBSR module defines a Non-blocking stream reader (NBSR class).
In short, the Minecraft stdout is a stream of data that doesn't end until
the server shuts down. Traditionally, Python would not run any code until
the server has shut down and returns its entire output.
The NBSR class allows us to read from the stream without blocking the entire
Python script. We will occasionally ask the NBSR for any updates, and it
will give us the latest output, if it exists.
"""
from threading import Thread
from queue import Queue, Empty
class NonBlockingStreamReader:
def __init__(self, stream):
'''
stream: the stream to read from.
Usually a process' stdout or stderr.
'''
self._s = stream
self._q = Queue()
def _populateQueue(stream, queue):
'''
Collect lines from 'stream' and put them in 'quque'.
'''
while True:
line = stream.readline()
if line:
queue.put(line)
else:
raise UnexpectedEndOfStream
self._t = Thread(target = _populateQueue,
args = (self._s, self._q))
self._t.daemon = True
self._t.start() #start collecting lines from the stream
def readline(self, timeout = None):
try:
return self._q.get(block = timeout is not None,
timeout = timeout)
except Empty:
return None
class UnexpectedEndOfStream(Exception): pass