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()