diff --git a/README.rst b/README.rst
index 2634007e..8e901202 100644
--- a/README.rst
+++ b/README.rst
@@ -16,7 +16,7 @@ communication with a MineCraft server.
Detailed information for developers can be found here:
``_.
-``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
diff --git a/docs/Makefile b/docs/Makefile
index cf094359..d4bb2cbb 100644
--- a/docs/Makefile
+++ b/docs/Makefile
@@ -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 ' where 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)"
-
-clean:
- rm -rf $(BUILDDIR)/*
-
-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."
+ @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
-xml:
- $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml
- @echo
- @echo "Build finished. The XML files are in $(BUILDDIR)/xml."
+.PHONY: help Makefile
-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)
diff --git a/docs/conf.py b/docs/conf.py
index d7d2ad25..bd13a1ae 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -19,50 +19,52 @@
# 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
# built documents.
#
import minecraft
+
# The short X.Y version.
version = minecraft.__version__
# The full version, including alpha/beta/rc tags.
@@ -77,37 +79,37 @@
# 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
@@ -120,161 +122,155 @@
# Attempt to use RTD theme even when compiling locally.
if os.environ.get("READTHEDOCS", "") != "True":
- try:
- import sphinx_rtd_theme
- html_theme = "sphinx_rtd_theme"
- html_theme_path = [sphinx_rtd_theme.get_html_theme_path()]
- except ImportError:
- html_theme = "classic"
+ try:
+ import sphinx_rtd_theme
+
+ html_theme = "sphinx_rtd_theme"
+ html_theme_path = [sphinx_rtd_theme.get_html_theme_path()]
+ except ImportError:
+ html_theme = "classic"
# 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
# " v 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 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 ---------------------------------------------
latex_elements = {
-# The paper size ('letterpaper' or 'a4paper').
-#'papersize': 'letterpaper',
-
-# The font size ('10pt', '11pt' or '12pt').
-#'pointsize': '10pt',
-
-# Additional stuff for the LaTeX preamble.
-#'preamble': '',
-
-# Latex figure (float) alignment
-#'figure_align': 'htbp',
+ # The paper size ('letterpaper' or 'a4paper').
+ #'papersize': 'letterpaper',
+ # The font size ('10pt', '11pt' or '12pt').
+ #'pointsize': '10pt',
+ # Additional stuff for the LaTeX preamble.
+ #'preamble': '',
+ # Latex figure (float) alignment
+ #'figure_align': 'htbp',
}
# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title,
# author, documentclass [howto, manual, or own class]).
latex_documents = [
- (master_doc, 'pyCraft.tex', u'pyCraft Documentation',
- u'Ammar Askar', 'manual'),
+ (master_doc, "pyCraft.tex", u"pyCraft Documentation", u"Ammar Askar", "manual"),
]
# 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 -------------------------------------------
@@ -283,23 +279,29 @@
# (source start file, target name, title, author,
# dir menu entry, description, category)
texinfo_documents = [
- (master_doc, 'pyCraft', u'pyCraft Documentation',
- author, 'pyCraft', 'One line description of project.',
- 'Miscellaneous'),
+ (
+ master_doc,
+ "pyCraft",
+ u"pyCraft Documentation",
+ author,
+ "pyCraft",
+ "One line description of project.",
+ "Miscellaneous",
+ ),
]
# 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}
diff --git a/docs/example.rst b/docs/example.rst
new file mode 100644
index 00000000..d221a99e
--- /dev/null
+++ b/docs/example.rst
@@ -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
\ No newline at end of file
diff --git a/docs/index.rst b/docs/index.rst
index 38cee5b8..5e465e2b 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -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
diff --git a/docs/make.bat b/docs/make.bat
index 980cec33..922152e9 100644
--- a/docs/make.bat
+++ b/docs/make.bat
@@ -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 ^` where ^ 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
-
-
-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
-)
+%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
+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
diff --git a/examples/Parsers.py b/examples/Parsers.py
new file mode 100644
index 00000000..f9070293
--- /dev/null
+++ b/examples/Parsers.py
@@ -0,0 +1,90 @@
+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
diff --git a/examples/Player.py b/examples/Player.py
new file mode 100644
index 00000000..b070c723
--- /dev/null
+++ b/examples/Player.py
@@ -0,0 +1,236 @@
+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())
diff --git a/examples/start.py b/examples/start.py
new file mode 100644
index 00000000..7554d59c
--- /dev/null
+++ b/examples/start.py
@@ -0,0 +1,189 @@
+#!/usr/bin/env python
+
+import getpass
+import sys
+import re
+from optparse import OptionParser
+
+from minecraft import authentication
+from minecraft.exceptions import YggdrasilError
+from minecraft.networking.connection import Connection
+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(
+ "-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(
+ "-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(
+ "-v",
+ "--dump-unknown-packets",
+ dest="dump_unknown",
+ action="store_true",
+ help="include unknown packets in --dump-packets output",
+ )
+
+ (options, args) = parser.parse_args()
+
+ if not options.username:
+ options.username = input("Enter your username: ")
+
+ if not options.password and not options.offline:
+ options.password = getpass.getpass(
+ "Enter your password (leave " "blank for offline mode): "
+ )
+ 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): "
+ )
+ # Try to split out port and address
+ match = re.match(
+ r"((?P[^\[\]:]+)|\[(?P[^\[\]]+)\])" r"(:(?P\d+))?$",
+ options.server,
+ )
+ if match is None:
+ raise ValueError("Invalid server address: '%s'." % options.server)
+ options.address = match.group("host") or match.group("addr")
+ options.port = int(match.group("port") or 25565)
+
+ return 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
+ )
+ else:
+ auth_token = authentication.AuthenticationToken()
+ try:
+ auth_token.authenticate(options.username, options.password)
+ except YggdrasilError as e:
+ print(e)
+ sys.exit()
+ print("Logged in as %s..." % auth_token.username)
+ connection = Connection(options.address, options.port, auth_token=auth_token)
+
+ 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)
+ else:
+ print("--> %s" % packet, file=sys.stderr)
+
+ def print_outgoing(packet):
+ print("<-- %s" % packet, file=sys.stderr)
+
+ 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.")
+
+ connection.register_packet_listener(
+ handle_join_game, clientbound.play.JoinGamePacket
+ )
+
+ def print_chat(chat_packet):
+ print(
+ "Message (%s): %s"
+ % (chat_packet.field_string("position"), chat_packet.json_data)
+ )
+
+ connection.register_packet_listener(print_chat, clientbound.play.ChatMessagePacket)
+
+ connection.connect()
+
+ while True:
+ try:
+ text = input()
+ if text == "/respawn":
+ print("respawning...")
+ packet = serverbound.play.ClientStatusPacket()
+ packet.action_id = serverbound.play.ClientStatusPacket.RESPAWN
+ connection.write_packet(packet)
+ else:
+ packet = serverbound.play.ChatPacket()
+ packet.message = text
+ connection.write_packet(packet)
+ except KeyboardInterrupt:
+ print("Bye!")
+ sys.exit()
+
+
+if __name__ == "__main__":
+ main()
diff --git a/minecraft/networking/connection.py b/minecraft/networking/connection.py
index 57132955..98b712ca 100644
--- a/minecraft/networking/connection.py
+++ b/minecraft/networking/connection.py
@@ -14,9 +14,7 @@
from . import packets
from . import encryption
from .. import SUPPORTED_PROTOCOL_VERSIONS, SUPPORTED_MINECRAFT_VERSIONS
-from ..exceptions import (
- VersionMismatch, LoginDisconnect, IgnorePacket, InvalidState
-)
+from ..exceptions import VersionMismatch, LoginDisconnect, IgnorePacket, InvalidState
STATE_STATUS = 1
@@ -28,13 +26,19 @@ class ConnectionContext(object):
shared by the Connection class with other classes, such as Packet.
Importantly, it can be used without knowing the interface of Connection.
"""
+
def __init__(self, **kwds):
- self.protocol_version = kwds.get('protocol_version')
+ self.protocol_version = kwds.get("protocol_version")
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
@@ -46,6 +50,7 @@ class Connection(object):
server, it handles everything from connecting, sending packets to
handling default network behaviour
"""
+
def __init__(
self,
address,
@@ -63,41 +68,51 @@ def __init__(
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
@@ -120,7 +135,7 @@ def proto_version(version):
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:
@@ -135,7 +150,8 @@ def proto_version(version):
self.default_proto_version = proto_version(initial_version)
self.context = ConnectionContext(
- protocol_version=max(self.allowed_proto_versions))
+ protocol_version=max(self.allowed_proto_versions)
+ )
self.options = _ConnectionOptions()
self.options.address = address
@@ -154,10 +170,12 @@ def proto_version(version):
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()
@@ -165,8 +183,9 @@ def _start_network_thread(self):
# 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):
@@ -178,8 +197,13 @@ def write_packet(self, packet, force=False):
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:
@@ -189,13 +213,19 @@ def write_packet(self, packet, force=False):
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
@@ -206,6 +236,7 @@ def exception_handler(self, *exc_types, **kwds):
"""
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
@@ -213,8 +244,7 @@ def exception_handler_decorator(handler_func):
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
@@ -225,23 +255,38 @@ def register_packet_listener(self, method, *packet_types, **kwds):
'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):
@@ -262,21 +307,24 @@ def register_exception_handler(self, handler_func, *exc_types, **kwds):
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:
@@ -317,14 +365,19 @@ def _write_packet(self, packet):
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()
@@ -387,10 +440,12 @@ def connect(self):
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
@@ -401,15 +456,18 @@ def _connect(self):
# 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),
# then IPv6, then other address families.
def key(ai):
- return 0 if ai[0] == socket.AF_INET else \
- 1 if ai[0] == socket.AF_INET6 else 2
+ return (
+ 0 if ai[0] == socket.AF_INET else 1 if ai[0] == socket.AF_INET6 else 2
+ )
+
ai_faml, ai_type, ai_prot, _ai_cnam, ai_addr = min(info, key=key)
self.socket = socket.socket(ai_faml, ai_type, ai_prot)
@@ -421,7 +479,14 @@ def key(ai):
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
@@ -499,14 +564,20 @@ def _version_mismatch(self, server_protocol=None, server_version=None):
server_protocol = SUPPORTED_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
@@ -579,7 +650,8 @@ def _run(self):
# 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
@@ -601,6 +673,7 @@ class PacketReactor(object):
"""
Reads and reacts to packets
"""
+
state_name = None
# Handshaking is considered the "default" state
@@ -611,7 +684,8 @@ def __init__(self, connection):
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
@@ -625,19 +699,18 @@ def read_packet(self, stream, timeout=0):
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:
decompressed_size = VarInt.read(packet_data)
if decompressed_size > 0:
decompressor = zlib.decompressobj()
- decompressed_packet = decompressor.decompress(
- packet_data.read())
- assert len(decompressed_packet) == decompressed_size, \
- 'decompressed length %d, but expected %d' % \
- (len(decompressed_packet), decompressed_size)
+ decompressed_packet = decompressor.decompress(packet_data.read())
+ assert len(decompressed_packet) == decompressed_size, (
+ "decompressed length %d, but expected %d"
+ % (len(decompressed_packet), decompressed_size)
+ )
packet_data.reset()
packet_data.send(decompressed_packet)
packet_data.reset_cursor()
@@ -682,12 +755,14 @@ def react(self, packet):
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)
@@ -704,25 +779,29 @@ def react(self, packet):
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\S+)$", msg)
+ match = re.match(
+ r"Outdated (client! Please use|server!" r" I'm still on) (?P\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)
@@ -734,7 +813,9 @@ def react(self, packet):
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):
@@ -800,7 +881,7 @@ def handle_status(self, status_dict):
print(status_dict)
def handle_ping(self, latency_ms):
- print('Ping: %d ms' % latency_ms)
+ print("Ping: %d ms" % latency_ms)
class PlayingStatusReactor(StatusReactor):
@@ -812,15 +893,15 @@ def handle_status(self, status):
# 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_protocol=proto, server_version=status["version"].get("name")
+ )
self.handle_proto_version(proto)
diff --git a/start.py b/start.py
deleted file mode 100755
index 353a1581..00000000
--- a/start.py
+++ /dev/null
@@ -1,133 +0,0 @@
-#!/usr/bin/env python
-
-import getpass
-import sys
-import re
-from optparse import OptionParser
-
-from minecraft import authentication
-from minecraft.exceptions import YggdrasilError
-from minecraft.networking.connection import Connection
-from minecraft.networking.packets import Packet, clientbound, serverbound
-
-
-def get_options():
- parser = OptionParser()
-
- 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("-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("-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")
-
- (options, args) = parser.parse_args()
-
- if not options.username:
- options.username = input("Enter your username: ")
-
- if not options.password and not options.offline:
- options.password = getpass.getpass("Enter your password (leave "
- "blank for offline mode): ")
- 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): ")
- # Try to split out port and address
- match = re.match(r"((?P[^\[\]:]+)|\[(?P[^\[\]]+)\])"
- r"(:(?P\d+))?$", options.server)
- if match is None:
- raise ValueError("Invalid server address: '%s'." % options.server)
- options.address = match.group("host") or match.group("addr")
- options.port = int(match.group("port") or 25565)
-
- return options
-
-
-def main():
- options = get_options()
-
- if options.offline:
- print("Connecting in offline mode...")
- connection = Connection(
- options.address, options.port, username=options.username)
- else:
- auth_token = authentication.AuthenticationToken()
- try:
- auth_token.authenticate(options.username, options.password)
- except YggdrasilError as e:
- print(e)
- sys.exit()
- print("Logged in as %s..." % auth_token.username)
- connection = Connection(
- options.address, options.port, auth_token=auth_token)
-
- 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)
- else:
- print('--> %s' % packet, file=sys.stderr)
-
- def print_outgoing(packet):
- print('<-- %s' % packet, file=sys.stderr)
-
- 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.')
-
- connection.register_packet_listener(
- handle_join_game, clientbound.play.JoinGamePacket)
-
- def print_chat(chat_packet):
- print("Message (%s): %s" % (
- chat_packet.field_string('position'), chat_packet.json_data))
-
- connection.register_packet_listener(
- print_chat, clientbound.play.ChatMessagePacket)
-
- connection.connect()
-
- while True:
- try:
- text = input()
- if text == "/respawn":
- print("respawning...")
- packet = serverbound.play.ClientStatusPacket()
- packet.action_id = serverbound.play.ClientStatusPacket.RESPAWN
- connection.write_packet(packet)
- else:
- packet = serverbound.play.ChatPacket()
- packet.message = text
- connection.write_packet(packet)
- except KeyboardInterrupt:
- print("Bye!")
- sys.exit()
-
-
-if __name__ == "__main__":
- main()