Merge pull request #4 from TargetedEntropy/Skelmis-master

Merging Skelmis master
This commit is contained in:
TargetedEntropy 2023-03-20 21:15:22 -04:00 committed by GitHub
commit 742d7c5e57
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 747 additions and 645 deletions

View File

@ -16,7 +16,7 @@ communication with a MineCraft server.
Detailed information for developers can be found here:
`<http://pycraft.readthedocs.org/en/latest/>`_.
``start.py`` is a basic example of a headless client using the library
``start.py`` is a basic example of a headless client using the library that can be found under the `examples` folder.
Use ``start.py --help`` for the options.
Supported Minecraft versions

View File

@ -1,192 +1,20 @@
# Makefile for Sphinx documentation
# Minimal makefile for Sphinx documentation
#
# You can set these variables from the command line.
SPHINXOPTS =
SPHINXBUILD = sphinx-build
PAPER =
# You can set these variables from the command line, and also
# from the environment for the first two.
SPHINXOPTS ?=
SPHINXBUILD ?= sphinx-build
SOURCEDIR = .
BUILDDIR = _build
# User-friendly check for sphinx-build
ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1)
$(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/)
endif
# Internal variables.
PAPEROPT_a4 = -D latex_paper_size=a4
PAPEROPT_letter = -D latex_paper_size=letter
ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
# the i18n builder cannot share the environment and doctrees with the others
I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest coverage gettext
# Put it first so that "make" without argument is like "make help".
help:
@echo "Please use \`make <target>' where <target> is one of"
@echo " html to make standalone HTML files"
@echo " dirhtml to make HTML files named index.html in directories"
@echo " singlehtml to make a single large HTML file"
@echo " pickle to make pickle files"
@echo " json to make JSON files"
@echo " htmlhelp to make HTML files and a HTML help project"
@echo " qthelp to make HTML files and a qthelp project"
@echo " applehelp to make an Apple Help Book"
@echo " devhelp to make HTML files and a Devhelp project"
@echo " epub to make an epub"
@echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
@echo " latexpdf to make LaTeX files and run them through pdflatex"
@echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx"
@echo " text to make text files"
@echo " man to make manual pages"
@echo " texinfo to make Texinfo files"
@echo " info to make Texinfo files and run them through makeinfo"
@echo " gettext to make PO message catalogs"
@echo " changes to make an overview of all changed/added/deprecated items"
@echo " xml to make Docutils-native XML files"
@echo " pseudoxml to make pseudoxml-XML files for display purposes"
@echo " linkcheck to check all external links for integrity"
@echo " doctest to run all doctests embedded in the documentation (if enabled)"
@echo " coverage to run coverage check of the documentation (if enabled)"
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
clean:
rm -rf $(BUILDDIR)/*
.PHONY: help Makefile
html:
$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
@echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
dirhtml:
$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
@echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
singlehtml:
$(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
@echo
@echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
pickle:
$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
@echo
@echo "Build finished; now you can process the pickle files."
json:
$(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
@echo
@echo "Build finished; now you can process the JSON files."
htmlhelp:
$(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
@echo
@echo "Build finished; now you can run HTML Help Workshop with the" \
".hhp project file in $(BUILDDIR)/htmlhelp."
qthelp:
$(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
@echo
@echo "Build finished; now you can run "qcollectiongenerator" with the" \
".qhcp project file in $(BUILDDIR)/qthelp, like this:"
@echo "# qcollectiongenerator $(BUILDDIR)/qthelp/pyCraft.qhcp"
@echo "To view the help file:"
@echo "# assistant -collectionFile $(BUILDDIR)/qthelp/pyCraft.qhc"
applehelp:
$(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp
@echo
@echo "Build finished. The help book is in $(BUILDDIR)/applehelp."
@echo "N.B. You won't be able to view it unless you put it in" \
"~/Library/Documentation/Help or install it in your application" \
"bundle."
devhelp:
$(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
@echo
@echo "Build finished."
@echo "To view the help file:"
@echo "# mkdir -p $$HOME/.local/share/devhelp/pyCraft"
@echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/pyCraft"
@echo "# devhelp"
epub:
$(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
@echo
@echo "Build finished. The epub file is in $(BUILDDIR)/epub."
latex:
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo
@echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
@echo "Run \`make' in that directory to run these through (pdf)latex" \
"(use \`make latexpdf' here to do that automatically)."
latexpdf:
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo "Running LaTeX files through pdflatex..."
$(MAKE) -C $(BUILDDIR)/latex all-pdf
@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
latexpdfja:
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo "Running LaTeX files through platex and dvipdfmx..."
$(MAKE) -C $(BUILDDIR)/latex all-pdf-ja
@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
text:
$(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
@echo
@echo "Build finished. The text files are in $(BUILDDIR)/text."
man:
$(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
@echo
@echo "Build finished. The manual pages are in $(BUILDDIR)/man."
texinfo:
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
@echo
@echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
@echo "Run \`make' in that directory to run these through makeinfo" \
"(use \`make info' here to do that automatically)."
info:
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
@echo "Running Texinfo files through makeinfo..."
make -C $(BUILDDIR)/texinfo info
@echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
gettext:
$(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
@echo
@echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
changes:
$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
@echo
@echo "The overview file is in $(BUILDDIR)/changes."
linkcheck:
$(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
@echo
@echo "Link check complete; look for any errors in the above output " \
"or in $(BUILDDIR)/linkcheck/output.txt."
doctest:
$(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
@echo "Testing of doctests in the sources finished, look at the " \
"results in $(BUILDDIR)/doctest/output.txt."
coverage:
$(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage
@echo "Testing of coverage in the sources finished, look at the " \
"results in $(BUILDDIR)/coverage/python.txt."
xml:
$(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml
@echo
@echo "Build finished. The XML files are in $(BUILDDIR)/xml."
pseudoxml:
$(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml
@echo
@echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml."
# Catch-all target: route all unknown targets to Sphinx using the new
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
%: Makefile
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)

View File

@ -20,44 +20,45 @@ import shlex
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
sys.path.insert(0, os.path.abspath('../'))
sys.path.insert(0, os.path.abspath("../"))
# -- General configuration ------------------------------------------------
# If your documentation needs a minimal Sphinx version, state it here.
#needs_sphinx = '1.0'
# needs_sphinx = '1.0'
# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = [
'sphinx.ext.autodoc',
'sphinx.ext.intersphinx',
'sphinx.ext.todo',
'sphinx.ext.coverage',
'sphinx.ext.viewcode',
"sphinx.ext.autodoc",
"sphinx.ext.intersphinx",
"sphinx.ext.todo",
"sphinx.ext.coverage",
"sphinx.ext.viewcode",
"sphinx.ext.napoleon",
]
autoclass_content = 'both'
autoclass_content = "both"
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
templates_path = ["_templates"]
# The suffix(es) of source filenames.
# You can specify multiple suffix as a list of string:
# source_suffix = ['.rst', '.md']
source_suffix = '.rst'
source_suffix = ".rst"
# The encoding of source files.
#source_encoding = 'utf-8-sig'
# source_encoding = 'utf-8-sig'
# The master toctree document.
master_doc = 'index'
master_doc = "index"
# General information about the project.
project = u'pyCraft'
copyright = u'2015, Ammar Askar'
author = u'Ammar Askar'
project = u"pyCraft"
copyright = u"2015, Ammar Askar"
author = u"Ammar Askar"
# The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the
@ -77,37 +78,37 @@ language = None
# There are two options for replacing |today|: either, you set today to some
# non-false value, then it is used:
#today = ''
# today = ''
# Else, today_fmt is used as the format for a strftime call.
#today_fmt = '%B %d, %Y'
# today_fmt = '%B %d, %Y'
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
exclude_patterns = ['_build']
exclude_patterns = ["_build"]
# The reST default role (used for this markup: `text`) to use for all
# documents.
#default_role = None
# default_role = None
# If true, '()' will be appended to :func: etc. cross-reference text.
#add_function_parentheses = True
# add_function_parentheses = True
# If true, the current module name will be prepended to all description
# unit titles (such as .. function::).
#add_module_names = True
# add_module_names = True
# If true, sectionauthor and moduleauthor directives will be shown in the
# output. They are ignored by default.
#show_authors = False
# show_authors = False
# The name of the Pygments (syntax highlighting) style to use.
pygments_style = 'sphinx'
pygments_style = "sphinx"
# A list of ignored prefixes for module index sorting.
#modindex_common_prefix = []
# modindex_common_prefix = []
# If true, keep warnings as "system message" paragraphs in the built documents.
#keep_warnings = False
# keep_warnings = False
# If true, `todo` and `todoList` produce output, else they produce nothing.
todo_include_todos = True
@ -130,94 +131,94 @@ if os.environ.get("READTHEDOCS", "") != "True":
# Theme options are theme-specific and customize the look and feel of a theme
# further. For a list of options available for each theme, see the
# documentation.
#html_theme_options = {}
# html_theme_options = {}
# Add any paths that contain custom themes here, relative to this directory.
#html_theme_path = []
# html_theme_path = []
# The name for this set of Sphinx documents. If None, it defaults to
# "<project> v<release> documentation".
#html_title = None
# html_title = None
# A shorter title for the navigation bar. Default is the same as html_title.
#html_short_title = None
# html_short_title = None
# The name of an image file (relative to this directory) to place at the top
# of the sidebar.
#html_logo = None
# html_logo = None
# The name of an image file (within the static path) to use as favicon of the
# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
# pixels large.
#html_favicon = None
# html_favicon = None
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ['_static']
html_static_path = ["_static"]
# Add any extra paths that contain custom files (such as robots.txt or
# .htaccess) here, relative to this directory. These files are copied
# directly to the root of the documentation.
#html_extra_path = []
# html_extra_path = []
# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
# using the given strftime format.
#html_last_updated_fmt = '%b %d, %Y'
# html_last_updated_fmt = '%b %d, %Y'
# If true, SmartyPants will be used to convert quotes and dashes to
# typographically correct entities.
#html_use_smartypants = True
# html_use_smartypants = True
# Custom sidebar templates, maps document names to template names.
#html_sidebars = {}
# html_sidebars = {}
# Additional templates that should be rendered to pages, maps page names to
# template names.
#html_additional_pages = {}
# html_additional_pages = {}
# If false, no module index is generated.
#html_domain_indices = True
# html_domain_indices = True
# If false, no index is generated.
#html_use_index = True
# html_use_index = True
# If true, the index is split into individual pages for each letter.
#html_split_index = False
# html_split_index = False
# If true, links to the reST sources are added to the pages.
#html_show_sourcelink = True
# html_show_sourcelink = True
# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
#html_show_sphinx = True
# html_show_sphinx = True
# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
#html_show_copyright = True
# html_show_copyright = True
# If true, an OpenSearch description file will be output, and all pages will
# contain a <link> tag referring to it. The value of this option must be the
# base URL from which the finished HTML is served.
#html_use_opensearch = ''
# html_use_opensearch = ''
# This is the file name suffix for HTML files (e.g. ".xhtml").
#html_file_suffix = None
# html_file_suffix = None
# Language to be used for generating the HTML full-text search index.
# Sphinx supports the following languages:
# 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja'
# 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr'
#html_search_language = 'en'
# html_search_language = 'en'
# A dictionary with options for the search language support, empty by default.
# Now only 'ja' uses this config value
#html_search_options = {'type': 'default'}
# html_search_options = {'type': 'default'}
# The name of a javascript file (relative to the configuration directory) that
# implements a search results scorer. If empty, the default will be used.
#html_search_scorer = 'scorer.js'
# html_search_scorer = 'scorer.js'
# Output file base name for HTML help builder.
htmlhelp_basename = 'pyCraftdoc'
htmlhelp_basename = "pyCraftdoc"
# -- Options for LaTeX output ---------------------------------------------
@ -245,36 +246,33 @@ latex_documents = [
# The name of an image file (relative to this directory) to place at the top of
# the title page.
#latex_logo = None
# latex_logo = None
# For "manual" documents, if this is true, then toplevel headings are parts,
# not chapters.
#latex_use_parts = False
# latex_use_parts = False
# If true, show page references after internal links.
#latex_show_pagerefs = False
# latex_show_pagerefs = False
# If true, show URL addresses after external links.
#latex_show_urls = False
# latex_show_urls = False
# Documents to append as an appendix to all manuals.
#latex_appendices = []
# latex_appendices = []
# If false, no module index is generated.
#latex_domain_indices = True
# latex_domain_indices = True
# -- Options for manual page output ---------------------------------------
# One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section).
man_pages = [
(master_doc, 'pycraft', u'pyCraft Documentation',
[author], 1)
]
man_pages = [(master_doc, "pycraft", u"pyCraft Documentation", [author], 1)]
# If true, show URL addresses after external links.
#man_show_urls = False
# man_show_urls = False
# -- Options for Texinfo output -------------------------------------------
@ -289,17 +287,17 @@ texinfo_documents = [
]
# Documents to append as an appendix to all manuals.
#texinfo_appendices = []
# texinfo_appendices = []
# If false, no module index is generated.
#texinfo_domain_indices = True
# texinfo_domain_indices = True
# How to display URL addresses: 'footnote', 'no', or 'inline'.
#texinfo_show_urls = 'footnote'
# texinfo_show_urls = 'footnote'
# If true, do not generate a @detailmenu in the "Top" node's menu.
#texinfo_no_detailmenu = False
# texinfo_no_detailmenu = False
# Example configuration for intersphinx: refer to the Python standard library.
intersphinx_mapping = {'https://docs.python.org/': None}
intersphinx_mapping = {"https://docs.python.org/": None}

34
docs/example.rst Normal file
View File

@ -0,0 +1,34 @@
Example Implementations
=======================
.. currentmodule:: examples.Player
.. _Players: https://github.com/ammaraskar/pyCraft/blob/master/examples/Player.py
.. _Start: https://github.com/ammaraskar/pyCraft/blob/master/examples/start.py
Both of these examples can be used to show how to go about initiating a simple
connection to a server using `pyCraft`.
`Note: These implementations expect to be running in the root directory of this project.
That being one directory higher then they are on the GitHub repo.`
Basic Headless Client
~~~~~~~~~~~~~~~~~~~~~~
Use `python start.py --help` for the available options.
.. automodule:: examples.start
:members:
See the Start_ file for the implementation
Simple Player Class
~~~~~~~~~~~~~~~~~~~~
This implements all the required functionality to connect and maintain a connection
to a given server. This also handles the parsing of chat and then prints it to the screen.
.. automodule:: examples.Player
:members:
See the Players_ file for the implementation

View File

@ -16,6 +16,10 @@ account, edit profiles etc
The Connection class under the networking package handles
connecting to a server, sending packets, listening for packets etc
The example implementation show a couple different approaches to how
you can get started with the library. One from command line and the
other being more programmatically inclined.
Contents:
@ -24,3 +28,4 @@ Contents:
authentication
connecting
example

View File

@ -1,62 +1,18 @@
@ECHO OFF
pushd %~dp0
REM Command file for Sphinx documentation
if "%SPHINXBUILD%" == "" (
set SPHINXBUILD=sphinx-build
)
set SOURCEDIR=.
set BUILDDIR=_build
set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% .
set I18NSPHINXOPTS=%SPHINXOPTS% .
if NOT "%PAPER%" == "" (
set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS%
set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS%
)
if "%1" == "" goto help
if "%1" == "help" (
:help
echo.Please use `make ^<target^>` where ^<target^> is one of
echo. html to make standalone HTML files
echo. dirhtml to make HTML files named index.html in directories
echo. singlehtml to make a single large HTML file
echo. pickle to make pickle files
echo. json to make JSON files
echo. htmlhelp to make HTML files and a HTML help project
echo. qthelp to make HTML files and a qthelp project
echo. devhelp to make HTML files and a Devhelp project
echo. epub to make an epub
echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter
echo. text to make text files
echo. man to make manual pages
echo. texinfo to make Texinfo files
echo. gettext to make PO message catalogs
echo. changes to make an overview over all changed/added/deprecated items
echo. xml to make Docutils-native XML files
echo. pseudoxml to make pseudoxml-XML files for display purposes
echo. linkcheck to check all external links for integrity
echo. doctest to run all doctests embedded in the documentation if enabled
echo. coverage to run coverage check of the documentation if enabled
goto end
)
if "%1" == "clean" (
for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i
del /q /s %BUILDDIR%\*
goto end
)
REM Check if sphinx-build is available and fallback to Python version if any
%SPHINXBUILD% 2> nul
if errorlevel 9009 goto sphinx_python
goto sphinx_ok
:sphinx_python
set SPHINXBUILD=python -m sphinx.__init__
%SPHINXBUILD% 2> nul
%SPHINXBUILD% >NUL 2>NUL
if errorlevel 9009 (
echo.
echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
@ -69,195 +25,11 @@ if errorlevel 9009 (
exit /b 1
)
:sphinx_ok
%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
goto end
if "%1" == "html" (
%SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The HTML pages are in %BUILDDIR%/html.
goto end
)
if "%1" == "dirhtml" (
%SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml.
goto end
)
if "%1" == "singlehtml" (
%SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml.
goto end
)
if "%1" == "pickle" (
%SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle
if errorlevel 1 exit /b 1
echo.
echo.Build finished; now you can process the pickle files.
goto end
)
if "%1" == "json" (
%SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json
if errorlevel 1 exit /b 1
echo.
echo.Build finished; now you can process the JSON files.
goto end
)
if "%1" == "htmlhelp" (
%SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp
if errorlevel 1 exit /b 1
echo.
echo.Build finished; now you can run HTML Help Workshop with the ^
.hhp project file in %BUILDDIR%/htmlhelp.
goto end
)
if "%1" == "qthelp" (
%SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp
if errorlevel 1 exit /b 1
echo.
echo.Build finished; now you can run "qcollectiongenerator" with the ^
.qhcp project file in %BUILDDIR%/qthelp, like this:
echo.^> qcollectiongenerator %BUILDDIR%\qthelp\pyCraft.qhcp
echo.To view the help file:
echo.^> assistant -collectionFile %BUILDDIR%\qthelp\pyCraft.ghc
goto end
)
if "%1" == "devhelp" (
%SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp
if errorlevel 1 exit /b 1
echo.
echo.Build finished.
goto end
)
if "%1" == "epub" (
%SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The epub file is in %BUILDDIR%/epub.
goto end
)
if "%1" == "latex" (
%SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
if errorlevel 1 exit /b 1
echo.
echo.Build finished; the LaTeX files are in %BUILDDIR%/latex.
goto end
)
if "%1" == "latexpdf" (
%SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
cd %BUILDDIR%/latex
make all-pdf
cd %~dp0
echo.
echo.Build finished; the PDF files are in %BUILDDIR%/latex.
goto end
)
if "%1" == "latexpdfja" (
%SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
cd %BUILDDIR%/latex
make all-pdf-ja
cd %~dp0
echo.
echo.Build finished; the PDF files are in %BUILDDIR%/latex.
goto end
)
if "%1" == "text" (
%SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The text files are in %BUILDDIR%/text.
goto end
)
if "%1" == "man" (
%SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The manual pages are in %BUILDDIR%/man.
goto end
)
if "%1" == "texinfo" (
%SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo.
goto end
)
if "%1" == "gettext" (
%SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The message catalogs are in %BUILDDIR%/locale.
goto end
)
if "%1" == "changes" (
%SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes
if errorlevel 1 exit /b 1
echo.
echo.The overview file is in %BUILDDIR%/changes.
goto end
)
if "%1" == "linkcheck" (
%SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck
if errorlevel 1 exit /b 1
echo.
echo.Link check complete; look for any errors in the above output ^
or in %BUILDDIR%/linkcheck/output.txt.
goto end
)
if "%1" == "doctest" (
%SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest
if errorlevel 1 exit /b 1
echo.
echo.Testing of doctests in the sources finished, look at the ^
results in %BUILDDIR%/doctest/output.txt.
goto end
)
if "%1" == "coverage" (
%SPHINXBUILD% -b coverage %ALLSPHINXOPTS% %BUILDDIR%/coverage
if errorlevel 1 exit /b 1
echo.
echo.Testing of coverage in the sources finished, look at the ^
results in %BUILDDIR%/coverage/python.txt.
goto end
)
if "%1" == "xml" (
%SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The XML files are in %BUILDDIR%/xml.
goto end
)
if "%1" == "pseudoxml" (
%SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml.
goto end
)
:help
%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
:end
popd

92
examples/Parsers.py Normal file
View File

@ -0,0 +1,92 @@
import re
import json
"""
A file for Player utilities, focused around parsing chat and making it human readable.
The DefaultParser should be able to handle most situations currently,
however, there are known weakness's in the approach but as it stands,
it is better then other examples I have seen.
DefaultParser - Tested on mc-central, should work decent globally
"""
# TODO Parse banner messages, example:
# https://gyazo.com/c0a4cfee23a31fe8b6e4c7c7848e5e5a
def DefaultParser(data):
"""The default Player chat packet parser, designed to make chat human readable.
Parameters
----------
data : Chat Packet
The chat packet to be parsed.
Returns
-------
message : str
The chat message in human readable form
False : bool
If the parser encounters an error during parsing
"""
try:
# Convert to valid python dict
data = json.loads(data)
# Create the prefix & text
prefixing = True
data = data["extra"]
stringDict = {"prefix": [], "message": []}
dm = False
if isinstance(data[len(data) - 1], str):
# Given the last item is a string, rather then dictionary
# we can safely assume that this is in fact a /msg
dm = True
for i, item in enumerate(data):
# Remove minecraft character stuff
if dm and i == len(data) - 1:
stringDict["message"].append(item)
continue
text = re.sub(
r"\§c|\§f|\§b|\§d|\§a|\§1|\§2|\§3|\§4|\§5|\§6|\§7|\§8|\§9|\§0",
"",
item["text"],
)
if text.lstrip().rstrip() == ":" and prefixing:
# No longer need to handle the before message
prefixing = False
continue
elif prefixing:
stringDict["prefix"].append(text)
elif not prefixing:
if "extra" in item:
# Chat parsing for text means this is most likely another
# nested dict in list situation
if len(item["extra"]) > 0:
if "text" in item["extra"][0]:
text = item["extra"][0]["text"]
stringDict["message"].append(text)
prefix = "".join(stringDict["prefix"])
text = " ".join(stringDict["message"]).rstrip().lstrip()
if len(prefix) > 0 and len(text) > 0:
message = ": ".join([prefix, text])
elif len(prefix) > 0:
message = prefix
elif len(text) > 0:
message = text
message = message.lstrip().rstrip()
return message
except Exception as e:
# print(f"Unable to parse: {data}\nException: {e}")
return False

237
examples/Player.py Normal file
View File

@ -0,0 +1,237 @@
import re
import time
import asyncio
from concurrent.futures.thread import ThreadPoolExecutor
from minecraft import authentication
from minecraft.exceptions import YggdrasilError
from minecraft.networking.connection import Connection
from minecraft.networking.packets import serverbound, clientbound
from .Parsers import DefaultParser
class Player:
"""
A class built to handle all required actions to maintain:
- Gaining auth tokens, and connecting to online minecraft servers.
- Clientbound chat
- Serverbound chat
Warnings
--------
This class explicitly expects a username & password, then expects to
be able to connect to a server in online mode.
If you wish to add different functionality please view the example
headless client, `start.py`, for how to implement it.
"""
def __init__(self, username, password, *, admins=None):
"""
Init handles the following:
- Client Authentication
- Setting the current connection state
- Setting the recognized 'admins' for this instance
Parameters
----------
username : String
Used for authentication
password : String
Used for authentication
admins : list, optional
The minecraft accounts to auto accept tpa's requests from
Raises
------
YggdrasilError
Username or Password was incorrect
"""
self.kickout = False
self.admins = [] if admins is None else admins
self.auth_token = authentication.AuthenticationToken()
self.auth_token.authenticate(username, password)
def Parser(self, data):
"""
Converts the chat packet received from the server
into human readable strings
Parameters
----------
data : JSON
The chat data json receive from the server
Returns
-------
message : String
The text received from the server in human readable form
"""
message = DefaultParser(
data) # This is where you would call other parsers
if not message:
return False
if "teleport" in message.lower():
self.HandleTpa(message)
return message
def HandleTpa(self, message):
"""
Using the given message, figure out whether or not to accept the tpa
Parameters
----------
message : String
The current chat, where 'tpa' was found in message.lower()
"""
try:
found = re.search(
"(.+?) has requested that you teleport to them.", message
).group(1)
if found in self.admins:
self.SendChat("/tpyes")
return
except AttributeError:
pass
try:
found = re.search(
"(.+?) has requested to teleport to you.",
message).group(1)
if found in self.admins:
self.SendChat("/tpyes")
return
except AttributeError:
pass
def SendChat(self, msg):
"""
Send a given message to the server
Parameters
----------
msg : String
The message to send to the server
"""
msg = str(msg)
if len(msg) > 0:
packet = serverbound.play.ChatPacket()
packet.message = msg
self.connection.write_packet(packet)
def ReceiveChat(self, chat_packet):
"""
The listener for ClientboundChatPackets
Parameters
----------
chat_packet : ClientboundChatPacket
The incoming chat packet
chat_packet.json : JSON
The chat packet to pass of to our Parser for handling
"""
message = self.Parser(chat_packet.json_data)
if not message:
# This means our Parser failed lol
print("Parser failed")
return
print(message)
def SetServer(self, ip, port=25565, handler=None):
"""
Sets the server, ready for connection
Parameters
----------
ip : str
The server to connect to
port : int, optional
The port to connect on
handler : Function pointer, optional
Points to the function used to handle Clientbound chat packets
"""
handler = handler or self.ReceiveChat
self.ip = ip
self.port = port
self.connection = Connection(
ip, port, auth_token=self.auth_token, handle_exception=print
)
self.connection.register_packet_listener(
handler, clientbound.play.ChatMessagePacket
)
self.connection.exception_handler(print)
def Connect(self):
"""
Actually connect to the server for this player and maintain said connection
Notes
-----
This is a blocking function and will not return until `Disconnect()` is called on said instance.
"""
self.connection.connect()
print(f"Connected to server with: {self.auth_token.username}")
while True:
time.sleep(1)
if self.kickout:
break
def Disconnect(self):
"""
In order to disconnect the client, and break the blocking loop
this method must be called
"""
self.kickout = True
self.connection.disconnect()
async def Main():
try:
player = Player("Account Email/Username", "Account Password")
except YggdrasilError as e:
# Authentication Error
print("Incorrect Login", e)
return
player.SetServer("Server to connect to.")
# We do this to ensure it is non blocking as Connect() is a
# forever loop used to maintain a connection to a server
executor = ThreadPoolExecutor()
executor.submit(player.Connect)
# Forever do things unless the user wants us to logout
while True:
message = input("What should I do/say?\n")
# Disconnect the client from the server before finishing everything up
if message.lower() in ["logout", "disconnected", "exit"]:
player.Disconnect()
print("Disconnected")
return
# Send the message to the server via the player
player.SendChat(message)
# Simply run our program
if __name__ == "__main__":
asyncio.run(Main())

119
start.py → examples/start.py Executable file → Normal file
View File

@ -12,29 +12,66 @@ from minecraft.networking.packets import Packet, clientbound, serverbound
def get_options():
"""
Using Pythons OptionParser, get the sys args and the corresponding
input parsed as required until there is enough input to proceed.
Returns
-------
options
The options to run this instance with
"""
parser = OptionParser()
parser.add_option("-u", "--username", dest="username", default=None,
help="username to log in with")
parser.add_option(
"-u",
"--username",
dest="username",
default=None,
help="username to log in with",
)
parser.add_option("-p", "--password", dest="password", default=None,
help="password to log in with")
parser.add_option(
"-p",
"--password",
dest="password",
default=None,
help="password to log in with",
)
parser.add_option("-s", "--server", dest="server", default=None,
help="server host or host:port "
"(enclose IPv6 addresses in square brackets)")
parser.add_option(
"-s",
"--server",
dest="server",
default=None,
help="server host or host:port "
"(enclose IPv6 addresses in square brackets)",
)
parser.add_option("-o", "--offline", dest="offline", action="store_true",
help="connect to a server in offline mode "
"(no password required)")
parser.add_option(
"-o",
"--offline",
dest="offline",
action="store_true",
help="connect to a server in offline mode " "(no password required)",
)
parser.add_option("-d", "--dump-packets", dest="dump_packets",
action="store_true",
help="print sent and received packets to standard error")
parser.add_option(
"-d",
"--dump-packets",
dest="dump_packets",
action="store_true",
help="print sent and received packets to standard error",
)
parser.add_option("-v", "--dump-unknown-packets", dest="dump_unknown",
action="store_true",
help="include unknown packets in --dump-packets output")
parser.add_option(
"-v",
"--dump-unknown-packets",
dest="dump_unknown",
action="store_true",
help="include unknown packets in --dump-packets output",
)
parser.add_option(
"-m",
@ -55,11 +92,15 @@ def get_options():
options.offline = options.offline or (options.password == "")
if not options.server:
options.server = input("Enter server host or host:port "
"(enclose IPv6 addresses in square brackets): ")
options.server = input(
"Enter server host or host:port "
"(enclose IPv6 addresses in square brackets): "
)
# Try to split out port and address
match = re.match(r"((?P<host>[^\[\]:]+)|\[(?P<addr>[^\[\]]+)\])"
r"(:(?P<port>\d+))?$", options.server)
match = re.match(
r"((?P<host>[^\[\]:]+)|\[(?P<addr>[^\[\]]+)\])" r"(:(?P<port>\d+))?$",
options.server,
)
if match is None:
raise ValueError("Invalid server address: '%s'." % options.server)
options.address = match.group("host") or match.group("addr")
@ -69,12 +110,27 @@ def get_options():
def main():
"""Our main function for running the simple pyCraft implementation.
This function handles and maintains:
- Gaining authentication tokens & 'logging in'
- Connecting to the provided server, online or offline
- Prints the chat packet data to standard out on Clientbound Packet
- Writes Serverbound chat Packets when required
- Dumping all packets to standard out
Notes
-----
This is a blocking function.
"""
options = get_options()
if options.offline:
print("Connecting in offline mode...")
connection = Connection(
options.address, options.port, username=options.username)
options.address, options.port, username=options.username
)
else:
try:
@ -92,33 +148,36 @@ def main():
"1.8")
if options.dump_packets:
def print_incoming(packet):
if type(packet) is Packet:
# This is a direct instance of the base Packet type, meaning
# that it is a packet of unknown type, so we do not print it
# unless explicitly requested by the user.
if options.dump_unknown:
print('--> [unknown packet] %s' % packet, file=sys.stderr)
print("--> [unknown packet] %s" % packet, file=sys.stderr)
else:
print('--> %s' % packet, file=sys.stderr)
print("--> %s" % packet, file=sys.stderr)
def print_outgoing(packet):
print('<-- %s' % packet, file=sys.stderr)
print("<-- %s" % packet, file=sys.stderr)
connection.register_packet_listener(
print_incoming, Packet, early=True)
connection.register_packet_listener(print_incoming, Packet, early=True)
connection.register_packet_listener(
print_outgoing, Packet, outgoing=True)
def handle_join_game(join_game_packet):
print('Connected.')
print("Connected.")
connection.register_packet_listener(
handle_join_game, clientbound.play.JoinGamePacket)
handle_join_game, clientbound.play.JoinGamePacket
)
def print_chat(chat_packet):
print("Message (%s): %s" % (
chat_packet.field_string('position'), chat_packet.json_data))
print(
"Message (%s): %s"
% (chat_packet.field_string("position"), chat_packet.json_data)
)
connection.register_packet_listener(
print_chat, clientbound.play.ChatMessagePacket)

View File

@ -32,7 +32,7 @@ class ConnectionContext(object):
"""
def __init__(self, **kwds):
self.protocol_version = kwds.get('protocol_version')
self.protocol_version = kwds.get("protocol_version")
def protocol_earlier(self, other_pv):
"""Returns True if the protocol version of this context was published
@ -63,8 +63,13 @@ class ConnectionContext(object):
class _ConnectionOptions(object):
def __init__(self, address=None, port=None, compression_threshold=-1,
compression_enabled=False):
def __init__(
self,
address=None,
port=None,
compression_threshold=-1,
compression_enabled=False,
):
self.address = address
self.port = port
self.compression_threshold = compression_threshold
@ -94,41 +99,51 @@ class Connection(object):
The connect method needs to be called in order to actually begin
the connection
:param address: address of the server to connect to
:param port(int): port of the server to connect to
:param auth_token: :class:`minecraft.authentication.AuthenticationToken`
object. If None, no authentication is attempted and
the server is assumed to be running in offline mode.
:param username: Username string; only applicable in offline mode.
:param initial_version: A Minecraft version ID string or protocol
version number to use if the server's protocol
version cannot be determined. (Although it is
now somewhat inaccurate, this name is retained
for backward compatibility.)
:param allowed_versions: A set of versions, each being a Minecraft
version ID string or protocol version number,
restricting the versions that the client may
use in connecting to the server.
:param handle_exception: The final exception handler. This is triggered
when an exception occurs in the networking
thread that is not caught normally. After
any other user-registered exception handlers
are run, the final exception (which may be the
original exception or one raised by another
handler) is passed, regardless of whether or
not it was caught by another handler, to the
final handler, which may be a function obeying
the protocol of 'register_exception_handler';
the value 'None', meaning that if the
exception was otherwise uncaught, it is
re-raised from the networking thread after
closing the connection; or the value 'False',
meaning that the exception is never re-raised.
:param handle_exit: A function to be called when a connection to a
server terminates, not caused by an exception,
and not with the intention to automatically
reconnect. Exceptions raised from this function
will be handled by any matching exception handlers.
Parameters
----------
address
address of the server to connect to
port : int
port of the server to connect to
auth_token : `minecraft.authentication.AuthenticationToken`
If None, no authentication is attempted and
the server is assumed to be running in offline mode.
username : str
Username string; only applicable in offline mode.
initial_version
A Minecraft version ID string or protocol
version number to use if the server's protocol
version cannot be determined. (Although it is
now somewhat inaccurate, this name is retained
for backward compatibility.)
allowed_versions
A set of versions, each being a Minecraft
version ID string or protocol version number,
restricting the versions that the client may
use in connecting to the server.
handle_exception
The final exception handler. This is triggered
when an exception occurs in the networking
thread that is not caught normally. After
any other user-registered exception handlers
are run, the final exception (which may be the
original exception or one raised by another
handler) is passed, regardless of whether or
not it was caught by another handler, to the
final handler, which may be a function obeying
the protocol of 'register_exception_handler';
the value 'None', meaning that if the
exception was otherwise uncaught, it is
re-raised from the networking thread after
closing the connection; or the value 'False',
meaning that the exception is never re-raised.
handle_exit
A function to be called when a connection to a
server terminates, not caused by an exception,
and not with the intention to automatically
reconnect. Exceptions raised from this function
will be handled by any matching exception handlers.
""" # NOQA
# This lock is re-entrant because it may be acquired in a re-entrant
@ -151,7 +166,7 @@ class Connection(object):
else:
proto_version = None
if proto_version not in SUPPORTED_PROTOCOL_VERSIONS:
raise ValueError('Unsupported version number: %r.' % version)
raise ValueError("Unsupported version number: %r." % version)
return proto_version
if allowed_versions is None:
@ -187,10 +202,12 @@ class Connection(object):
def _start_network_thread(self):
with self._write_lock:
if self.networking_thread is not None and \
not self.networking_thread.interrupt or \
self.new_networking_thread is not None:
raise InvalidState('A networking thread is already running.')
if (
self.networking_thread is not None
and not self.networking_thread.interrupt
or self.new_networking_thread is not None
):
raise InvalidState("A networking thread is already running.")
elif self.networking_thread is None:
self.networking_thread = NetworkingThread(self)
self.networking_thread.start()
@ -198,8 +215,9 @@ class Connection(object):
# This thread will wait until the existing thread exits, and
# then set 'networking_thread' to itself and
# 'new_networking_thread' to None.
self.new_networking_thread \
= NetworkingThread(self, previous=self.networking_thread)
self.new_networking_thread = NetworkingThread(
self, previous=self.networking_thread
)
self.new_networking_thread.start()
def write_packet(self, packet, force=False):
@ -211,8 +229,13 @@ class Connection(object):
If force is false then the packet will be added to the end of the
packet writing queue to be sent 'as soon as possible'
:param packet: The :class:`network.packets.Packet` to write
:param force(bool): Specifies if the packet write should be immediate
Parameters
----------
packet : network.packets.Packet
The `network.packets.Packet` to write
force : bool
Specifies if the packet write should be immediate
"""
packet.context = self.context
if force:
@ -222,13 +245,19 @@ class Connection(object):
self._outgoing_packet_queue.append(packet)
def listener(self, *packet_types, **kwds):
"""
Shorthand decorator to register a function as a packet listener.
"""Shorthand decorator to register a function as a packet listener.
Wraps :meth:`minecraft.networking.connection.register_packet_listener`
:param packet_types: Packet types to listen for.
:param kwds: Keyword arguments for `register_packet_listener`
Parameters
----------
packet_types
Packet types to listen for.
kwds
Keyword arguments for `register_packet_listener`
"""
def listener_decorator(handler_func):
self.register_packet_listener(handler_func, *packet_types, **kwds)
return handler_func
@ -239,6 +268,7 @@ class Connection(object):
"""
Shorthand decorator to register a function as an exception handler.
"""
def exception_handler_decorator(handler_func):
self.register_exception_handler(handler_func, *exc_types, **kwds)
return handler_func
@ -246,8 +276,7 @@ class Connection(object):
return exception_handler_decorator
def register_packet_listener(self, method, *packet_types, **kwds):
"""
Registers a listener method which will be notified when a packet of
"""Registers a listener method which will be notified when a packet of
a selected type is received.
If :class:`minecraft.networking.connection.IgnorePacket` is raised from
@ -258,23 +287,38 @@ class Connection(object):
'outgoing=True', this will prevent the packet from being written to the
network.
:param method: The method which will be called back with the packet
:param packet_types: The packets to listen for
:param outgoing: If 'True', this listener will be called on outgoing
packets just after they are sent to the server, rather
than on incoming packets.
:param early: If 'True', this listener will be called before any
built-in default action is carried out, and before any
listeners with 'early=False' are called. If
'outgoing=True', the listener will be called before the
packet is written to the network, rather than afterwards.
Parameters
----------
method
The method which will be called back with the packet
packet_types
The packets to listen for
outgoing
If 'True', this listener will be called on outgoing
packets just after they are sent to the server, rather
than on incoming packets.
early
If 'True', this listener will be called before any
built-in default action is carried out, and before any
listeners with 'early=False' are called. If
'outgoing=True', the listener will be called before the
packet is written to the network, rather than afterwards.
Returns
-------
"""
outgoing = kwds.pop('outgoing', False)
early = kwds.pop('early', False)
target = self.packet_listeners if not early and not outgoing \
else self.early_packet_listeners if early and not outgoing \
else self.outgoing_packet_listeners if not early \
outgoing = kwds.pop("outgoing", False)
early = kwds.pop("early", False)
target = (
self.packet_listeners
if not early and not outgoing
else self.early_packet_listeners
if early and not outgoing
else self.outgoing_packet_listeners
if not early
else self.early_outgoing_packet_listeners
)
target.append(packets.PacketListener(method, *packet_types, **kwds))
def register_exception_handler(self, handler_func, *exc_types, **kwds):
@ -295,21 +339,24 @@ class Connection(object):
be set as the 'exception' and 'exc_info' attributes of the
'Connection'.
:param handler_func: A function taking two arguments: the exception
object 'e' as in 'except Exception as e:', and the corresponding
3-tuple given by 'sys.exc_info()'. The return value of the function is
ignored, but any exception raised in it replaces the original
exception, and may be passed to later exception handlers.
:param exc_types: The types of exceptions that this handler shall
catch, as in 'except (exc_type_1, exc_type_2, ...) as e:'. If this is
empty, the handler will catch all exceptions.
:param early: If 'True', the exception handler is registered before
any existing exception handlers in the handling order.
Parameters
----------
handler_func
A function taking two arguments: the exception
object 'e' as in 'except Exception as e:', and the corresponding
3-tuple given by 'sys.exc_info()'. The return value of the function is
ignored, but any exception raised in it replaces the original
exception, and may be passed to later exception handlers.
exc_types
The types of exceptions that this handler shall
catch, as in 'except (exc_type_1, exc_type_2, ...) as e:'. If this is
empty, the handler will catch all exceptions.
early
If 'True', the exception handler is registered before
any existing exception handlers in the handling order.
"""
early = kwds.pop('early', False)
assert not kwds, 'Unexpected keyword arguments: %r' % (kwds,)
early = kwds.pop("early", False)
assert not kwds, "Unexpected keyword arguments: %r" % (kwds,)
if early:
self._exception_handlers.insert(0, (handler_func, exc_types))
else:
@ -350,14 +397,19 @@ class Connection(object):
def status(self, handle_status=None, handle_ping=False):
"""Issue a status request to the server and then disconnect.
:param handle_status: a function to be called with the status
dictionary None for the default behaviour of
printing the dictionary to standard output, or
False to ignore the result.
:param handle_ping: a function to be called with the measured latency
in milliseconds, None for the default handler,
which prints the latency to standard outout, or
False, to prevent measurement of the latency.
Parameters
----------
handle_status
A function to be called with the status
dictionary None for the default behaviour of
printing the dictionary to standard output, or
False to ignore the result.
handle_ping
A function to be called with the measured latency
in milliseconds, None for the default handler,
which prints the latency to standard outout, or
False, to prevent measurement of the latency.
"""
with self._write_lock: # pylint: disable=not-context-manager
self._check_connection()
@ -422,10 +474,12 @@ class Connection(object):
self._start_network_thread()
def _check_connection(self):
if self.networking_thread is not None and \
not self.networking_thread.interrupt or \
self.new_networking_thread is not None:
raise InvalidState('There is an existing connection.')
if (
self.networking_thread is not None
and not self.networking_thread.interrupt
or self.new_networking_thread is not None
):
raise InvalidState("There is an existing connection.")
def _connect(self):
# Connect a socket to the server and create a file object from the
@ -436,8 +490,9 @@ class Connection(object):
# the server.
self._outgoing_packet_queue = deque()
info = socket.getaddrinfo(self.options.address, self.options.port,
0, socket.SOCK_STREAM)
info = socket.getaddrinfo(
self.options.address, self.options.port, 0, socket.SOCK_STREAM
)
# Prefer to use IPv4 (for backward compatibility with previous
# versions that always resolved hostnames to IPv4 addresses),
@ -456,7 +511,14 @@ class Connection(object):
def disconnect(self, immediate=False):
"""Terminate the existing server connection, if there is one.
If 'immediate' is True, do not attempt to write any packets.
If 'immediate' is True, do not attempt to write any packets.
Parameters
----------
immediate : bool, optional
Whether or not to terminate the existing connection immediately
"""
with self._write_lock: # pylint: disable=not-context-manager
self.connected = False
@ -545,14 +607,20 @@ class Connection(object):
server_protocol = KNOWN_MINECRAFT_VERSIONS.get(server_version)
if server_protocol is None:
vs = 'version' if server_version is None else \
('version of %s' % server_version)
vs = (
"version"
if server_version is None
else ("version of %s" % server_version)
)
else:
vs = ('protocol version of %d' % server_protocol) + \
('' if server_version is None else ' (%s)' % server_version)
ss = 'supported, but not allowed for this connection' \
if server_protocol in SUPPORTED_PROTOCOL_VERSIONS \
else 'not supported'
vs = ("protocol version of %d" % server_protocol) + (
"" if server_version is None else " (%s)" % server_version
)
ss = (
"supported, but not allowed for this connection"
if server_protocol in SUPPORTED_PROTOCOL_VERSIONS
else "not supported"
)
err = VersionMismatch("Server's %s is %s." % (vs, ss))
err.server_protocol = server_protocol
err.server_version = server_version
@ -625,7 +693,8 @@ class NetworkingThread(threading.Thread):
# Read and react to as many as 50 packets.
while num_packets < 50 and not self.interrupt:
packet = self.connection.reactor.read_packet(
self.connection.file_object, timeout=read_timeout)
self.connection.file_object, timeout=read_timeout
)
if not packet:
break
num_packets += 1
@ -647,6 +716,7 @@ class PacketReactor(object):
"""
Reads and reacts to packets
"""
state_name = None
# Handshaking is considered the "default" state
@ -657,7 +727,8 @@ class PacketReactor(object):
context = self.connection.context
self.clientbound_packets = {
packet.get_id(context): packet
for packet in self.__class__.get_clientbound_packets(context)}
for packet in self.__class__.get_clientbound_packets(context)
}
def read_packet(self, stream, timeout=0):
# Block for up to `timeout' seconds waiting for `stream' to become
@ -671,8 +742,8 @@ class PacketReactor(object):
packet_data.send(stream.read(length))
# Ensure we read all the packet
while len(packet_data.get_writable()) < length:
packet_data.send(
stream.read(length - len(packet_data.get_writable())))
packet_data.send(stream.read(
length - len(packet_data.get_writable())))
packet_data.reset_cursor()
if self.connection.options.compression_enabled:
@ -728,12 +799,14 @@ class LoginReactor(PacketReactor):
secret = encryption.generate_shared_secret()
token, encrypted_secret = encryption.encrypt_token_and_secret(
packet.public_key, packet.verify_token, secret)
packet.public_key, packet.verify_token, secret
)
# A server id of '-' means the server is in offline mode
if packet.server_id != '-':
if packet.server_id != "-":
server_id = encryption.generate_verification_hash(
packet.server_id, secret, packet.public_key)
packet.server_id, secret, packet.public_key
)
if self.connection.auth_token is not None:
self.connection.auth_token.join(server_id)
@ -750,25 +823,27 @@ class LoginReactor(PacketReactor):
encryptor = cipher.encryptor()
decryptor = cipher.decryptor()
self.connection.socket = encryption.EncryptedSocketWrapper(
self.connection.socket, encryptor, decryptor)
self.connection.file_object = \
encryption.EncryptedFileObjectWrapper(
self.connection.file_object, decryptor)
self.connection.socket, encryptor, decryptor
)
self.connection.file_object = encryption.EncryptedFileObjectWrapper(
self.connection.file_object, decryptor)
elif packet.packet_name == "disconnect":
# Receiving a disconnect packet in the login state indicates an
# abnormal condition. Raise an exception explaining the situation.
try:
msg = json.loads(packet.json_data)['text']
msg = json.loads(packet.json_data)["text"]
except (ValueError, TypeError, KeyError):
msg = packet.json_data
match = re.match(r"Outdated (client! Please use|server!"
r" I'm still on) (?P<ver>\S+)$", msg)
match = re.match(
r"Outdated (client! Please use|server!"
r" I'm still on) (?P<ver>\S+)$", msg, )
if match:
ver = match.group('ver')
ver = match.group("ver")
self.connection._version_mismatch(server_version=ver)
raise LoginDisconnect('The server rejected our login attempt '
'with: "%s".' % msg)
raise LoginDisconnect(
"The server rejected our login attempt " 'with: "%s".' % msg
)
elif packet.packet_name == "login success":
self.connection.reactor = PlayingReactor(self.connection)
@ -780,7 +855,9 @@ class LoginReactor(PacketReactor):
elif packet.packet_name == "login plugin request":
self.connection.write_packet(
serverbound.login.PluginResponsePacket(
message_id=packet.message_id, successful=False))
message_id=packet.message_id, successful=False
)
)
class PlayingReactor(PacketReactor):
@ -846,7 +923,7 @@ class StatusReactor(PacketReactor):
print(status_dict)
def handle_ping(self, latency_ms):
print('Ping: %d ms' % latency_ms)
print("Ping: %d ms" % latency_ms)
class PlayingStatusReactor(StatusReactor):
@ -858,15 +935,15 @@ class PlayingStatusReactor(StatusReactor):
# This can occur when we connect to a Mojang server while it is
# still initialising, so it must not cause the client to connect
# with the default version.
raise IOError('Invalid server status.')
elif 'version' not in status or 'protocol' not in status['version']:
raise IOError("Invalid server status.")
elif "version" not in status or "protocol" not in status["version"]:
return self.handle_failure()
proto = status['version']['protocol']
proto = status["version"]["protocol"]
if proto not in self.connection.allowed_proto_versions:
self.connection._version_mismatch(
server_protocol=proto,
server_version=status['version'].get('name'))
server_version=status["version"].get("name"))
self.handle_proto_version(proto)